FEATURE: Collect custom global metrics. (#26)

FEATURE: Collect custom global metrics. (#26)

Other plugins can add custom global metrics by adding them to a DiscoursePluginRegistry filtered register.

diff --git a/README.md b/README.md
index 6f1cc16..74f12e2 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,5 @@
 # see: https://meta.discourse.org/t/prometheus-exporter-plugin-for-discourse/72666
+
+## Adding custom global collectors
+
+The global reporter can pick custom metrics added by other Discourse plugins. The metric needs to define a collect method, and the `name`, `labels`, `description`, `value`, and `type` attributes. See an example [here](https://github.com/discourse/discourse-antivirus/pull/15).
diff --git a/lib/reporter/global.rb b/lib/reporter/global.rb
index bb5cd3c..b117b4c 100644
--- a/lib/reporter/global.rb
+++ b/lib/reporter/global.rb
@@ -9,8 +9,8 @@ module DiscoursePrometheus::Reporter
 
     def self.iteration(global_collector, client)
       clear_connections!
-      metric = global_collector.collect
-      client.send_json metric
+      metrics = global_collector.collect
+      metrics.each { |metric| client.send_json(metric) }
       clear_connections!
     rescue => e
       begin
@@ -28,6 +28,7 @@ module DiscoursePrometheus::Reporter
     def self.start(client)
       @r, @w = IO.pipe
       global_collector = new
+
       Thread.new do
         while !@stopping
           iteration(global_collector, client)
@@ -44,19 +45,28 @@ module DiscoursePrometheus::Reporter
     def initialize(recycle_every: 6)
       @recycle_every = recycle_every
       @collections = 0
-      @metrics = ::DiscoursePrometheus::InternalMetric::Global.new
+      fetch_metrics
     end
 
     def collect
       if @collections >= @recycle_every
-        @metrics = ::DiscoursePrometheus::InternalMetric::Global.new
+        fetch_metrics
         @collections = 0
       else
         @collections += 1
       end
 
-      @metrics.collect
+      @metrics.each(&:collect)
       @metrics
     end
+
+    private
+
+    def fetch_metrics
+      metrics = [::DiscoursePrometheus::InternalMetric::Global]
+      metrics = metrics.concat(DiscoursePluginRegistry.global_collectors)
+
+      @metrics = metrics.map(&:new)
+    end
   end
 end
diff --git a/plugin.rb b/plugin.rb
index 34ea198..632a01e 100644
--- a/plugin.rb
+++ b/plugin.rb
@@ -31,6 +31,7 @@ require_relative("lib/middleware/metrics")
 
 GlobalSetting.add_default :prometheus_collector_port, 9405
 GlobalSetting.add_default :prometheus_trusted_ip_allowlist_regex, ''
+DiscoursePluginRegistry.define_filtered_register :global_collectors
 
 Rails.configuration.middleware.unshift DiscoursePrometheus::Middleware::Metrics
 
diff --git a/spec/lib/reporter/global_spec.rb b/spec/lib/reporter/global_spec.rb
index 98cb600..d526cda 100644
--- a/spec/lib/reporter/global_spec.rb
+++ b/spec/lib/reporter/global_spec.rb
@@ -1,6 +1,7 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
+require_relative '../../support/null_metric'
 
 module DiscoursePrometheus
   describe Reporter::Global do
@@ -12,15 +13,15 @@ module DiscoursePrometheus
 
     it "Can collect gc stats" do
       collector = Reporter::Global.new(recycle_every: 2)
-      metric = collector.collect
+      metric = collector.collect.first
 
       expect(metric.redis_slave_available).to eq(0)
 
       id = metric.object_id
 
       # test recycling
-      expect(collector.collect.object_id).to eq(id)
-      expect(collector.collect.object_id).not_to eq(id)
+      expect(collector.collect.first.object_id).to eq(id)
+      expect(collector.collect.first.object_id).not_to eq(id)
     ensure
       metric.reset!
     end
@@ -32,7 +33,7 @@ module DiscoursePrometheus
       end
 
       it "can collect readonly data from the redis keys" do
-        metric = Reporter::Global.new.collect
+        metric = Reporter::Global.new.collect.first
 
         Discourse::READONLY_KEYS.each do |k|
           expect(metric.readonly_sites[key: k]).to eq(0)
@@ -40,7 +41,7 @@ module DiscoursePrometheus
 
         Discourse.enable_readonly_mode(Discourse::PG_FORCE_READONLY_MODE_KEY)
 
-        metric = Reporter::Global.new.collect
+        metric = Reporter::Global.new.collect.first
         Discourse::READONLY_KEYS.each do |k|
           expect(metric.readonly_sites[key: k]).to eq(k == Discourse::PG_FORCE_READONLY_MODE_KEY ? 1 : 0)
         end
@@ -48,5 +49,20 @@ module DiscoursePrometheus
         metric.reset!
       end
     end
+
+    describe 'adding custom collectors' do
+      after do
+        DiscoursePluginRegistry.reset_register!(:global_collectors)
+      end
+
+      it 'collects custom metrics added to the global_collectors registry' do
+        null_metric_klass = DiscoursePrometheus::NullMetric
+        DiscoursePluginRegistry.register_global_collector(null_metric_klass, Plugin::Instance.new)
+
+        metric = Reporter::Global.new.collect.last
+
+        expect(metric.name).to eq(null_metric_klass.new.name)
+      end
+    end
   end
 end
diff --git a/spec/support/null_metric.rb b/spec/support/null_metric.rb
new file mode 100644
index 0000000..836f555
--- /dev/null
+++ b/spec/support/null_metric.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module DiscoursePrometheus
+  class NullMetric < ::DiscoursePrometheus::InternalMetric::Custom
+    attribute :name , :labels, :description, :value, :type
+
+    def initialize
+      @name = 'null_metric'
+      @description = 'Testing'
+      @type = "Gauge"
+    end
+
+    def collect
+      @value = 1
+    end
+  end
+end

GitHub sha: 00a4dd7a187c484a8c23d7c9b1814f71b4917370

This commit appears in #26 which was approved by SamSaffron and tgxworld. It was merged by romanrizzi.