Remove MustNewConstMetric and validate config in usage tracker - #5981
Conversation
a88072d to
1229529
Compare
| // creating a desc just so can do the validation | ||
| desc := prometheus.NewDesc("test_desc", "test desc created for validation", []string{labelName}, nil) | ||
| // try to create a metric and see if there are any error, we use same method in usage.Collect | ||
| _, err := prometheus.NewConstMetric(desc, prometheus.CounterValue, float64(1), labelName) |
There was a problem hiding this comment.
this seems redundant, I think essentially we only need to check that it's valid utf-8
There was a problem hiding this comment.
I think essentially we only need to check that it's valid utf-8
prometheus has support for utf-8 but it's not there yet, so we still need to follow the Legacy validation to support most of the users.
in Legacy validation, prometheus is much more strict and won't allowed reserved labels (anything that starts with __), or something like __name__ so I am using the prometheus's code to do the validation instead of re-implementing the validations.
also, you can't use the same label twice or it will err
There was a problem hiding this comment.
My point is that creating the metric here is unnecessary, because looking at the function definition the only relevant validation is UTF-8:
func NewConstMetric(desc *Desc, valueType ValueType, value float64, labelValues ...string) (Metric, error) {
if desc.err != nil {
return nil, desc.err
}
if err := validateLabelValues(labelValues, len(desc.variableLabels.names)); err != nil {
return nil, err
}
metric := &dto.Metric{}
if err := populateMetric(valueType, value, MakeLabelPairs(desc, labelValues), nil, metric, nil); err != nil {
return nil, err
}
return &constMetric{
desc: desc,
metric: metric,
}, nil
}
We only care about validating the label name, and we can see that the only relevant validation is the UTF-8, so we just need to check that and we avoid doing extra work:
func validateLabelValues(vals []string, expectedNumberOfValues int) error {
if len(vals) != expectedNumberOfValues {
// The call below makes vals escape, copy them to avoid that.
vals := append([]string(nil), vals...)
return fmt.Errorf(
"%w: expected %d label values but got %d in %#v",
errInconsistentCardinality, expectedNumberOfValues,
len(vals), vals,
)
}
for _, val := range vals {
if !utf8.ValidString(val) {
return fmt.Errorf("label value %q is not valid UTF-8", val)
}
}
return nil
}
As far as I know the version of the Prometheus library we use uses UTF-8 validation by default, not legacy.
There was a problem hiding this comment.
Checking if the label is Valid UTF-8 string will not catch it things like __name__ and labels that start with __, we need to rejected those as well.
If you replace this metric creation code with just utf-8 validation, and you will see that we will allow bunch of invalid label names.
one easy way to see it is to comment out the NewConstMetric the code and add the UTF-8 Validation, and run the TestValidateCostAttributionDimensions unit test.
Creating the metrics allows us to reusing the validations that are sprinkled across client_golang code without re-implementing them.
we will need to also do the validations that are done in https://github.com/prometheus/client_golang/blob/c316de06dea0caf0631f9eca944bc74cca1eb19c/prometheus/desc.go#L92 if want to avoid using NewConstMetric
NewConstMetric should be fine since we only do call this code path on overrides config reload or when we get call the overrides API and modify the cost attribution configs. because of that, I am not too worried about performance of this code.
also, NewConstMetric just creates an ad-hoc metric so it's not registered or exposed anywhere, and won't be collected as well.
1229529 to
6dc3b37
Compare
joe-elliott
left a comment
There was a problem hiding this comment.
I'm good on this PR, but it looks like @carles-grafana has some open questions. Feel free to approve/merge once you two are satisfied.
| // creating a desc just so can do the validation | ||
| desc := prometheus.NewDesc("test_desc", "test desc created for validation", []string{labelName}, nil) | ||
| // try to create a metric and see if there are any error, we use same method in usage.Collect | ||
| _, err := prometheus.NewConstMetric(desc, prometheus.CounterValue, float64(1), labelName) |
There was a problem hiding this comment.
My point is that creating the metric here is unnecessary, because looking at the function definition the only relevant validation is UTF-8:
func NewConstMetric(desc *Desc, valueType ValueType, value float64, labelValues ...string) (Metric, error) {
if desc.err != nil {
return nil, desc.err
}
if err := validateLabelValues(labelValues, len(desc.variableLabels.names)); err != nil {
return nil, err
}
metric := &dto.Metric{}
if err := populateMetric(valueType, value, MakeLabelPairs(desc, labelValues), nil, metric, nil); err != nil {
return nil, err
}
return &constMetric{
desc: desc,
metric: metric,
}, nil
}
We only care about validating the label name, and we can see that the only relevant validation is the UTF-8, so we just need to check that and we avoid doing extra work:
func validateLabelValues(vals []string, expectedNumberOfValues int) error {
if len(vals) != expectedNumberOfValues {
// The call below makes vals escape, copy them to avoid that.
vals := append([]string(nil), vals...)
return fmt.Errorf(
"%w: expected %d label values but got %d in %#v",
errInconsistentCardinality, expectedNumberOfValues,
len(vals), vals,
)
}
for _, val := range vals {
if !utf8.ValidString(val) {
return fmt.Errorf("label value %q is not valid UTF-8", val)
}
}
return nil
}
As far as I know the version of the Prometheus library we use uses UTF-8 validation by default, not legacy.
…stMetric so we don't panic in distributors
f2427a2 to
844e870
Compare
…na#5981) `prometheus.MustNewConstMetric` will panic when you try to create an metric with invalid label name. in prometheus world any labels that start with `__` are invalid because they are reserved. registering same labels will also cause prometheus.MustNewConstMetric to panic. you can configure overrides to map to invalid label names or use label names that are sanitized into `__` and it will cause distributors to panic when you scrape `/usage_metrics` to collect usage tracker metrics. there are also ways to cause panic by pushing data into tempo that results in an invalid metric. `prometheus.MustNewConstMetric` is designed to panic and is expected to panic when you have invalid config. in this case, we should use `prometheus.NewConstMetric` (it's what prometheus.MustNewConstMetric uses internally) and handle errors when we can't guarantee valid label names. This PR replaces usage of `prometheus.MustNewConstMetric` in usage tracker with `prometheus.NewConstMetric` so never panic. Along with that, this PR does few more things: - validate per tenant runtime overrides and fail if overrides contains a usage tracker config that results in invalid label names (note: this will cause tempo to fail at startup with errors like `level=error ts=2025-11-19T18:23:43.440930939Z caller=app.go:231 msg="module failed" module=overrides-api err="failed to start overrides-api, because it depends on module overrides, which has failed: invalid service state: Failed, expected: Running, failure: starting module overrides: invalid service state: Failed, expected: Running, failure: failed to start subservices: not healthy, 0 terminated, 1 failed: [failed to load runtime config: load file: validating overrides for * failed: cost_attribution.dimensions has duplicate label name: 'name', both 'service.name' and 'span.http.target' map to it]"` - validate usage tracker config in user configurable overrides API so we don't allow invalid config into tempo. this will reject al - log the errors with a rate limited logger from `prometheus.NewConstMetric` in case of an invalid label name in usage tracker config. - Add `tempo_distributor_usage_tracker_errors_total` counter to capture the errors with reason for the failures in the the usage tracker
What this PR does:
prometheus.MustNewConstMetricwill panic when you try to create an metric with invalid label name. in prometheus world any labels that start with__are invalid because they are reserved.registering same labels will also cause prometheus.MustNewConstMetric to panic.
you can configure overrides to map to invalid label names or use label names that are sanitized into
__and it will cause distributors to panic when you scrape/usage_metricsto collect usage tracker metrics.there are also ways to cause panic by pushing data into tempo that results in an invalid metric.
prometheus.MustNewConstMetricis designed to panic and is expected to panic when you have invalid config.in this case, we should use
prometheus.NewConstMetric(it's what prometheus.MustNewConstMetric uses internally) and handle errors when we can't guarantee valid label names.This PR replaces usage of
prometheus.MustNewConstMetricin usage tracker withprometheus.NewConstMetricso never panic.Along with that, this PR does few more things:
level=error ts=2025-11-19T18:23:43.440930939Z caller=app.go:231 msg="module failed" module=overrides-api err="failed to start overrides-api, because it depends on module overrides, which has failed: invalid service state: Failed, expected: Running, failure: starting module overrides: invalid service state: Failed, expected: Running, failure: failed to start subservices: not healthy, 0 terminated, 1 failed: [failed to load runtime config: load file: validating overrides for * failed: cost_attribution.dimensions has duplicate label name: 'name', both 'service.name' and 'span.http.target' map to it]"prometheus.NewConstMetricin case of an invalid label name in usage tracker config.tempo_distributor_usage_tracker_errors_totalcounter to capture the errors with reason for the failures in the the usage trackerMore Details
I used a config like ⏬ to cause a panic:
here is panic log, I used local docker-compose in this test
Which issue(s) this PR fixes:
Fixes #
Checklist
CHANGELOG.mdupdated - the order of entries should be[CHANGE],[FEATURE],[ENHANCEMENT],[BUGFIX]