Add histogram metric type (#39)

add histogram metric type (#39)

  • add histogram metric type

  • support custom buckets in Histogram

  • use plus string in summary too

  • add histogram example to readme

  • better name for default buckets

From f538e19ef9a6c02da02f508484cdc597d0210cb2 Mon Sep 17 00:00:00 2001
From: Jonathan Stern <jonathan.a.stern@gmail.com>
Date: Wed, 17 Oct 2018 21:18:34 -0500
Subject: [PATCH] add histogram metric type (#39)

* add histogram metric type

* support custom buckets in Histogram

* use plus string in summary too

* add histogram example to readme

* better name for default buckets

diff --git a/README.md b/README.md
index 78b588e..32b535e 100644
--- a/README.md
+++ b/README.md
@@ -61,10 +61,12 @@ server.start
 gauge = PrometheusExporter::Metric::Gauge.new("rss", "used RSS for process")
 counter = PrometheusExporter::Metric::Counter.new("web_requests", "number of web requests")
 summary = PrometheusExporter::Metric::Summary.new("page_load_time", "time it took to load page")
+histogram = PrometheusExporter::Metric::Histogram.new("api_access_time", "time it took to call api")
 
 server.collector.register_metric(gauge)
 server.collector.register_metric(counter)
 server.collector.register_metric(summary)
+server.collector.register_metric(histogram)
 
 gauge.observe(get_rss)
 gauge.observe(get_rss)
@@ -76,6 +78,8 @@ summary.observe(1.1)
 summary.observe(1.12)
 summary.observe(0.12)
 
+histogram.observe(0.2, api: 'twitter')
+
 # http://localhost:12345/metrics now returns all your metrics
 

diff --git a/lib/prometheus_exporter/metric.rb b/lib/prometheus_exporter/metric.rb
index 85dca63..67dca29 100644
--- a/lib/prometheus_exporter/metric.rb
+++ b/lib/prometheus_exporter/metric.rb
@@ -1,4 +1,5 @@
 require_relative "metric/base"
 require_relative "metric/counter"
 require_relative "metric/gauge"
+require_relative "metric/histogram"
 require_relative "metric/summary"
diff --git a/lib/prometheus_exporter/metric/histogram.rb b/lib/prometheus_exporter/metric/histogram.rb
new file mode 100644
index 0000000..fd4ae05
--- /dev/null
+++ b/lib/prometheus_exporter/metric/histogram.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+module PrometheusExporter::Metric
+  class Histogram < Base
+
+    DEFAULT_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5.0, 10.0].freeze
+
+    def initialize(name, help, opts = {})
+      super(name, help)
+      @buckets = (opts[:buckets] || DEFAULT_BUCKETS).sort.reverse
+      @sums = {}
+      @counts = {}
+      @observations = {}
+    end
+
+    def type
+      "histogram"
+    end
+
+    def metric_text
+      text = +""
+      first = true
+      @observations.each do |labels, buckets|
+        text << "\n" unless first
+        first = false
+        count = @counts[labels]
+        sum = @sums[labels]
+        text << "#{prefix(@name)}_bucket#{labels_text(with_bucket(labels, "+Inf"))} #{count}\n"
+        @buckets.each do |bucket|
+          value = @observations[labels][bucket]
+          text << "#{prefix(@name)}_bucket#{labels_text(with_bucket(labels, bucket.to_s))} #{value}\n"
+        end
+        text << "#{prefix(@name)}_count#{labels_text(labels)} #{count}\n"
+        text << "#{prefix(@name)}_sum#{labels_text(labels)} #{sum}"
+      end
+      text
+    end
+
+    def observe(value, labels = nil)
+      labels ||= {}
+      buckets = ensure_histogram(labels)
+
+      value = value.to_f
+      @sums[labels] += value
+      @counts[labels] += 1
+
+      fill_buckets(value, buckets)
+    end
+
+    def ensure_histogram(labels)
+      @sums[labels] ||= 0.0
+      @counts[labels] ||= 0
+      buckets = @observations[labels]
+      if buckets.nil?
+        buckets = @buckets.map{|b| [b, 0]}.to_h
+        @observations[labels] = buckets
+      end
+      buckets
+    end
+
+    def fill_buckets(value, buckets)
+      @buckets.each do |b|
+        break if value > b
+        buckets[b] += 1
+      end
+    end
+
+    def with_bucket(labels, bucket)
+      labels.merge({"le" => bucket})
+    end
+
+  end
+end
diff --git a/lib/prometheus_exporter/server/collector.rb b/lib/prometheus_exporter/server/collector.rb
index 01402ee..d251e4a 100644
--- a/lib/prometheus_exporter/server/collector.rb
+++ b/lib/prometheus_exporter/server/collector.rb
@@ -86,6 +86,8 @@ module PrometheusExporter::Server
           PrometheusExporter::Metric::Counter.new(name, help)
         when "summary"
           PrometheusExporter::Metric::Summary.new(name, help)
+        when "histogram"
+          PrometheusExporter::Metric::Histogram.new(name, help)
         end
 
       if metric
diff --git a/test/metric/histogram_test.rb b/test/metric/histogram_test.rb
new file mode 100644
index 0000000..187e2b1
--- /dev/null
+++ b/test/metric/histogram_test.rb
@@ -0,0 +1,120 @@
+require 'test_helper'
+require 'prometheus_exporter/metric'
+
+module PrometheusExporter::Metric
+  describe Histogram do
+    let :histogram do
+      Histogram.new("a_histogram", "my amazing histogram")
+    end
+
+    before do
+      Base.default_prefix = ''
+    end
+
+    it "can correctly gather a histogram" do
+      histogram.observe(0.1)
+      histogram.observe(0.2)
+      histogram.observe(0.610001)
+      histogram.observe(0.610001)
+      histogram.observe(0.610001)
+      histogram.observe(0.910001)
+      histogram.observe(0.1)
+
+      expected = <<~TEXT
+        # HELP a_histogram my amazing histogram
+        # TYPE a_histogram histogram
+        a_histogram_bucket{le="+Inf"} 7
+        a_histogram_bucket{le="10.0"} 7
+        a_histogram_bucket{le="5.0"} 7
+        a_histogram_bucket{le="2.5"} 7
+        a_histogram_bucket{le="1"} 7
+        a_histogram_bucket{le="0.5"} 3
+        a_histogram_bucket{le="0.25"} 3
+        a_histogram_bucket{le="0.1"} 2
+        a_histogram_bucket{le="0.05"} 0
+        a_histogram_bucket{le="0.025"} 0
+        a_histogram_bucket{le="0.01"} 0
+        a_histogram_bucket{le="0.005"} 0
+        a_histogram_count 7
+        a_histogram_sum 3.1400040000000002
+      TEXT
+
+      assert_equal(histogram.to_prometheus_text, expected)
+    end
+
+    it "can correctly gather a histogram over multiple labels" do
+
+      histogram.observe(0.1, nil)
+      histogram.observe(0.2)
+      histogram.observe(0.610001)
+      histogram.observe(0.610001)
+
+      histogram.observe(0.1, name: "bob", family: "skywalker")
+      histogram.observe(0.7, name: "bob", family: "skywalker")
+      histogram.observe(0.99, name: "bob", family: "skywalker")
+
+      expected = <<~TEXT
+        # HELP a_histogram my amazing histogram
+        # TYPE a_histogram histogram
+        a_histogram_bucket{le="+Inf"} 4
+        a_histogram_bucket{le="10.0"} 4
+        a_histogram_bucket{le="5.0"} 4
+        a_histogram_bucket{le="2.5"} 4
+        a_histogram_bucket{le="1"} 4
+        a_histogram_bucket{le="0.5"} 2
+        a_histogram_bucket{le="0.25"} 2
+        a_histogram_bucket{le="0.1"} 1
+        a_histogram_bucket{le="0.05"} 0
+        a_histogram_bucket{le="0.025"} 0
+        a_histogram_bucket{le="0.01"} 0
+        a_histogram_bucket{le="0.005"} 0
+        a_histogram_count 4
+        a_histogram_sum 1.520002
+        a_histogram_bucket{name="bob",family="skywalker",le="+Inf"} 3
+        a_histogram_bucket{name="bob",family="skywalker",le="10.0"} 3
+        a_histogram_bucket{name="bob",family="skywalker",le="5.0"} 3
+        a_histogram_bucket{name="bob",family="skywalker",le="2.5"} 3
+        a_histogram_bucket{name="bob",family="skywalker",le="1"} 3
+        a_histogram_bucket{name="bob",family="skywalker",le="0.5"} 1
+        a_histogram_bucket{name="bob",family="skywalker",le="0.25"} 1
+        a_histogram_bucket{name="bob",family="skywalker",le="0.1"} 1
+        a_histogram_bucket{name="bob",family="skywalker",le="0.05"} 0
+        a_histogram_bucket{name="bob",family="skywalker",le="0.025"} 0
+        a_histogram_bucket{name="bob",family="skywalker",le="0.01"} 0
+        a_histogram_bucket{name="bob",family="skywalker",le="0.005"} 0
+        a_histogram_count{name="bob",family="skywalker"} 3
+        a_histogram_sum{name="bob",family="skywalker"} 1.79
+      TEXT
+
+      assert_equal(histogram.to_prometheus_text, expected)
+    end
+
+    it "can

GitHub