Skip to content

Commit 00d5f9e

Browse files
authored
Merge pull request #743 from prometheus/beorn7/test
Add linter helpers to testutil
2 parents 2f196c7 + dc79bd6 commit 00d5f9e

File tree

4 files changed

+166
-27
lines changed

4 files changed

+166
-27
lines changed

prometheus/testutil/lint.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2020 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
15+
16+
import (
17+
"fmt"
18+
19+
"github.com/prometheus/client_golang/prometheus"
20+
"github.com/prometheus/client_golang/prometheus/testutil/promlint"
21+
)
22+
23+
// CollectAndLint registers the provided Collector with a newly created pedantic
24+
// Registry. It then calls GatherAndLint with that Registry and with the
25+
// provided metricNames.
26+
func CollectAndLint(c prometheus.Collector, metricNames ...string) ([]promlint.Problem, error) {
27+
reg := prometheus.NewPedanticRegistry()
28+
if err := reg.Register(c); err != nil {
29+
return nil, fmt.Errorf("registering collector failed: %s", err)
30+
}
31+
return GatherAndLint(reg, metricNames...)
32+
}
33+
34+
// GatherAndLint gathers all metrics from the provided Gatherer and checks them
35+
// with the linter in the promlint package. If any metricNames are provided,
36+
// only metrics with those names are checked.
37+
func GatherAndLint(g prometheus.Gatherer, metricNames ...string) ([]promlint.Problem, error) {
38+
got, err := g.Gather()
39+
if err != nil {
40+
return nil, fmt.Errorf("gathering metrics failed: %s", err)
41+
}
42+
if metricNames != nil {
43+
got = filterMetrics(got, metricNames)
44+
}
45+
return promlint.NewWithMetricFamilies(got).Lint()
46+
}

prometheus/testutil/lint_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright 2020 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
15+
16+
import (
17+
"testing"
18+
19+
"github.com/prometheus/client_golang/prometheus"
20+
)
21+
22+
func TestCollectAndLintGood(t *testing.T) {
23+
cnt := prometheus.NewCounterVec(
24+
prometheus.CounterOpts{
25+
Name: "some_total",
26+
Help: "A value that represents a counter.",
27+
ConstLabels: prometheus.Labels{
28+
"label1": "value1",
29+
},
30+
},
31+
[]string{"foo"},
32+
)
33+
cnt.WithLabelValues("bar")
34+
cnt.WithLabelValues("baz")
35+
36+
problems, err := CollectAndLint(cnt)
37+
if err != nil {
38+
t.Error("Unexpected error:", err)
39+
}
40+
if len(problems) > 0 {
41+
t.Error("Unexpected lint problems:", problems)
42+
}
43+
}
44+
45+
func TestCollectAndLintBad(t *testing.T) {
46+
cnt := prometheus.NewCounterVec(
47+
prometheus.CounterOpts{
48+
Name: "someThing_ms",
49+
Help: "A value that represents a counter.",
50+
ConstLabels: prometheus.Labels{
51+
"label1": "value1",
52+
},
53+
},
54+
[]string{"fooBar"},
55+
)
56+
cnt.WithLabelValues("bar")
57+
cnt.WithLabelValues("baz")
58+
59+
problems, err := CollectAndLint(cnt)
60+
if err != nil {
61+
t.Error("Unexpected error:", err)
62+
}
63+
if len(problems) < 5 {
64+
// The exact nature of the lint problems found is tested within
65+
// the promlint package itself. Here we only want to make sure
66+
// that the collector successfully hits the linter and that at
67+
// least the five problems that the linter could recognize at
68+
// the time of writing this test are flagged.
69+
t.Error("Not enough lint problems found.")
70+
}
71+
}

prometheus/testutil/promlint/promlint.go

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,13 @@ import (
2929
// A Linter is a Prometheus metrics linter. It identifies issues with metric
3030
// names, types, and metadata, and reports them to the caller.
3131
type Linter struct {
32-
r io.Reader
32+
// The linter will read metrics in the Prometheus text format from r and
33+
// then lint it, _and_ it will lint the metrics provided directly as
34+
// MetricFamily proto messages in mfs. Note, however, that the current
35+
// constructor functions New and NewWithMetricFamilies only ever set one
36+
// of them.
37+
r io.Reader
38+
mfs []*dto.MetricFamily
3339
}
3440

3541
// A Problem is an issue detected by a Linter.
@@ -42,40 +48,52 @@ type Problem struct {
4248
}
4349

4450
// newProblem is helper function to create a Problem.
45-
func newProblem(mf dto.MetricFamily, text string) Problem {
51+
func newProblem(mf *dto.MetricFamily, text string) Problem {
4652
return Problem{
4753
Metric: mf.GetName(),
4854
Text: text,
4955
}
5056
}
5157

52-
// New creates a new Linter that reads an input stream of Prometheus metrics.
53-
// Only the Prometheus text exposition format is supported.
58+
// New creates a new Linter that reads an input stream of Prometheus metrics in
59+
// the Prometheus text exposition format.
5460
func New(r io.Reader) *Linter {
5561
return &Linter{
5662
r: r,
5763
}
5864
}
5965

66+
// NewWithMetricFamilies creates a new Linter that reads from a slice of
67+
// MetricFamily protobuf messages.
68+
func NewWithMetricFamilies(mfs []*dto.MetricFamily) *Linter {
69+
return &Linter{
70+
mfs: mfs,
71+
}
72+
}
73+
6074
// Lint performs a linting pass, returning a slice of Problems indicating any
61-
// issues found in the metrics stream. The slice is sorted by metric name
75+
// issues found in the metrics stream. The slice is sorted by metric name
6276
// and issue description.
6377
func (l *Linter) Lint() ([]Problem, error) {
64-
// TODO(mdlayher): support for protobuf exposition format?
65-
d := expfmt.NewDecoder(l.r, expfmt.FmtText)
66-
6778
var problems []Problem
6879

69-
var mf dto.MetricFamily
70-
for {
71-
if err := d.Decode(&mf); err != nil {
72-
if err == io.EOF {
73-
break
80+
if l.r != nil {
81+
d := expfmt.NewDecoder(l.r, expfmt.FmtText)
82+
83+
mf := &dto.MetricFamily{}
84+
for {
85+
if err := d.Decode(mf); err != nil {
86+
if err == io.EOF {
87+
break
88+
}
89+
90+
return nil, err
7491
}
7592

76-
return nil, err
93+
problems = append(problems, lint(mf)...)
7794
}
78-
95+
}
96+
for _, mf := range l.mfs {
7997
problems = append(problems, lint(mf)...)
8098
}
8199

@@ -91,8 +109,8 @@ func (l *Linter) Lint() ([]Problem, error) {
91109
}
92110

93111
// lint is the entry point for linting a single metric.
94-
func lint(mf dto.MetricFamily) []Problem {
95-
fns := []func(mf dto.MetricFamily) []Problem{
112+
func lint(mf *dto.MetricFamily) []Problem {
113+
fns := []func(mf *dto.MetricFamily) []Problem{
96114
lintHelp,
97115
lintMetricUnits,
98116
lintCounter,
@@ -113,7 +131,7 @@ func lint(mf dto.MetricFamily) []Problem {
113131
}
114132

115133
// lintHelp detects issues related to the help text for a metric.
116-
func lintHelp(mf dto.MetricFamily) []Problem {
134+
func lintHelp(mf *dto.MetricFamily) []Problem {
117135
var problems []Problem
118136

119137
// Expect all metrics to have help text available.
@@ -125,7 +143,7 @@ func lintHelp(mf dto.MetricFamily) []Problem {
125143
}
126144

127145
// lintMetricUnits detects issues with metric unit names.
128-
func lintMetricUnits(mf dto.MetricFamily) []Problem {
146+
func lintMetricUnits(mf *dto.MetricFamily) []Problem {
129147
var problems []Problem
130148

131149
unit, base, ok := metricUnits(*mf.Name)
@@ -146,7 +164,7 @@ func lintMetricUnits(mf dto.MetricFamily) []Problem {
146164

147165
// lintCounter detects issues specific to counters, as well as patterns that should
148166
// only be used with counters.
149-
func lintCounter(mf dto.MetricFamily) []Problem {
167+
func lintCounter(mf *dto.MetricFamily) []Problem {
150168
var problems []Problem
151169

152170
isCounter := mf.GetType() == dto.MetricType_COUNTER
@@ -165,7 +183,7 @@ func lintCounter(mf dto.MetricFamily) []Problem {
165183

166184
// lintHistogramSummaryReserved detects when other types of metrics use names or labels
167185
// reserved for use by histograms and/or summaries.
168-
func lintHistogramSummaryReserved(mf dto.MetricFamily) []Problem {
186+
func lintHistogramSummaryReserved(mf *dto.MetricFamily) []Problem {
169187
// These rules do not apply to untyped metrics.
170188
t := mf.GetType()
171189
if t == dto.MetricType_UNTYPED {
@@ -206,7 +224,7 @@ func lintHistogramSummaryReserved(mf dto.MetricFamily) []Problem {
206224
}
207225

208226
// lintMetricTypeInName detects when metric types are included in the metric name.
209-
func lintMetricTypeInName(mf dto.MetricFamily) []Problem {
227+
func lintMetricTypeInName(mf *dto.MetricFamily) []Problem {
210228
var problems []Problem
211229
n := strings.ToLower(mf.GetName())
212230

@@ -224,7 +242,7 @@ func lintMetricTypeInName(mf dto.MetricFamily) []Problem {
224242
}
225243

226244
// lintReservedChars detects colons in metric names.
227-
func lintReservedChars(mf dto.MetricFamily) []Problem {
245+
func lintReservedChars(mf *dto.MetricFamily) []Problem {
228246
var problems []Problem
229247
if strings.Contains(mf.GetName(), ":") {
230248
problems = append(problems, newProblem(mf, "metric names should not contain ':'"))
@@ -235,7 +253,7 @@ func lintReservedChars(mf dto.MetricFamily) []Problem {
235253
var camelCase = regexp.MustCompile(`[a-z][A-Z]`)
236254

237255
// lintCamelCase detects metric names and label names written in camelCase.
238-
func lintCamelCase(mf dto.MetricFamily) []Problem {
256+
func lintCamelCase(mf *dto.MetricFamily) []Problem {
239257
var problems []Problem
240258
if camelCase.FindString(mf.GetName()) != "" {
241259
problems = append(problems, newProblem(mf, "metric names should be written in 'snake_case' not 'camelCase'"))
@@ -252,7 +270,7 @@ func lintCamelCase(mf dto.MetricFamily) []Problem {
252270
}
253271

254272
// lintUnitAbbreviations detects abbreviated units in the metric name.
255-
func lintUnitAbbreviations(mf dto.MetricFamily) []Problem {
273+
func lintUnitAbbreviations(mf *dto.MetricFamily) []Problem {
256274
var problems []Problem
257275
n := strings.ToLower(mf.GetName())
258276
for _, s := range unitAbbreviations {

prometheus/testutil/testutil.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
// testing custom prometheus.Collector implementations and in particular whole
3232
// exporters, i.e. programs that retrieve telemetry data from a 3rd party source
3333
// 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.
3438
package testutil
3539

3640
import (
@@ -136,8 +140,8 @@ func CollectAndCount(c prometheus.Collector) int {
136140
}
137141

138142
// CollectAndCompare registers the provided Collector with a newly created
139-
// pedantic Registry. It then does the same as GatherAndCompare, gathering the
140-
// metrics from the pedantic Registry.
143+
// pedantic Registry. It then calls GatherAndCompare with that Registry and with
144+
// the provided metricNames.
141145
func CollectAndCompare(c prometheus.Collector, expected io.Reader, metricNames ...string) error {
142146
reg := prometheus.NewPedanticRegistry()
143147
if err := reg.Register(c); err != nil {

0 commit comments

Comments
 (0)