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