FEATURE: support custom metrics

FEATURE: support custom metrics

This change allows reporting of custom metrics to the collector
so all sorts of external processes can add numbers if they wish

From 8c1d55b43be44548ae2e2cf24572ff9534656a8d Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Thu, 22 Nov 2018 14:45:31 +1100
Subject: [PATCH] FEATURE: support custom metrics

This change allows reporting of custom metrics to the collector
so all sorts of external processes can add numbers if they wish

diff --git a/lib/collector.rb b/lib/collector.rb
index f14a69a..9ab96cd 100644
--- a/lib/collector.rb
+++ b/lib/collector.rb
@@ -26,6 +26,8 @@ module ::DiscoursePrometheus
 
       @process_metrics = []
       @global_metrics = []
+
+      @custom_metrics = nil
     end
 
     def process(str)
@@ -40,6 +42,8 @@ module ::DiscoursePrometheus
         process_job(metric)
       elsif InternalMetric::Global === metric
         process_global(metric)
+      elsif InternalMetric::Custom === metric
+        process_custom(metric)
       end
     end
 
@@ -47,6 +51,31 @@ module ::DiscoursePrometheus
       prometheus_metrics.map(&:to_prometheus_text).join("\n")
     end
 
+    def process_custom(metric)
+      obj = ensure_custom_metric(metric)
+      if Counter === obj
+        obj.observe(metric.value || 1, metric.labels)
+      elsif Gauge === obj
+        obj.observe(metric.value, metric.labels)
+      end
+    end
+
+    def ensure_custom_metric(metric)
+      @custom_metrics ||= {}
+      if !(obj = @custom_metrics[metric.name])
+        if metric.type == "Counter"
+          obj = Counter.new(metric.name, metric.description)
+        elsif metric.type == "Gauge"
+          obj = Gauge.new(metric.name, metric.description)
+        else
+          raise ApplicationError, "Unknown metric type #{metric.type}"
+        end
+        @custom_metrics[metric.name] = obj
+      end
+
+      obj
+    end
+
     def process_global(metric)
       ensure_global_metrics
       @global_metrics.each do |gauge|
@@ -220,7 +249,11 @@ module ::DiscoursePrometheus
     end
 
     def prometheus_metrics
-      web_metrics + process_metrics + job_metrics + @global_metrics
+      metrics = web_metrics + process_metrics + job_metrics + @global_metrics
+      if @custom_metrics
+        metrics += @custom_metrics.values
+      end
+      metrics
     end
 
     private
diff --git a/lib/internal_metric/base.rb b/lib/internal_metric/base.rb
index 96cc3ff..9f1faa8 100644
--- a/lib/internal_metric/base.rb
+++ b/lib/internal_metric/base.rb
@@ -23,6 +23,8 @@ module DiscoursePrometheus::InternalMetric
           Web
         when "Process"
           Process
+        when "Custom"
+          Custom
         else
           raise "class deserialization not implemented"
         end
diff --git a/lib/internal_metric/custom.rb b/lib/internal_metric/custom.rb
new file mode 100644
index 0000000..19670af
--- /dev/null
+++ b/lib/internal_metric/custom.rb
@@ -0,0 +1,5 @@
+module DiscoursePrometheus::InternalMetric
+  class Custom < Base
+    attribute :name , :labels, :description, :value, :type
+  end
+end
diff --git a/plugin.rb b/plugin.rb
index ba90af1..6e06533 100644
--- a/plugin.rb
+++ b/plugin.rb
@@ -16,6 +16,7 @@ require_relative("lib/internal_metric/global")
 require_relative("lib/internal_metric/job")
 require_relative("lib/internal_metric/process")
 require_relative("lib/internal_metric/web")
+require_relative("lib/internal_metric/custom")
 
 require_relative("lib/reporter/process")
 require_relative("lib/reporter/global")
@@ -53,11 +54,11 @@ after_initialize do
     end
 
     DiscoursePrometheus::Reporter::Global.start($prometheus_client)
-  end
 
-  # in dev we use puma and it runs in a single process
-  if Rails.env == "development"
-    DiscoursePrometheus::Reporter::Process.start($prometheus_client, :web)
+    # in dev we may use puma and it runs in a single process
+    if Rails.env == "development"
+      DiscoursePrometheus::Reporter::Process.start($prometheus_client, :web)
+    end
   end
 
   DiscourseEvent.on(:sidekiq_fork_started) do
diff --git a/prometheus_exporter_version b/prometheus_exporter_version
index 0d91a54..267577d 100644
--- a/prometheus_exporter_version
+++ b/prometheus_exporter_version
@@ -1 +1 @@
-0.3.0
+0.4.1
diff --git a/spec/lib/collector_spec.rb b/spec/lib/collector_spec.rb
index 0f53d89..0633f9b 100644
--- a/spec/lib/collector_spec.rb
+++ b/spec/lib/collector_spec.rb
@@ -5,6 +5,48 @@ require_relative '../../lib/collector'
 module DiscoursePrometheus
   describe Collector do
 
+    it "Can process custom metrics" do
+      collector = Collector.new
+
+      collector.process(<<~METRIC)
+        {
+          "_type": "Custom",
+          "name": "counter",
+          "description": "some description",
+          "value": 2,
+          "type": "Counter"
+        }
+      METRIC
+
+      collector.process(<<~METRIC)
+        {
+          "_type": "Custom",
+          "name": "counter",
+          "description": "some description",
+          "type": "Counter"
+        }
+      METRIC
+
+      collector.process(<<~METRIC)
+        {
+          "_type": "Custom",
+          "name": "gauge",
+          "labels": { "test": "super" },
+          "description": "some description",
+          "value": 122.1,
+          "type": "Gauge"
+        }
+      METRIC
+
+      metrics = collector.prometheus_metrics
+
+      counter = metrics.find { |m| m.name == "counter" }
+      gauge = metrics.find { |m| m.name == "gauge" }
+
+      expect(gauge.data).to eq({ "test" => "super" } => 122.1)
+      expect(counter.data).to eq(nil => 3)
+    end
+
     it "Can handle scheduled job metrics" do
       collector = Collector.new
       metric = InternalMetric::Job.new

GitHub