diff --git a/util/promlint/promlint.go b/util/promlint/promlint.go index 69e80627e..8dcb925cb 100644 --- a/util/promlint/promlint.go +++ b/util/promlint/promlint.go @@ -86,6 +86,7 @@ func lint(mf dto.MetricFamily) []Problem { fns := []func(mf dto.MetricFamily) []Problem{ lintHelp, lintMetricUnits, + lintCounter, } var problems []Problem @@ -135,6 +136,31 @@ func lintMetricUnits(mf dto.MetricFamily) []Problem { return problems } +// lintCounter detects issues specific to counters, as well as patterns that should +// only be used with counters. +func lintCounter(mf dto.MetricFamily) []Problem { + var problems []Problem + + isCounter := *mf.Type == dto.MetricType_COUNTER + isUntyped := *mf.Type == dto.MetricType_UNTYPED + hasTotalSuffix := strings.HasSuffix(*mf.Name, "_total") + + switch { + case isCounter && !hasTotalSuffix: + problems = append(problems, Problem{ + Metric: *mf.Name, + Text: `counter metrics should have "_total" suffix`, + }) + case !isUntyped && !isCounter && hasTotalSuffix: + problems = append(problems, Problem{ + Metric: *mf.Name, + Text: `non-counter metrics should not have "_total" suffix`, + }) + } + + return problems +} + // metricUnits attempts to detect known unit types used as part of a metric name, // e.g. "foo_bytes_total" or "bar_baz_milligrams". func metricUnits(m string) (unit string, base string, ok bool) { diff --git a/util/promlint/promlint_test.go b/util/promlint/promlint_test.go index a232563e8..19358d69e 100644 --- a/util/promlint/promlint_test.go +++ b/util/promlint/promlint_test.go @@ -251,3 +251,84 @@ thermometers_kelvin 0 }) } } + +func TestLintCounter(t *testing.T) { + tests := []struct { + name string + in string + problems []promlint.Problem + }{ + { + name: "counter without _total suffix", + in: ` +# HELP x_bytes Test metric. +# TYPE x_bytes counter +x_bytes 10 +`, + problems: []promlint.Problem{{ + Metric: "x_bytes", + Text: `counter metrics should have "_total" suffix`, + }}, + }, + { + name: "gauge with _total suffix", + in: ` +# HELP x_bytes_total Test metric. +# TYPE x_bytes_total gauge +x_bytes_total 10 +`, + problems: []promlint.Problem{{ + Metric: "x_bytes_total", + Text: `non-counter metrics should not have "_total" suffix`, + }}, + }, + { + name: "counter with _total suffix", + in: ` +# HELP x_bytes_total Test metric. +# TYPE x_bytes_total counter +x_bytes_total 10 +`, + }, + { + name: "gauge without _total suffix", + in: ` +# HELP x_bytes Test metric. +# TYPE x_bytes gauge +x_bytes 10 +`, + }, + { + name: "untyped with _total suffix", + in: ` +# HELP x_bytes_total Test metric. +# TYPE x_bytes_total untyped +x_bytes_total 10 +`, + }, + { + name: "untyped without _total suffix", + in: ` +# HELP x_bytes Test metric. +# TYPE x_bytes untyped +x_bytes 10 +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := promlint.New(strings.NewReader(tt.in)) + + problems, err := l.Lint() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if want, got := tt.problems, problems; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected problems:\n- want: %v\n- got: %v", + want, got) + } + }) + } +}