Skip to content

Commit 3c5e60e

Browse files
committed
Porting promlint from prometheus/prometheus.
Signed-off-by: RainbowMango <[email protected]>
1 parent efb148c commit 3c5e60e

File tree

2 files changed

+1160
-0
lines changed

2 files changed

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

0 commit comments

Comments
 (0)