Merge pull request #454 from prometheus/beorn7/testing
Add a single-value extractor and general package doc
This commit is contained in:
commit
2dbc3a58c2
|
@ -11,6 +11,26 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// 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
|
package testutil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -27,6 +47,68 @@ import (
|
||||||
"github.com/prometheus/client_golang/prometheus/internal"
|
"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
|
// CollectAndCompare registers the provided Collector with a newly created
|
||||||
// pedantic Registry. It then does the same as GatherAndCompare, gathering the
|
// pedantic Registry. It then does the same as GatherAndCompare, gathering the
|
||||||
// metrics from the pedantic Registry.
|
// metrics from the pedantic Registry.
|
||||||
|
|
|
@ -20,6 +20,104 @@ import (
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"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) {
|
func TestCollectAndCompare(t *testing.T) {
|
||||||
const metadata = `
|
const metadata = `
|
||||||
# HELP some_total A value that represents a counter.
|
# HELP some_total A value that represents a counter.
|
||||||
|
|
Loading…
Reference in New Issue