Skip to content

Commit 60a8513

Browse files
Add support for custom validations in promlint (#1311)
* Refactor promlint validation structure Signed-off-by: João Vilaça <[email protected]> * Add support for custom validations in promlint Signed-off-by: João Vilaça <[email protected]> * Keep backwards compatibility Signed-off-by: João Vilaça <[email protected]> --------- Signed-off-by: João Vilaça <[email protected]>
1 parent 486d514 commit 60a8513

File tree

9 files changed

+497
-285
lines changed

9 files changed

+497
-285
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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 promlint
15+
16+
import dto "github.com/prometheus/client_model/go"
17+
18+
// A Problem is an issue detected by a linter.
19+
type Problem struct {
20+
// The name of the metric indicated by this Problem.
21+
Metric string
22+
23+
// A description of the issue for this Problem.
24+
Text string
25+
}
26+
27+
// newProblem is helper function to create a Problem.
28+
func newProblem(mf *dto.MetricFamily, text string) Problem {
29+
return Problem{
30+
Metric: mf.GetName(),
31+
Text: text,
32+
}
33+
}

prometheus/testutil/promlint/promlint.go

Lines changed: 23 additions & 285 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,11 @@ package promlint
1616

1717
import (
1818
"errors"
19-
"fmt"
2019
"io"
21-
"regexp"
2220
"sort"
23-
"strings"
24-
25-
"github.com/prometheus/common/expfmt"
2621

2722
dto "github.com/prometheus/client_model/go"
23+
"github.com/prometheus/common/expfmt"
2824
)
2925

3026
// A Linter is a Prometheus metrics linter. It identifies issues with metric
@@ -37,23 +33,8 @@ type Linter struct {
3733
// of them.
3834
r io.Reader
3935
mfs []*dto.MetricFamily
40-
}
4136

42-
// A Problem is an issue detected by a Linter.
43-
type Problem struct {
44-
// The name of the metric indicated by this Problem.
45-
Metric string
46-
47-
// A description of the issue for this Problem.
48-
Text string
49-
}
50-
51-
// newProblem is helper function to create a Problem.
52-
func newProblem(mf *dto.MetricFamily, text string) Problem {
53-
return Problem{
54-
Metric: mf.GetName(),
55-
Text: text,
56-
}
37+
customValidations []Validation
5738
}
5839

5940
// New creates a new Linter that reads an input stream of Prometheus metrics in
@@ -72,6 +53,14 @@ func NewWithMetricFamilies(mfs []*dto.MetricFamily) *Linter {
7253
}
7354
}
7455

56+
// AddCustomValidations adds custom validations to the linter.
57+
func (l *Linter) AddCustomValidations(vs ...Validation) {
58+
if l.customValidations == nil {
59+
l.customValidations = make([]Validation, 0, len(vs))
60+
}
61+
l.customValidations = append(l.customValidations, vs...)
62+
}
63+
7564
// Lint performs a linting pass, returning a slice of Problems indicating any
7665
// issues found in the metrics stream. The slice is sorted by metric name
7766
// and issue description.
@@ -91,11 +80,11 @@ func (l *Linter) Lint() ([]Problem, error) {
9180
return nil, err
9281
}
9382

94-
problems = append(problems, lint(mf)...)
83+
problems = append(problems, l.lint(mf)...)
9584
}
9685
}
9786
for _, mf := range l.mfs {
98-
problems = append(problems, lint(mf)...)
87+
problems = append(problems, l.lint(mf)...)
9988
}
10089

10190
// Ensure deterministic output.
@@ -110,276 +99,25 @@ func (l *Linter) Lint() ([]Problem, error) {
11099
}
111100

112101
// lint is the entry point for linting a single metric.
113-
func lint(mf *dto.MetricFamily) []Problem {
114-
fns := []func(mf *dto.MetricFamily) []Problem{
115-
lintHelp,
116-
lintMetricUnits,
117-
lintCounter,
118-
lintHistogramSummaryReserved,
119-
lintMetricTypeInName,
120-
lintReservedChars,
121-
lintCamelCase,
122-
lintUnitAbbreviations,
123-
}
124-
125-
var problems []Problem
126-
for _, fn := range fns {
127-
problems = append(problems, fn(mf)...)
128-
}
129-
130-
// TODO(mdlayher): lint rules for specific metrics types.
131-
return problems
132-
}
133-
134-
// lintHelp detects issues related to the help text for a metric.
135-
func lintHelp(mf *dto.MetricFamily) []Problem {
102+
func (l *Linter) lint(mf *dto.MetricFamily) []Problem {
136103
var problems []Problem
137104

138-
// Expect all metrics to have help text available.
139-
if mf.Help == nil {
140-
problems = append(problems, newProblem(mf, "no help text"))
141-
}
142-
143-
return problems
144-
}
145-
146-
// lintMetricUnits detects issues with metric unit names.
147-
func lintMetricUnits(mf *dto.MetricFamily) []Problem {
148-
var problems []Problem
149-
150-
unit, base, ok := metricUnits(*mf.Name)
151-
if !ok {
152-
// No known units detected.
153-
return nil
154-
}
155-
156-
// Unit is already a base unit.
157-
if unit == base {
158-
return nil
159-
}
160-
161-
problems = append(problems, newProblem(mf, fmt.Sprintf("use base unit %q instead of %q", base, unit)))
162-
163-
return problems
164-
}
165-
166-
// lintCounter detects issues specific to counters, as well as patterns that should
167-
// only be used with counters.
168-
func lintCounter(mf *dto.MetricFamily) []Problem {
169-
var problems []Problem
170-
171-
isCounter := mf.GetType() == dto.MetricType_COUNTER
172-
isUntyped := mf.GetType() == dto.MetricType_UNTYPED
173-
hasTotalSuffix := strings.HasSuffix(mf.GetName(), "_total")
174-
175-
switch {
176-
case isCounter && !hasTotalSuffix:
177-
problems = append(problems, newProblem(mf, `counter metrics should have "_total" suffix`))
178-
case !isUntyped && !isCounter && hasTotalSuffix:
179-
problems = append(problems, newProblem(mf, `non-counter metrics should not have "_total" suffix`))
180-
}
181-
182-
return problems
183-
}
184-
185-
// lintHistogramSummaryReserved detects when other types of metrics use names or labels
186-
// reserved for use by histograms and/or summaries.
187-
func lintHistogramSummaryReserved(mf *dto.MetricFamily) []Problem {
188-
// These rules do not apply to untyped metrics.
189-
t := mf.GetType()
190-
if t == dto.MetricType_UNTYPED {
191-
return nil
192-
}
193-
194-
var problems []Problem
195-
196-
isHistogram := t == dto.MetricType_HISTOGRAM
197-
isSummary := t == dto.MetricType_SUMMARY
198-
199-
n := mf.GetName()
200-
201-
if !isHistogram && strings.HasSuffix(n, "_bucket") {
202-
problems = append(problems, newProblem(mf, `non-histogram metrics should not have "_bucket" suffix`))
203-
}
204-
if !isHistogram && !isSummary && strings.HasSuffix(n, "_count") {
205-
problems = append(problems, newProblem(mf, `non-histogram and non-summary metrics should not have "_count" suffix`))
206-
}
207-
if !isHistogram && !isSummary && strings.HasSuffix(n, "_sum") {
208-
problems = append(problems, newProblem(mf, `non-histogram and non-summary metrics should not have "_sum" suffix`))
209-
}
210-
211-
for _, m := range mf.GetMetric() {
212-
for _, l := range m.GetLabel() {
213-
ln := l.GetName()
214-
215-
if !isHistogram && ln == "le" {
216-
problems = append(problems, newProblem(mf, `non-histogram metrics should not have "le" label`))
217-
}
218-
if !isSummary && ln == "quantile" {
219-
problems = append(problems, newProblem(mf, `non-summary metrics should not have "quantile" label`))
220-
}
221-
}
222-
}
223-
224-
return problems
225-
}
226-
227-
// lintMetricTypeInName detects when metric types are included in the metric name.
228-
func lintMetricTypeInName(mf *dto.MetricFamily) []Problem {
229-
var problems []Problem
230-
n := strings.ToLower(mf.GetName())
231-
232-
for i, t := range dto.MetricType_name {
233-
if i == int32(dto.MetricType_UNTYPED) {
234-
continue
105+
for _, fn := range defaultValidations {
106+
errs := fn(mf)
107+
for _, err := range errs {
108+
problems = append(problems, newProblem(mf, err.Error()))
235109
}
236-
237-
typename := strings.ToLower(t)
238-
if strings.Contains(n, "_"+typename+"_") || strings.HasSuffix(n, "_"+typename) {
239-
problems = append(problems, newProblem(mf, fmt.Sprintf(`metric name should not include type '%s'`, typename)))
240-
}
241-
}
242-
return problems
243-
}
244-
245-
// lintReservedChars detects colons in metric names.
246-
func lintReservedChars(mf *dto.MetricFamily) []Problem {
247-
var problems []Problem
248-
if strings.Contains(mf.GetName(), ":") {
249-
problems = append(problems, newProblem(mf, "metric names should not contain ':'"))
250-
}
251-
return problems
252-
}
253-
254-
var camelCase = regexp.MustCompile(`[a-z][A-Z]`)
255-
256-
// lintCamelCase detects metric names and label names written in camelCase.
257-
func lintCamelCase(mf *dto.MetricFamily) []Problem {
258-
var problems []Problem
259-
if camelCase.FindString(mf.GetName()) != "" {
260-
problems = append(problems, newProblem(mf, "metric names should be written in 'snake_case' not 'camelCase'"))
261110
}
262111

263-
for _, m := range mf.GetMetric() {
264-
for _, l := range m.GetLabel() {
265-
if camelCase.FindString(l.GetName()) != "" {
266-
problems = append(problems, newProblem(mf, "label names should be written in 'snake_case' not 'camelCase'"))
112+
if l.customValidations != nil {
113+
for _, fn := range l.customValidations {
114+
errs := fn(mf)
115+
for _, err := range errs {
116+
problems = append(problems, newProblem(mf, err.Error()))
267117
}
268118
}
269119
}
270-
return problems
271-
}
272120

273-
// lintUnitAbbreviations detects abbreviated units in the metric name.
274-
func lintUnitAbbreviations(mf *dto.MetricFamily) []Problem {
275-
var problems []Problem
276-
n := strings.ToLower(mf.GetName())
277-
for _, s := range unitAbbreviations {
278-
if strings.Contains(n, "_"+s+"_") || strings.HasSuffix(n, "_"+s) {
279-
problems = append(problems, newProblem(mf, "metric names should not contain abbreviated units"))
280-
}
281-
}
121+
// TODO(mdlayher): lint rules for specific metrics types.
282122
return problems
283123
}
284-
285-
// metricUnits attempts to detect known unit types used as part of a metric name,
286-
// e.g. "foo_bytes_total" or "bar_baz_milligrams".
287-
func metricUnits(m string) (unit, base string, ok bool) {
288-
ss := strings.Split(m, "_")
289-
290-
for _, s := range ss {
291-
if base, found := units[s]; found {
292-
return s, base, true
293-
}
294-
295-
for _, p := range unitPrefixes {
296-
if strings.HasPrefix(s, p) {
297-
if base, found := units[s[len(p):]]; found {
298-
return s, base, true
299-
}
300-
}
301-
}
302-
}
303-
304-
return "", "", false
305-
}
306-
307-
// Units and their possible prefixes recognized by this library. More can be
308-
// added over time as needed.
309-
var (
310-
// map a unit to the appropriate base unit.
311-
units = map[string]string{
312-
// Base units.
313-
"amperes": "amperes",
314-
"bytes": "bytes",
315-
"celsius": "celsius", // Also allow Celsius because it is common in typical Prometheus use cases.
316-
"grams": "grams",
317-
"joules": "joules",
318-
"kelvin": "kelvin", // SI base unit, used in special cases (e.g. color temperature, scientific measurements).
319-
"meters": "meters", // Both American and international spelling permitted.
320-
"metres": "metres",
321-
"seconds": "seconds",
322-
"volts": "volts",
323-
324-
// Non base units.
325-
// Time.
326-
"minutes": "seconds",
327-
"hours": "seconds",
328-
"days": "seconds",
329-
"weeks": "seconds",
330-
// Temperature.
331-
"kelvins": "kelvin",
332-
"fahrenheit": "celsius",
333-
"rankine": "celsius",
334-
// Length.
335-
"inches": "meters",
336-
"yards": "meters",
337-
"miles": "meters",
338-
// Bytes.
339-
"bits": "bytes",
340-
// Energy.
341-
"calories": "joules",
342-
// Mass.
343-
"pounds": "grams",
344-
"ounces": "grams",
345-
}
346-
347-
unitPrefixes = []string{
348-
"pico",
349-
"nano",
350-
"micro",
351-
"milli",
352-
"centi",
353-
"deci",
354-
"deca",
355-
"hecto",
356-
"kilo",
357-
"kibi",
358-
"mega",
359-
"mibi",
360-
"giga",
361-
"gibi",
362-
"tera",
363-
"tebi",
364-
"peta",
365-
"pebi",
366-
}
367-
368-
// Common abbreviations that we'd like to discourage.
369-
unitAbbreviations = []string{
370-
"s",
371-
"ms",
372-
"us",
373-
"ns",
374-
"sec",
375-
"b",
376-
"kb",
377-
"mb",
378-
"gb",
379-
"tb",
380-
"pb",
381-
"m",
382-
"h",
383-
"d",
384-
}
385-
)

0 commit comments

Comments
 (0)