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