1// Copyright 2018 The Prometheus Authors
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14// Package testutil provides helpers to test code using the prometheus package
15// of client_golang.
16//
17// While writing unit tests to verify correct instrumentation of your code, it's
18// a common mistake to mostly test the instrumentation library instead of your
19// own code. Rather than verifying that a prometheus.Counter's value has changed
20// as expected or that it shows up in the exposition after registration, it is
21// in general more robust and more faithful to the concept of unit tests to use
22// mock implementations of the prometheus.Counter and prometheus.Registerer
23// interfaces that simply assert that the Add or Register methods have been
24// called with the expected arguments. However, this might be overkill in simple
25// scenarios. The ToFloat64 function is provided for simple inspection of a
26// single-value metric, but it has to be used with caution.
27//
28// End-to-end tests to verify all or larger parts of the metrics exposition can
29// be implemented with the CollectAndCompare or GatherAndCompare functions. The
30// most appropriate use is not so much testing instrumentation of your code, but
31// testing custom prometheus.Collector implementations and in particular whole
32// exporters, i.e. programs that retrieve telemetry data from a 3rd party source
33// and convert it into Prometheus metrics.
34//
35// In a similar pattern, CollectAndLint and GatherAndLint can be used to detect
36// metrics that have issues with their name, type, or metadata without being
37// necessarily invalid, e.g. a counter with a name missing the “_total” suffix.
38package testutil
39
40import (
41	"bytes"
42	"fmt"
43	"io"
44
45	"github.com/prometheus/common/expfmt"
46
47	dto "github.com/prometheus/client_model/go"
48
49	"github.com/prometheus/client_golang/prometheus"
50	"github.com/prometheus/client_golang/prometheus/internal"
51)
52
53// ToFloat64 collects all Metrics from the provided Collector. It expects that
54// this results in exactly one Metric being collected, which must be a Gauge,
55// Counter, or Untyped. In all other cases, ToFloat64 panics. ToFloat64 returns
56// the value of the collected Metric.
57//
58// The Collector provided is typically a simple instance of Gauge or Counter, or
59// – less commonly – a GaugeVec or CounterVec with exactly one element. But any
60// Collector fulfilling the prerequisites described above will do.
61//
62// Use this function with caution. It is computationally very expensive and thus
63// not suited at all to read values from Metrics in regular code. This is really
64// only for testing purposes, and even for testing, other approaches are often
65// more appropriate (see this package's documentation).
66//
67// A clear anti-pattern would be to use a metric type from the prometheus
68// package to track values that are also needed for something else than the
69// exposition of Prometheus metrics. For example, you would like to track the
70// number of items in a queue because your code should reject queuing further
71// items if a certain limit is reached. It is tempting to track the number of
72// items in a prometheus.Gauge, as it is then easily available as a metric for
73// exposition, too. However, then you would need to call ToFloat64 in your
74// regular code, potentially quite often. The recommended way is to track the
75// number of items conventionally (in the way you would have done it without
76// considering Prometheus metrics) and then expose the number with a
77// prometheus.GaugeFunc.
78func ToFloat64(c prometheus.Collector) float64 {
79	var (
80		m      prometheus.Metric
81		mCount int
82		mChan  = make(chan prometheus.Metric)
83		done   = make(chan struct{})
84	)
85
86	go func() {
87		for m = range mChan {
88			mCount++
89		}
90		close(done)
91	}()
92
93	c.Collect(mChan)
94	close(mChan)
95	<-done
96
97	if mCount != 1 {
98		panic(fmt.Errorf("collected %d metrics instead of exactly 1", mCount))
99	}
100
101	pb := &dto.Metric{}
102	m.Write(pb)
103	if pb.Gauge != nil {
104		return pb.Gauge.GetValue()
105	}
106	if pb.Counter != nil {
107		return pb.Counter.GetValue()
108	}
109	if pb.Untyped != nil {
110		return pb.Untyped.GetValue()
111	}
112	panic(fmt.Errorf("collected a non-gauge/counter/untyped metric: %s", pb))
113}
114
115// CollectAndCount registers the provided Collector with a newly created
116// pedantic Registry. It then calls GatherAndCount with that Registry and with
117// the provided metricNames. In the unlikely case that the registration or the
118// gathering fails, this function panics. (This is inconsistent with the other
119// CollectAnd… functions in this package and has historical reasons. Changing
120// the function signature would be a breaking change and will therefore only
121// happen with the next major version bump.)
122func CollectAndCount(c prometheus.Collector, metricNames ...string) int {
123	reg := prometheus.NewPedanticRegistry()
124	if err := reg.Register(c); err != nil {
125		panic(fmt.Errorf("registering collector failed: %s", err))
126	}
127	result, err := GatherAndCount(reg, metricNames...)
128	if err != nil {
129		panic(err)
130	}
131	return result
132}
133
134// GatherAndCount gathers all metrics from the provided Gatherer and counts
135// them. It returns the number of metric children in all gathered metric
136// families together. If any metricNames are provided, only metrics with those
137// names are counted.
138func GatherAndCount(g prometheus.Gatherer, metricNames ...string) (int, error) {
139	got, err := g.Gather()
140	if err != nil {
141		return 0, fmt.Errorf("gathering metrics failed: %s", err)
142	}
143	if metricNames != nil {
144		got = filterMetrics(got, metricNames)
145	}
146
147	result := 0
148	for _, mf := range got {
149		result += len(mf.GetMetric())
150	}
151	return result, nil
152}
153
154// CollectAndCompare registers the provided Collector with a newly created
155// pedantic Registry. It then calls GatherAndCompare with that Registry and with
156// the provided metricNames.
157func CollectAndCompare(c prometheus.Collector, expected io.Reader, metricNames ...string) error {
158	reg := prometheus.NewPedanticRegistry()
159	if err := reg.Register(c); err != nil {
160		return fmt.Errorf("registering collector failed: %s", err)
161	}
162	return GatherAndCompare(reg, expected, metricNames...)
163}
164
165// GatherAndCompare gathers all metrics from the provided Gatherer and compares
166// it to an expected output read from the provided Reader in the Prometheus text
167// exposition format. If any metricNames are provided, only metrics with those
168// names are compared.
169func GatherAndCompare(g prometheus.Gatherer, expected io.Reader, metricNames ...string) error {
170	got, err := g.Gather()
171	if err != nil {
172		return fmt.Errorf("gathering metrics failed: %s", err)
173	}
174	if metricNames != nil {
175		got = filterMetrics(got, metricNames)
176	}
177	var tp expfmt.TextParser
178	wantRaw, err := tp.TextToMetricFamilies(expected)
179	if err != nil {
180		return fmt.Errorf("parsing expected metrics failed: %s", err)
181	}
182	want := internal.NormalizeMetricFamilies(wantRaw)
183
184	return compare(got, want)
185}
186
187// compare encodes both provided slices of metric families into the text format,
188// compares their string message, and returns an error if they do not match.
189// The error contains the encoded text of both the desired and the actual
190// result.
191func compare(got, want []*dto.MetricFamily) error {
192	var gotBuf, wantBuf bytes.Buffer
193	enc := expfmt.NewEncoder(&gotBuf, expfmt.FmtText)
194	for _, mf := range got {
195		if err := enc.Encode(mf); err != nil {
196			return fmt.Errorf("encoding gathered metrics failed: %s", err)
197		}
198	}
199	enc = expfmt.NewEncoder(&wantBuf, expfmt.FmtText)
200	for _, mf := range want {
201		if err := enc.Encode(mf); err != nil {
202			return fmt.Errorf("encoding expected metrics failed: %s", err)
203		}
204	}
205
206	if wantBuf.String() != gotBuf.String() {
207		return fmt.Errorf(`
208metric output does not match expectation; want:
209
210%s
211got:
212
213%s`, wantBuf.String(), gotBuf.String())
214
215	}
216	return nil
217}
218
219func filterMetrics(metrics []*dto.MetricFamily, names []string) []*dto.MetricFamily {
220	var filtered []*dto.MetricFamily
221	for _, m := range metrics {
222		for _, name := range names {
223			if m.GetName() == name {
224				filtered = append(filtered, m)
225				break
226			}
227		}
228	}
229	return filtered
230}
231