Skip to content

Commit a3a5923

Browse files
authored
Merge pull request #739 from RainbowMango/pr_porting_promlint
Porting promlint from prometheus/prometheus
2 parents a200f19 + 50cda50 commit a3a5923

File tree

2 files changed

+1156
-0
lines changed

2 files changed

+1156
-0
lines changed
Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
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 provides a linter for Prometheus metrics.
15+
package promlint
16+
17+
import (
18+
"fmt"
19+
"io"
20+
"regexp"
21+
"sort"
22+
"strings"
23+
24+
"github.com/prometheus/common/expfmt"
25+
26+
dto "github.com/prometheus/client_model/go"
27+
)
28+
29+
// A Linter is a Prometheus metrics linter. It identifies issues with metric
30+
// names, types, and metadata, and reports them to the caller.
31+
type Linter struct {
32+
r io.Reader
33+
}
34+
35+
// A Problem is an issue detected by a Linter.
36+
type Problem struct {
37+
// The name of the metric indicated by this Problem.
38+
Metric string
39+
40+
// A description of the issue for this Problem.
41+
Text string
42+
}
43+
44+
// newProblem is helper function to create a Problem.
45+
func newProblem(mf dto.MetricFamily, text string) Problem {
46+
return Problem{
47+
Metric: mf.GetName(),
48+
Text: text,
49+
}
50+
}
51+
52+
// New creates a new Linter that reads an input stream of Prometheus metrics.
53+
// Only the Prometheus text exposition format is supported.
54+
func New(r io.Reader) *Linter {
55+
return &Linter{
56+
r: r,
57+
}
58+
}
59+
60+
// 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
62+
// and issue description.
63+
func (l *Linter) Lint() ([]Problem, error) {
64+
// TODO(mdlayher): support for protobuf exposition format?
65+
d := expfmt.NewDecoder(l.r, expfmt.FmtText)
66+
67+
var problems []Problem
68+
69+
var mf dto.MetricFamily
70+
for {
71+
if err := d.Decode(&mf); err != nil {
72+
if err == io.EOF {
73+
break
74+
}
75+
76+
return nil, err
77+
}
78+
79+
problems = append(problems, lint(mf)...)
80+
}
81+
82+
// Ensure deterministic output.
83+
sort.SliceStable(problems, func(i, j int) bool {
84+
if problems[i].Metric == problems[j].Metric {
85+
return problems[i].Text < problems[j].Text
86+
}
87+
return problems[i].Metric < problems[j].Metric
88+
})
89+
90+
return problems, nil
91+
}
92+
93+
// lint is the entry point for linting a single metric.
94+
func lint(mf dto.MetricFamily) []Problem {
95+
fns := []func(mf dto.MetricFamily) []Problem{
96+
lintHelp,
97+
lintMetricUnits,
98+
lintCounter,
99+
lintHistogramSummaryReserved,
100+
lintMetricTypeInName,
101+
lintReservedChars,
102+
lintCamelCase,
103+
lintUnitAbbreviations,
104+
}
105+
106+
var problems []Problem
107+
for _, fn := range fns {
108+
problems = append(problems, fn(mf)...)
109+
}
110+
111+
// TODO(mdlayher): lint rules for specific metrics types.
112+
return problems
113+
}
114+
115+
// lintHelp detects issues related to the help text for a metric.
116+
func lintHelp(mf dto.MetricFamily) []Problem {
117+
var problems []Problem
118+
119+
// Expect all metrics to have help text available.
120+
if mf.Help == nil {
121+
problems = append(problems, newProblem(mf, "no help text"))
122+
}
123+
124+
return problems
125+
}
126+
127+
// lintMetricUnits detects issues with metric unit names.
128+
func lintMetricUnits(mf dto.MetricFamily) []Problem {
129+
var problems []Problem
130+
131+
unit, base, ok := metricUnits(*mf.Name)
132+
if !ok {
133+
// No known units detected.
134+
return nil
135+
}
136+
137+
// Unit is already a base unit.
138+
if unit == base {
139+
return nil
140+
}
141+
142+
problems = append(problems, newProblem(mf, fmt.Sprintf("use base unit %q instead of %q", base, unit)))
143+
144+
return problems
145+
}
146+
147+
// lintCounter detects issues specific to counters, as well as patterns that should
148+
// only be used with counters.
149+
func lintCounter(mf dto.MetricFamily) []Problem {
150+
var problems []Problem
151+
152+
isCounter := mf.GetType() == dto.MetricType_COUNTER
153+
isUntyped := mf.GetType() == dto.MetricType_UNTYPED
154+
hasTotalSuffix := strings.HasSuffix(mf.GetName(), "_total")
155+
156+
switch {
157+
case isCounter && !hasTotalSuffix:
158+
problems = append(problems, newProblem(mf, `counter metrics should have "_total" suffix`))
159+
case !isUntyped && !isCounter && hasTotalSuffix:
160+
problems = append(problems, newProblem(mf, `non-counter metrics should not have "_total" suffix`))
161+
}
162+
163+
return problems
164+
}
165+
166+
// lintHistogramSummaryReserved detects when other types of metrics use names or labels
167+
// reserved for use by histograms and/or summaries.
168+
func lintHistogramSummaryReserved(mf dto.MetricFamily) []Problem {
169+
// These rules do not apply to untyped metrics.
170+
t := mf.GetType()
171+
if t == dto.MetricType_UNTYPED {
172+
return nil
173+
}
174+
175+
var problems []Problem
176+
177+
isHistogram := t == dto.MetricType_HISTOGRAM
178+
isSummary := t == dto.MetricType_SUMMARY
179+
180+
n := mf.GetName()
181+
182+
if !isHistogram && strings.HasSuffix(n, "_bucket") {
183+
problems = append(problems, newProblem(mf, `non-histogram metrics should not have "_bucket" suffix`))
184+
}
185+
if !isHistogram && !isSummary && strings.HasSuffix(n, "_count") {
186+
problems = append(problems, newProblem(mf, `non-histogram and non-summary metrics should not have "_count" suffix`))
187+
}
188+
if !isHistogram && !isSummary && strings.HasSuffix(n, "_sum") {
189+
problems = append(problems, newProblem(mf, `non-histogram and non-summary metrics should not have "_sum" suffix`))
190+
}
191+
192+
for _, m := range mf.GetMetric() {
193+
for _, l := range m.GetLabel() {
194+
ln := l.GetName()
195+
196+
if !isHistogram && ln == "le" {
197+
problems = append(problems, newProblem(mf, `non-histogram metrics should not have "le" label`))
198+
}
199+
if !isSummary && ln == "quantile" {
200+
problems = append(problems, newProblem(mf, `non-summary metrics should not have "quantile" label`))
201+
}
202+
}
203+
}
204+
205+
return problems
206+
}
207+
208+
// lintMetricTypeInName detects when metric types are included in the metric name.
209+
func lintMetricTypeInName(mf dto.MetricFamily) []Problem {
210+
var problems []Problem
211+
n := strings.ToLower(mf.GetName())
212+
213+
for i, t := range dto.MetricType_name {
214+
if i == int32(dto.MetricType_UNTYPED) {
215+
continue
216+
}
217+
218+
typename := strings.ToLower(t)
219+
if strings.Contains(n, "_"+typename+"_") || strings.HasSuffix(n, "_"+typename) {
220+
problems = append(problems, newProblem(mf, fmt.Sprintf(`metric name should not include type '%s'`, typename)))
221+
}
222+
}
223+
return problems
224+
}
225+
226+
// lintReservedChars detects colons in metric names.
227+
func lintReservedChars(mf dto.MetricFamily) []Problem {
228+
var problems []Problem
229+
if strings.Contains(mf.GetName(), ":") {
230+
problems = append(problems, newProblem(mf, "metric names should not contain ':'"))
231+
}
232+
return problems
233+
}
234+
235+
var camelCase = regexp.MustCompile(`[a-z][A-Z]`)
236+
237+
// lintCamelCase detects metric names and label names written in camelCase.
238+
func lintCamelCase(mf dto.MetricFamily) []Problem {
239+
var problems []Problem
240+
if camelCase.FindString(mf.GetName()) != "" {
241+
problems = append(problems, newProblem(mf, "metric names should be written in 'snake_case' not 'camelCase'"))
242+
}
243+
244+
for _, m := range mf.GetMetric() {
245+
for _, l := range m.GetLabel() {
246+
if camelCase.FindString(l.GetName()) != "" {
247+
problems = append(problems, newProblem(mf, "label names should be written in 'snake_case' not 'camelCase'"))
248+
}
249+
}
250+
}
251+
return problems
252+
}
253+
254+
// lintUnitAbbreviations detects abbreviated units in the metric name.
255+
func lintUnitAbbreviations(mf dto.MetricFamily) []Problem {
256+
var problems []Problem
257+
n := strings.ToLower(mf.GetName())
258+
for _, s := range unitAbbreviations {
259+
if strings.Contains(n, "_"+s+"_") || strings.HasSuffix(n, "_"+s) {
260+
problems = append(problems, newProblem(mf, "metric names should not contain abbreviated units"))
261+
}
262+
}
263+
return problems
264+
}
265+
266+
// metricUnits attempts to detect known unit types used as part of a metric name,
267+
// e.g. "foo_bytes_total" or "bar_baz_milligrams".
268+
func metricUnits(m string) (unit string, base string, ok bool) {
269+
ss := strings.Split(m, "_")
270+
271+
for unit, base := range units {
272+
// Also check for "no prefix".
273+
for _, p := range append(unitPrefixes, "") {
274+
for _, s := range ss {
275+
// Attempt to explicitly match a known unit with a known prefix,
276+
// as some words may look like "units" when matching suffix.
277+
//
278+
// As an example, "thermometers" should not match "meters", but
279+
// "kilometers" should.
280+
if s == p+unit {
281+
return p + unit, base, true
282+
}
283+
}
284+
}
285+
}
286+
287+
return "", "", false
288+
}
289+
290+
// Units and their possible prefixes recognized by this library. More can be
291+
// added over time as needed.
292+
var (
293+
// map a unit to the appropriate base unit.
294+
units = map[string]string{
295+
// Base units.
296+
"amperes": "amperes",
297+
"bytes": "bytes",
298+
"celsius": "celsius", // Celsius is more common in practice than Kelvin.
299+
"grams": "grams",
300+
"joules": "joules",
301+
"meters": "meters", // Both American and international spelling permitted.
302+
"metres": "metres",
303+
"seconds": "seconds",
304+
"volts": "volts",
305+
306+
// Non base units.
307+
// Time.
308+
"minutes": "seconds",
309+
"hours": "seconds",
310+
"days": "seconds",
311+
"weeks": "seconds",
312+
// Temperature.
313+
"kelvin": "celsius",
314+
"kelvins": "celsius",
315+
"fahrenheit": "celsius",
316+
"rankine": "celsius",
317+
// Length.
318+
"inches": "meters",
319+
"yards": "meters",
320+
"miles": "meters",
321+
// Bytes.
322+
"bits": "bytes",
323+
// Energy.
324+
"calories": "joules",
325+
// Mass.
326+
"pounds": "grams",
327+
"ounces": "grams",
328+
}
329+
330+
unitPrefixes = []string{
331+
"pico",
332+
"nano",
333+
"micro",
334+
"milli",
335+
"centi",
336+
"deci",
337+
"deca",
338+
"hecto",
339+
"kilo",
340+
"kibi",
341+
"mega",
342+
"mibi",
343+
"giga",
344+
"gibi",
345+
"tera",
346+
"tebi",
347+
"peta",
348+
"pebi",
349+
}
350+
351+
// Common abbreviations that we'd like to discourage.
352+
unitAbbreviations = []string{
353+
"s",
354+
"ms",
355+
"us",
356+
"ns",
357+
"sec",
358+
"b",
359+
"kb",
360+
"mb",
361+
"gb",
362+
"tb",
363+
"pb",
364+
"m",
365+
"h",
366+
"d",
367+
}
368+
)

0 commit comments

Comments
 (0)