From 1301cf8fcd407a1140ebfbb6dbedb9566d31bc17 Mon Sep 17 00:00:00 2001 From: beorn7 Date: Tue, 4 Sep 2018 12:38:29 +0200 Subject: [PATCH 1/2] Add helper function to extract a simple float value from a metric Signed-off-by: beorn7 foo --- prometheus/testutil/testutil.go | 62 ++++++++++++++++++ prometheus/testutil/testutil_test.go | 98 ++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+) diff --git a/prometheus/testutil/testutil.go b/prometheus/testutil/testutil.go index dc2ce22..9e0cd4b 100644 --- a/prometheus/testutil/testutil.go +++ b/prometheus/testutil/testutil.go @@ -27,6 +27,68 @@ import ( "github.com/prometheus/client_golang/prometheus/internal" ) +// ToFloat64 collects all Metrics from the provided Collector. It expects that +// this results in exactly one Metric being collected, which must be a Gauge, +// Counter, or Untyped. In all other cases, ToFloat64 panics. ToFloat64 returns +// the value of the collected Metric. +// +// The Collector provided is typically a simple instance of Gauge or Counter, or +// – less commonly – a GaugeVec or CounterVec with exactly one element. But any +// Collector fulfilling the prerequisites described above will do. +// +// Use this function with caution. It is computationally very expensive and thus +// not suited at all to read values from Metrics in regular code. This is really +// only for testing purposes, and even for testing, other approaches are often +// more appropriate (see this package's documentation). +// +// A clear anti-pattern would be to use a metric type from the prometheus +// package to track values that are also needed for something else than the +// exposition of Prometheus metrics. For example, you would like to track the +// number of items in a queue because your code should reject queuing further +// items if a certain limit is reached. It is tempting to track the number of +// items in a prometheus.Gauge, as it is then easily available as a metric for +// exposition, too. However, then you would need to call ToFloat64 in your +// regular code, potentially quite often. The recommended way is to track the +// number of items conventionally (in the way you would have done it without +// considering Prometheus metrics) and then expose the number with a +// prometheus.GaugeFunc. +func ToFloat64(c prometheus.Collector) float64 { + var ( + m prometheus.Metric + mCount int + mChan = make(chan prometheus.Metric) + done = make(chan struct{}) + ) + + go func() { + for m = range mChan { + mCount++ + } + close(done) + }() + + c.Collect(mChan) + close(mChan) + <-done + + if mCount != 1 { + panic(fmt.Errorf("collected %d metrics instead of exactly 1", mCount)) + } + + pb := &dto.Metric{} + m.Write(pb) + if pb.Gauge != nil { + return pb.Gauge.GetValue() + } + if pb.Counter != nil { + return pb.Counter.GetValue() + } + if pb.Untyped != nil { + return pb.Untyped.GetValue() + } + panic(fmt.Errorf("collected a non-gauge/counter/untyped metric: %s", pb)) +} + // CollectAndCompare registers the provided Collector with a newly created // pedantic Registry. It then does the same as GatherAndCompare, gathering the // metrics from the pedantic Registry. diff --git a/prometheus/testutil/testutil_test.go b/prometheus/testutil/testutil_test.go index b4e7b01..e25b130 100644 --- a/prometheus/testutil/testutil_test.go +++ b/prometheus/testutil/testutil_test.go @@ -20,6 +20,104 @@ import ( "github.com/prometheus/client_golang/prometheus" ) +type untypedCollector struct{} + +func (u untypedCollector) Describe(c chan<- *prometheus.Desc) { + c <- prometheus.NewDesc("name", "help", nil, nil) +} + +func (u untypedCollector) Collect(c chan<- prometheus.Metric) { + c <- prometheus.MustNewConstMetric( + prometheus.NewDesc("name", "help", nil, nil), + prometheus.UntypedValue, + 2001, + ) +} + +func TestToFloat64(t *testing.T) { + gaugeWithAValueSet := prometheus.NewGauge(prometheus.GaugeOpts{}) + gaugeWithAValueSet.Set(3.14) + + counterVecWithOneElement := prometheus.NewCounterVec(prometheus.CounterOpts{}, []string{"foo"}) + counterVecWithOneElement.WithLabelValues("bar").Inc() + + counterVecWithTwoElements := prometheus.NewCounterVec(prometheus.CounterOpts{}, []string{"foo"}) + counterVecWithTwoElements.WithLabelValues("bar").Add(42) + counterVecWithTwoElements.WithLabelValues("baz").Inc() + + histogramVecWithOneElement := prometheus.NewHistogramVec(prometheus.HistogramOpts{}, []string{"foo"}) + histogramVecWithOneElement.WithLabelValues("bar").Observe(2.7) + + scenarios := map[string]struct { + collector prometheus.Collector + panics bool + want float64 + }{ + "simple counter": { + collector: prometheus.NewCounter(prometheus.CounterOpts{}), + panics: false, + want: 0, + }, + "simple gauge": { + collector: prometheus.NewGauge(prometheus.GaugeOpts{}), + panics: false, + want: 0, + }, + "simple untyped": { + collector: untypedCollector{}, + panics: false, + want: 2001, + }, + "simple histogram": { + collector: prometheus.NewHistogram(prometheus.HistogramOpts{}), + panics: true, + }, + "simple summary": { + collector: prometheus.NewSummary(prometheus.SummaryOpts{}), + panics: true, + }, + "simple gauge with an actual value set": { + collector: gaugeWithAValueSet, + panics: false, + want: 3.14, + }, + "counter vec with zero elements": { + collector: prometheus.NewCounterVec(prometheus.CounterOpts{}, nil), + panics: true, + }, + "counter vec with one element": { + collector: counterVecWithOneElement, + panics: false, + want: 1, + }, + "counter vec with two elements": { + collector: counterVecWithTwoElements, + panics: true, + }, + "histogram vec with one element": { + collector: histogramVecWithOneElement, + panics: true, + }, + } + + for n, s := range scenarios { + t.Run(n, func(t *testing.T) { + defer func() { + r := recover() + if r == nil && s.panics { + t.Error("expected panic") + } else if r != nil && !s.panics { + t.Error("unexpected panic: ", r) + } + // Any other combination is the expected outcome. + }() + if got := ToFloat64(s.collector); got != s.want { + t.Errorf("want %f, got %f", s.want, got) + } + }) + } +} + func TestCollectAndCompare(t *testing.T) { const metadata = ` # HELP some_total A value that represents a counter. From 74a2f46d2c245f6b2ada0065e9c44b474685b007 Mon Sep 17 00:00:00 2001 From: beorn7 Date: Tue, 4 Sep 2018 12:59:32 +0200 Subject: [PATCH 2/2] Add package documentation Signed-off-by: beorn7 --- prometheus/testutil/testutil.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/prometheus/testutil/testutil.go b/prometheus/testutil/testutil.go index 9e0cd4b..d148af9 100644 --- a/prometheus/testutil/testutil.go +++ b/prometheus/testutil/testutil.go @@ -11,6 +11,26 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package testutil provides helpers to test code using the prometheus package +// of client_golang. +// +// While writing unit tests to verify correct instrumentation of your code, it's +// a common mistake to mostly test the instrumentation library instead of your +// own code. Rather than verifying that a prometheus.Counter's value has changed +// as expected or that it shows up in the exposition after registration, it is +// in general more robust and more faithful to the concept of unit tests to use +// mock implementations of the prometheus.Counter and prometheus.Registerer +// interfaces that simply assert that the Add or Register methods have been +// called with the expected arguments. However, this might be overkill in simple +// scenarios. The ToFloat64 function is provided for simple inspection of a +// single-value metric, but it has to be used with caution. +// +// End-to-end tests to verify all or larger parts of the metrics exposition can +// be implemented with the CollectAndCompare or GatherAndCompare functions. The +// most appropriate use is not so much testing instrumentation of your code, but +// testing custom prometheus.Collector implementations and in particular whole +// exporters, i.e. programs that retrieve telemetry data from a 3rd party source +// and convert it into Prometheus metrics. package testutil import (