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.