diff --git a/CHANGELOG.md b/CHANGELOG.md index 649196758b..8067f2c48a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## main / unreleased +## 3.7.1 / 2025-10-16 + +* [BUGFIX] OTLP: Prefix `key_` to label name when translating an OTel attribute name starting with a single underscore, and keep multiple consecutive underscores in label name when translating an OTel attribute name. This reverts the breaking changes introduced in 3.7.0. #17344 + ## 3.7.0 / 2025-10-15 * [CHANGE] Remote-write: the following metrics are deprecated: diff --git a/VERSION b/VERSION index 7c69a55dbb..a76ccff2a6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.7.0 +3.7.1 diff --git a/storage/remote/otlptranslator/prometheusremotewrite/helper.go b/storage/remote/otlptranslator/prometheusremotewrite/helper.go index 4c50099e06..a27447a90f 100644 --- a/storage/remote/otlptranslator/prometheusremotewrite/helper.go +++ b/storage/remote/otlptranslator/prometheusremotewrite/helper.go @@ -88,7 +88,11 @@ func (c *PrometheusConverter) createAttributes(resource pcommon.Resource, attrib c.scratchBuilder.Sort() sortedLabels := c.scratchBuilder.Labels() - labelNamer := otlptranslator.LabelNamer{UTF8Allowed: settings.AllowUTF8} + labelNamer := otlptranslator.LabelNamer{ + UTF8Allowed: settings.AllowUTF8, + UnderscoreLabelSanitization: settings.LabelNameUnderscoreSanitization, + PreserveMultipleUnderscores: settings.LabelNamePreserveMultipleUnderscores, + } if settings.AllowUTF8 { // UTF8 is allowed, so conflicts aren't possible. @@ -118,7 +122,7 @@ func (c *PrometheusConverter) createAttributes(resource pcommon.Resource, attrib } } - err := settings.PromoteResourceAttributes.addPromotedAttributes(c.builder, resourceAttrs, settings.AllowUTF8) + err := settings.PromoteResourceAttributes.addPromotedAttributes(c.builder, resourceAttrs, labelNamer) if err != nil { return labels.EmptyLabels(), err } diff --git a/storage/remote/otlptranslator/prometheusremotewrite/helper_test.go b/storage/remote/otlptranslator/prometheusremotewrite/helper_test.go index 9ecb2c15f7..948bd8ca7d 100644 --- a/storage/remote/otlptranslator/prometheusremotewrite/helper_test.go +++ b/storage/remote/otlptranslator/prometheusremotewrite/helper_test.go @@ -67,15 +67,35 @@ func TestCreateAttributes(t *testing.T) { attrs.PutStr("metric-attr", "metric value") attrs.PutStr("metric-attr-other", "metric value other") + // Setup resources with underscores for sanitization tests + resourceAttrsWithUnderscores := map[string]string{ + "service.name": "service name", + "service.instance.id": "service ID", + "_private": "private value", + "__reserved__": "reserved value", + "label___multi": "multi value", + } + resourceWithUnderscores := pcommon.NewResource() + for k, v := range resourceAttrsWithUnderscores { + resourceWithUnderscores.Attributes().PutStr(k, v) + } + attrsWithUnderscores := pcommon.NewMap() + attrsWithUnderscores.PutStr("_metric_private", "private metric") + attrsWithUnderscores.PutStr("metric___multi", "multi metric") + testCases := []struct { - name string - scope scope - promoteAllResourceAttributes bool - promoteResourceAttributes []string - promoteScope bool - ignoreResourceAttributes []string - ignoreAttrs []string - expectedLabels labels.Labels + name string + resource pcommon.Resource + attrs pcommon.Map + scope scope + promoteAllResourceAttributes bool + promoteResourceAttributes []string + promoteScope bool + ignoreResourceAttributes []string + ignoreAttrs []string + labelNameUnderscoreLabelSanitization bool + labelNamePreserveMultipleUnderscores bool + expectedLabels labels.Labels }{ { name: "Successful conversion without resource attribute promotion and without scope promotion", @@ -251,6 +271,121 @@ func TestCreateAttributes(t *testing.T) { "otel_scope_attr2", "value2", ), }, + // Label sanitization test cases + { + name: "Underscore sanitization enabled - prepends key_ to labels starting with single _", + resource: resourceWithUnderscores, + attrs: attrsWithUnderscores, + promoteResourceAttributes: []string{"_private"}, + labelNameUnderscoreLabelSanitization: true, + labelNamePreserveMultipleUnderscores: true, + expectedLabels: labels.FromStrings( + "__name__", "test_metric", + "instance", "service ID", + "job", "service name", + "key_private", "private value", + "key_metric_private", "private metric", + "metric___multi", "multi metric", + ), + }, + { + name: "Underscore sanitization disabled - keeps labels with _ as-is", + resource: resourceWithUnderscores, + attrs: attrsWithUnderscores, + promoteResourceAttributes: []string{"_private"}, + labelNameUnderscoreLabelSanitization: false, + labelNamePreserveMultipleUnderscores: true, + expectedLabels: labels.FromStrings( + "__name__", "test_metric", + "instance", "service ID", + "job", "service name", + "_private", "private value", + "_metric_private", "private metric", + "metric___multi", "multi metric", + ), + }, + { + name: "Multiple underscores preserved - keeps consecutive underscores", + resource: resourceWithUnderscores, + attrs: attrsWithUnderscores, + promoteResourceAttributes: []string{"label___multi"}, + labelNameUnderscoreLabelSanitization: false, + labelNamePreserveMultipleUnderscores: true, + expectedLabels: labels.FromStrings( + "__name__", "test_metric", + "instance", "service ID", + "job", "service name", + "label___multi", "multi value", + "_metric_private", "private metric", + "metric___multi", "multi metric", + ), + }, + { + name: "Multiple underscores collapsed - collapses to single underscore", + resource: resourceWithUnderscores, + attrs: attrsWithUnderscores, + promoteResourceAttributes: []string{"label___multi"}, + labelNameUnderscoreLabelSanitization: false, + labelNamePreserveMultipleUnderscores: false, + expectedLabels: labels.FromStrings( + "__name__", "test_metric", + "instance", "service ID", + "job", "service name", + "label_multi", "multi value", + "_metric_private", "private metric", + "metric_multi", "multi metric", + ), + }, + { + name: "Both sanitization options enabled", + resource: resourceWithUnderscores, + attrs: attrsWithUnderscores, + promoteResourceAttributes: []string{"_private", "label___multi"}, + labelNameUnderscoreLabelSanitization: true, + labelNamePreserveMultipleUnderscores: true, + expectedLabels: labels.FromStrings( + "__name__", "test_metric", + "instance", "service ID", + "job", "service name", + "key_private", "private value", + "label___multi", "multi value", + "key_metric_private", "private metric", + "metric___multi", "multi metric", + ), + }, + { + name: "Both sanitization options disabled", + resource: resourceWithUnderscores, + attrs: attrsWithUnderscores, + promoteResourceAttributes: []string{"_private", "label___multi"}, + labelNameUnderscoreLabelSanitization: false, + labelNamePreserveMultipleUnderscores: false, + expectedLabels: labels.FromStrings( + "__name__", "test_metric", + "instance", "service ID", + "job", "service name", + "_private", "private value", + "label_multi", "multi value", + "_metric_private", "private metric", + "metric_multi", "multi metric", + ), + }, + { + name: "Reserved labels (starting with __) are never modified", + resource: resourceWithUnderscores, + attrs: attrsWithUnderscores, + promoteResourceAttributes: []string{"__reserved__"}, + labelNameUnderscoreLabelSanitization: true, + labelNamePreserveMultipleUnderscores: false, + expectedLabels: labels.FromStrings( + "__name__", "test_metric", + "instance", "service ID", + "job", "service name", + "__reserved__", "reserved value", + "key_metric_private", "private metric", + "metric_multi", "multi metric", + ), + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { @@ -261,9 +396,24 @@ func TestCreateAttributes(t *testing.T) { PromoteResourceAttributes: tc.promoteResourceAttributes, IgnoreResourceAttributes: tc.ignoreResourceAttributes, }), - PromoteScopeMetadata: tc.promoteScope, + PromoteScopeMetadata: tc.promoteScope, + LabelNameUnderscoreSanitization: tc.labelNameUnderscoreLabelSanitization, + LabelNamePreserveMultipleUnderscores: tc.labelNamePreserveMultipleUnderscores, } - lbls, err := c.createAttributes(resource, attrs, tc.scope, settings, tc.ignoreAttrs, false, Metadata{}, model.MetricNameLabel, "test_metric") + // Use test case specific resource/attrs if provided, otherwise use defaults + // Check if tc.resource is initialized (non-zero) by trying to get its attributes + testResource := resource + testAttrs := attrs + // For pcommon types, we can check if they're non-zero by seeing if they have attributes + // Since zero-initialized Resource is not valid, we use a simple heuristic: + // if the struct has been explicitly set in the test case, use it + if tc.resource != (pcommon.Resource{}) { + testResource = tc.resource + } + if tc.attrs != (pcommon.Map{}) { + testAttrs = tc.attrs + } + lbls, err := c.createAttributes(testResource, testAttrs, tc.scope, settings, tc.ignoreAttrs, false, Metadata{}, model.MetricNameLabel, "test_metric") require.NoError(t, err) testutil.RequireEqual(t, lbls, tc.expectedLabels) diff --git a/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw.go b/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw.go index 9c71c17d77..eb540fc611 100644 --- a/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw.go +++ b/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw.go @@ -54,6 +54,12 @@ type Settings struct { // PromoteScopeMetadata controls whether to promote OTel scope metadata to metric labels. PromoteScopeMetadata bool EnableTypeAndUnitLabels bool + // LabelNameUnderscoreSanitization controls whether to enable prepending of 'key' to labels + // starting with '_'. Reserved labels starting with `__` are not modified. + LabelNameUnderscoreSanitization bool + // LabelNamePreserveMultipleUnderscores enables preserving of multiple + // consecutive underscores in label names when AllowUTF8 is false. + LabelNamePreserveMultipleUnderscores bool } // PrometheusConverter converts from OTel write format to Prometheus remote write format. @@ -305,12 +311,11 @@ func NewPromoteResourceAttributes(otlpCfg config.OTLPConfig) *PromoteResourceAtt } // addPromotedAttributes adds labels for promoted resourceAttributes to the builder. -func (s *PromoteResourceAttributes) addPromotedAttributes(builder *labels.Builder, resourceAttributes pcommon.Map, allowUTF8 bool) error { +func (s *PromoteResourceAttributes) addPromotedAttributes(builder *labels.Builder, resourceAttributes pcommon.Map, labelNamer otlptranslator.LabelNamer) error { if s == nil { return nil } - labelNamer := otlptranslator.LabelNamer{UTF8Allowed: allowUTF8} if s.promoteAll { var err error resourceAttributes.Range(func(name string, value pcommon.Value) bool { diff --git a/storage/remote/write_handler.go b/storage/remote/write_handler.go index d92ceaecea..c250dc2199 100644 --- a/storage/remote/write_handler.go +++ b/storage/remote/write_handler.go @@ -680,6 +680,10 @@ func (rw *rwExporter) ConsumeMetrics(ctx context.Context, md pmetric.Metrics) er AllowDeltaTemporality: rw.allowDeltaTemporality, LookbackDelta: rw.lookbackDelta, EnableTypeAndUnitLabels: rw.enableTypeAndUnitLabels, + // For backwards compatibility. + LabelNameUnderscoreSanitization: true, + // For backwards compatibility. + LabelNamePreserveMultipleUnderscores: true, }) defer func() { diff --git a/web/ui/mantine-ui/package.json b/web/ui/mantine-ui/package.json index aed3bcd593..6144afa70d 100644 --- a/web/ui/mantine-ui/package.json +++ b/web/ui/mantine-ui/package.json @@ -1,7 +1,7 @@ { "name": "@prometheus-io/mantine-ui", "private": true, - "version": "0.307.0", + "version": "0.307.1", "type": "module", "scripts": { "start": "vite", @@ -28,7 +28,7 @@ "@microsoft/fetch-event-source": "^2.0.1", "@nexucis/fuzzy": "^0.5.1", "@nexucis/kvsearch": "^0.9.1", - "@prometheus-io/codemirror-promql": "0.307.0", + "@prometheus-io/codemirror-promql": "0.307.1", "@reduxjs/toolkit": "^2.9.0", "@tabler/icons-react": "^3.35.0", "@tanstack/react-query": "^5.90.2", diff --git a/web/ui/module/codemirror-promql/package.json b/web/ui/module/codemirror-promql/package.json index 78d8a36921..3c8f6fd58c 100644 --- a/web/ui/module/codemirror-promql/package.json +++ b/web/ui/module/codemirror-promql/package.json @@ -1,6 +1,6 @@ { "name": "@prometheus-io/codemirror-promql", - "version": "0.307.0", + "version": "0.307.1", "description": "a CodeMirror mode for the PromQL language", "types": "dist/esm/index.d.ts", "module": "dist/esm/index.js", @@ -29,7 +29,7 @@ }, "homepage": "https://github.com/prometheus/prometheus/blob/main/web/ui/module/codemirror-promql/README.md", "dependencies": { - "@prometheus-io/lezer-promql": "0.307.0", + "@prometheus-io/lezer-promql": "0.307.1", "lru-cache": "^11.2.2" }, "devDependencies": { diff --git a/web/ui/module/lezer-promql/package.json b/web/ui/module/lezer-promql/package.json index 1837bc078b..951955cd53 100644 --- a/web/ui/module/lezer-promql/package.json +++ b/web/ui/module/lezer-promql/package.json @@ -1,6 +1,6 @@ { "name": "@prometheus-io/lezer-promql", - "version": "0.307.0", + "version": "0.307.1", "description": "lezer-based PromQL grammar", "main": "dist/index.cjs", "type": "module", diff --git a/web/ui/package-lock.json b/web/ui/package-lock.json index 9c39ef7c7e..6eafc4f156 100644 --- a/web/ui/package-lock.json +++ b/web/ui/package-lock.json @@ -1,12 +1,12 @@ { "name": "prometheus-io", - "version": "0.307.0", + "version": "0.307.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "prometheus-io", - "version": "0.307.0", + "version": "0.307.1", "workspaces": [ "mantine-ui", "module/*" @@ -24,7 +24,7 @@ }, "mantine-ui": { "name": "@prometheus-io/mantine-ui", - "version": "0.307.0", + "version": "0.307.1", "dependencies": { "@codemirror/autocomplete": "^6.19.0", "@codemirror/language": "^6.11.3", @@ -42,7 +42,7 @@ "@microsoft/fetch-event-source": "^2.0.1", "@nexucis/fuzzy": "^0.5.1", "@nexucis/kvsearch": "^0.9.1", - "@prometheus-io/codemirror-promql": "0.307.0", + "@prometheus-io/codemirror-promql": "0.307.1", "@reduxjs/toolkit": "^2.9.0", "@tabler/icons-react": "^3.35.0", "@tanstack/react-query": "^5.90.2", @@ -87,10 +87,10 @@ }, "module/codemirror-promql": { "name": "@prometheus-io/codemirror-promql", - "version": "0.307.0", + "version": "0.307.1", "license": "Apache-2.0", "dependencies": { - "@prometheus-io/lezer-promql": "0.307.0", + "@prometheus-io/lezer-promql": "0.307.1", "lru-cache": "^11.2.2" }, "devDependencies": { @@ -120,7 +120,7 @@ }, "module/lezer-promql": { "name": "@prometheus-io/lezer-promql", - "version": "0.307.0", + "version": "0.307.1", "license": "Apache-2.0", "devDependencies": { "@lezer/generator": "^1.8.0", diff --git a/web/ui/package.json b/web/ui/package.json index b4f70b17e5..be701b3577 100644 --- a/web/ui/package.json +++ b/web/ui/package.json @@ -1,7 +1,7 @@ { "name": "prometheus-io", "description": "Monorepo for the Prometheus UI", - "version": "0.307.0", + "version": "0.307.1", "private": true, "scripts": { "build": "bash build_ui.sh --all",