-
Notifications
You must be signed in to change notification settings - Fork 4.8k
MON-1157: Test metrics for prometheus best practice #31370
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,119 @@ | ||||||
| package prometheus | ||||||
|
|
||||||
| import ( | ||||||
| "context" | ||||||
| "fmt" | ||||||
| "regexp" | ||||||
| "strings" | ||||||
| "time" | ||||||
|
|
||||||
| prometheusv1 "github.com/prometheus/client_golang/api/prometheus/v1" | ||||||
| dto "github.com/prometheus/client_model/go" | ||||||
| "github.com/prometheus/common/model" | ||||||
| ) | ||||||
|
|
||||||
| var camelCaseRe = regexp.MustCompile(`[a-z][A-Z]`) | ||||||
|
|
||||||
| // MetadataToMetricFamilies converts Prometheus API metadata into a slice of | ||||||
| // MetricFamily suitable for promlint validation. | ||||||
| func MetadataToMetricFamilies(metadata map[string][]prometheusv1.Metadata) []*dto.MetricFamily { | ||||||
| var families []*dto.MetricFamily | ||||||
|
|
||||||
| for name, entries := range metadata { | ||||||
| if len(entries) == 0 { | ||||||
| continue | ||||||
| } | ||||||
| // Recording rules use colons in their names and are not subject to | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm afraid there are some recording rules that don't have colons in their names, we'd need to cross-references with GET /api/v1/rules or sth else... That being said, /api/v1/metadata doesn't return recording rules, so this check isn't needed. |
||||||
| // metric naming conventions. | ||||||
| if strings.Contains(name, ":") { | ||||||
| continue | ||||||
| } | ||||||
|
|
||||||
| entry := entries[0] | ||||||
| name := name | ||||||
| help := entry.Help | ||||||
| metricType := convertMetricType(entry.Type) | ||||||
| families = append(families, &dto.MetricFamily{ | ||||||
| Name: &name, | ||||||
| Help: &help, | ||||||
| Type: &metricType, | ||||||
| Metric: []*dto.Metric{{}}, | ||||||
| }) | ||||||
| } | ||||||
|
|
||||||
| return families | ||||||
| } | ||||||
|
|
||||||
| // SetCamelCaseLabels attaches camelCase label pairs to the corresponding | ||||||
| // metric families so that promlint's LintCamelCase validation can detect them. | ||||||
| func SetCamelCaseLabels(families []*dto.MetricFamily, labelsPerMetric map[string][]*dto.LabelPair) { | ||||||
| for i, family := range families { | ||||||
| labels, ok := labelsPerMetric[family.GetName()] | ||||||
| if ok { | ||||||
| families[i].Metric[0].Label = labels | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // FindCamelCaseLabels queries Prometheus for all label names that contain | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| // camelCase, then for each such label, determines which metrics use it. | ||||||
| func FindCamelCaseLabels(ctx context.Context, promClient prometheusv1.API) (map[string][]*dto.LabelPair, error) { | ||||||
| labelNames, _, err := promClient.LabelNames(ctx, nil, time.Time{}, time.Time{}) | ||||||
| if err != nil { | ||||||
| return nil, fmt.Errorf("failed to get label names: %w", err) | ||||||
| } | ||||||
|
|
||||||
| var camelCaseLabels []string | ||||||
| for _, label := range labelNames { | ||||||
| if camelCaseRe.MatchString(label) { | ||||||
| camelCaseLabels = append(camelCaseLabels, label) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| if len(camelCaseLabels) == 0 { | ||||||
| return nil, nil | ||||||
| } | ||||||
|
|
||||||
| result := make(map[string][]*dto.LabelPair) | ||||||
| for _, label := range camelCaseLabels { | ||||||
| query := fmt.Sprintf(`count({__name__=~".+",%s=~".+"}) by (__name__)`, label) | ||||||
| val, _, err := promClient.Query(ctx, query, time.Time{}) | ||||||
| if err != nil { | ||||||
| return nil, fmt.Errorf("failed to query metrics for label %q: %w", label, err) | ||||||
| } | ||||||
|
|
||||||
| vector, ok := val.(model.Vector) | ||||||
| if !ok { | ||||||
| continue | ||||||
| } | ||||||
| for _, sample := range vector { | ||||||
| metricName := string(sample.Metric[model.MetricNameLabel]) | ||||||
| if metricName == "" { | ||||||
| continue | ||||||
| } | ||||||
| lbl := label | ||||||
| val := "" | ||||||
| result[metricName] = append(result[metricName], &dto.LabelPair{ | ||||||
| Name: &lbl, | ||||||
| Value: &val, | ||||||
| }) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| return result, nil | ||||||
| } | ||||||
|
|
||||||
| func convertMetricType(mt prometheusv1.MetricType) dto.MetricType { | ||||||
| switch mt { | ||||||
| case prometheusv1.MetricTypeCounter: | ||||||
| return dto.MetricType_COUNTER | ||||||
| case prometheusv1.MetricTypeGauge: | ||||||
| return dto.MetricType_GAUGE | ||||||
| case prometheusv1.MetricTypeHistogram: | ||||||
| return dto.MetricType_HISTOGRAM | ||||||
| case prometheusv1.MetricTypeSummary: | ||||||
| return dto.MetricType_SUMMARY | ||||||
| default: | ||||||
| return dto.MetricType_UNTYPED | ||||||
| } | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I first read camelCase, I thought it was a standalone check. I later realized that fetching only the camelCase labels is just an optimization because that is all the linter currently checks for. (If new checks are added upstream on labels, the labels won't all be set, but that's not a problem; we can easily adapt the tests once we learn about that)
To be clear, I'm not saying we should populate labels for all metrics right now, as that would be unnecessarily expensive I assume.
But, to make the test easier to read, I suggest we hide the fetching and setting of these camelCase labels inside a util, and just have a helper that prepares/builds the metric families to run the linter on.