@@ -16,15 +16,11 @@ package promlint
16
16
17
17
import (
18
18
"errors"
19
- "fmt"
20
19
"io"
21
- "regexp"
22
20
"sort"
23
- "strings"
24
-
25
- "github.com/prometheus/common/expfmt"
26
21
27
22
dto "github.com/prometheus/client_model/go"
23
+ "github.com/prometheus/common/expfmt"
28
24
)
29
25
30
26
// A Linter is a Prometheus metrics linter. It identifies issues with metric
@@ -37,23 +33,8 @@ type Linter struct {
37
33
// of them.
38
34
r io.Reader
39
35
mfs []* dto.MetricFamily
40
- }
41
36
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
57
38
}
58
39
59
40
// New creates a new Linter that reads an input stream of Prometheus metrics in
@@ -72,6 +53,14 @@ func NewWithMetricFamilies(mfs []*dto.MetricFamily) *Linter {
72
53
}
73
54
}
74
55
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
+
75
64
// Lint performs a linting pass, returning a slice of Problems indicating any
76
65
// issues found in the metrics stream. The slice is sorted by metric name
77
66
// and issue description.
@@ -91,11 +80,11 @@ func (l *Linter) Lint() ([]Problem, error) {
91
80
return nil , err
92
81
}
93
82
94
- problems = append (problems , lint (mf )... )
83
+ problems = append (problems , l . lint (mf )... )
95
84
}
96
85
}
97
86
for _ , mf := range l .mfs {
98
- problems = append (problems , lint (mf )... )
87
+ problems = append (problems , l . lint (mf )... )
99
88
}
100
89
101
90
// Ensure deterministic output.
@@ -110,276 +99,25 @@ func (l *Linter) Lint() ([]Problem, error) {
110
99
}
111
100
112
101
// 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 {
136
103
var problems []Problem
137
104
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 ()))
235
109
}
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'" ))
261
110
}
262
111
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 ()))
267
117
}
268
118
}
269
119
}
270
- return problems
271
- }
272
120
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.
282
122
return problems
283
123
}
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