Skip to content

Commit d66bbd8

Browse files
authored
Merge pull request kubernetes#86477 from RainbowMango/pr_introduce_promlint
Introduce promlint to guarantee metrics follow Prometheus best practices
2 parents 1d365a8 + 1d44f63 commit d66bbd8

File tree

3 files changed

+174
-0
lines changed

3 files changed

+174
-0
lines changed

staging/src/k8s.io/component-base/metrics/testutil/BUILD

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go_library(
44
name = "go_default_library",
55
srcs = [
66
"metrics.go",
7+
"promlint.go",
78
"testutil.go",
89
],
910
importmap = "k8s.io/kubernetes/vendor/k8s.io/component-base/metrics/testutil",
@@ -13,6 +14,7 @@ go_library(
1314
"//staging/src/k8s.io/apimachinery/pkg/version:go_default_library",
1415
"//staging/src/k8s.io/component-base/metrics:go_default_library",
1516
"//vendor/github.com/prometheus/client_golang/prometheus/testutil:go_default_library",
17+
"//vendor/github.com/prometheus/client_golang/prometheus/testutil/promlint:go_default_library",
1618
"//vendor/github.com/prometheus/client_model/go:go_default_library",
1719
"//vendor/github.com/prometheus/common/expfmt:go_default_library",
1820
"//vendor/github.com/prometheus/common/model:go_default_library",
@@ -37,6 +39,7 @@ go_test(
3739
name = "go_default_test",
3840
srcs = [
3941
"metrics_test.go",
42+
"promlint_test.go",
4043
"testutil_test.go",
4144
],
4245
embed = [":go_default_library"],
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
Copyright 2020 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package testutil
18+
19+
import (
20+
"io"
21+
22+
"github.com/prometheus/client_golang/prometheus/testutil/promlint"
23+
)
24+
25+
// exceptionMetrics is an exception list of metrics which violates promlint rules.
26+
//
27+
// The original entries come from the existing metrics when we introduce promlint.
28+
// We setup this list for allow and not fail on the current violations.
29+
// Generally speaking, you need to fix the problem for a new metric rather than add it into the list.
30+
var exceptionMetrics = []string{
31+
// kube-apiserver
32+
"aggregator_openapi_v2_regeneration_count",
33+
"apiserver_admission_step_admission_duration_seconds_summary",
34+
"apiserver_current_inflight_requests",
35+
"apiserver_longrunning_gauge",
36+
"apiserver_request_total",
37+
"authenticated_user_requests",
38+
"authentication_attempts",
39+
"get_token_count",
40+
"get_token_fail_count",
41+
"ssh_tunnel_open_count",
42+
"ssh_tunnel_open_fail_count",
43+
44+
// kube-controller-manager
45+
"attachdetach_controller_forced_detaches",
46+
"authenticated_user_requests",
47+
"authentication_attempts",
48+
"get_token_count",
49+
"get_token_fail_count",
50+
"kubernetes_build_info",
51+
"node_collector_evictions_number",
52+
53+
// kube-proxy
54+
"kubernetes_build_info",
55+
56+
// kube-scheduler
57+
"scheduler_total_preemption_attempts",
58+
59+
// kubelet-resource-v1alpha1
60+
"container_cpu_usage_seconds_total",
61+
"node_cpu_usage_seconds_total",
62+
}
63+
64+
// A Problem is an issue detected by a Linter.
65+
type Problem promlint.Problem
66+
67+
// A Linter is a Prometheus metrics linter. It identifies issues with metric
68+
// names, types, and metadata, and reports them to the caller.
69+
type Linter struct {
70+
promLinter *promlint.Linter
71+
}
72+
73+
// Lint performs a linting pass, returning a slice of Problems indicating any
74+
// issues found in the metrics stream. The slice is sorted by metric name
75+
// and issue description.
76+
func (l *Linter) Lint() ([]Problem, error) {
77+
promProblems, err := l.promLinter.Lint()
78+
if err != nil {
79+
return nil, err
80+
}
81+
82+
// Ignore problems those in exception list
83+
problems := make([]Problem, 0, len(promProblems))
84+
for i := range promProblems {
85+
if !l.shouldIgnore(promProblems[i].Metric) {
86+
problems = append(problems, Problem(promProblems[i]))
87+
}
88+
}
89+
90+
return problems, nil
91+
}
92+
93+
// shouldIgnore returns true if metric in the exception list, otherwise returns false.
94+
func (l *Linter) shouldIgnore(metricName string) bool {
95+
for i := range exceptionMetrics {
96+
if metricName == exceptionMetrics[i] {
97+
return true
98+
}
99+
}
100+
101+
return false
102+
}
103+
104+
// NewPromLinter creates a new Linter that reads an input stream of Prometheus metrics.
105+
// Only the text exposition format is supported.
106+
func NewPromLinter(r io.Reader) *Linter {
107+
return &Linter{
108+
promLinter: promlint.New(r),
109+
}
110+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
Copyright 2020 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package testutil
18+
19+
import (
20+
"strings"
21+
"testing"
22+
)
23+
24+
func TestLinter(t *testing.T) {
25+
var tests = []struct {
26+
name string
27+
metric string
28+
expect string
29+
}{
30+
{
31+
name: "problematic metric should be reported",
32+
metric: `
33+
# HELP test_problematic_total [ALPHA] non-counter metrics should not have total suffix
34+
# TYPE test_problematic_total gauge
35+
test_problematic_total{some_label="some_value"} 1
36+
`,
37+
expect: `non-counter metrics should not have "_total" suffix`,
38+
},
39+
// Don't need to test metrics in exception list, they will be covered by e2e test.
40+
// In addition, we don't need to update this test when we remove metrics from exception list in the future.
41+
}
42+
43+
for _, test := range tests {
44+
tc := test
45+
t.Run(tc.name, func(t *testing.T) {
46+
linter := NewPromLinter(strings.NewReader(tc.metric))
47+
problems, err := linter.Lint()
48+
if err != nil {
49+
t.Fatalf("unexpected error: %v", err)
50+
}
51+
52+
if len(problems) == 0 {
53+
t.Fatalf("expecte a problem but got none")
54+
}
55+
56+
if problems[0].Text != tc.expect {
57+
t.Fatalf("expect: %s, but got: %s", tc.expect, problems[0])
58+
}
59+
})
60+
}
61+
}

0 commit comments

Comments
 (0)