From dd3a607d2d5d67b3608dfd6d34941343d642a24d Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Thu, 16 Oct 2025 13:41:53 +0200 Subject: [PATCH] Add configuration parameters Signed-off-by: Arve Knudsen --- config/config.go | 12 +- config/config_test.go | 46 ++++- ..._underscore_sanitization_defaults.good.yml | 2 + ..._underscore_sanitization_disabled.good.yml | 3 + ...l_underscore_sanitization_enabled.good.yml | 3 + docs/configuration/configuration.md | 9 + .../prometheusremotewrite/helper.go | 4 +- .../prometheusremotewrite/helper_test.go | 170 ++++++++++++++++-- .../prometheusremotewrite/metrics_to_prw.go | 12 +- storage/remote/write_handler.go | 20 ++- 10 files changed, 254 insertions(+), 27 deletions(-) create mode 100644 config/testdata/otlp_label_underscore_sanitization_defaults.good.yml create mode 100644 config/testdata/otlp_label_underscore_sanitization_disabled.good.yml create mode 100644 config/testdata/otlp_label_underscore_sanitization_enabled.good.yml diff --git a/config/config.go b/config/config.go index 8e7afc1f2f..91b91d25fc 100644 --- a/config/config.go +++ b/config/config.go @@ -258,7 +258,9 @@ var ( // DefaultOTLPConfig is the default OTLP configuration. DefaultOTLPConfig = OTLPConfig{ - TranslationStrategy: otlptranslator.UnderscoreEscapingWithSuffixes, + TranslationStrategy: otlptranslator.UnderscoreEscapingWithSuffixes, + LabelNameUnderscoreSanitization: true, + LabelNamePreserveMultipleUnderscores: true, } ) @@ -1609,6 +1611,14 @@ type OTLPConfig struct { // PromoteScopeMetadata controls whether to promote OTel scope metadata (i.e. name, version, schema URL, and attributes) to metric labels. // As per OTel spec, the aforementioned scope metadata should be identifying, i.e. made into metric labels. PromoteScopeMetadata bool `yaml:"promote_scope_metadata,omitempty"` + // LabelNameUnderscoreSanitization controls whether to enable prepending of 'key_' to labels + // starting with '_'. Reserved labels starting with `__` are not modified. + // This is only relevant when AllowUTF8 is false (i.e., when using underscore escaping). + LabelNameUnderscoreSanitization bool `yaml:"label_name_underscore_sanitization,omitempty"` + // LabelNamePreserveMultipleUnderscores enables preserving of multiple consecutive underscores + // in label names when AllowUTF8 is false. When false, multiple consecutive underscores are + // collapsed to a single underscore during label name sanitization. + LabelNamePreserveMultipleUnderscores bool `yaml:"label_name_preserve_multiple_underscores,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. diff --git a/config/config_test.go b/config/config_test.go index 1f093c7959..d729d2a6aa 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -175,7 +175,9 @@ var expectedConf = &Config{ PromoteResourceAttributes: []string{ "k8s.cluster.name", "k8s.job.name", "k8s.namespace.name", }, - TranslationStrategy: otlptranslator.UnderscoreEscapingWithSuffixes, + TranslationStrategy: otlptranslator.UnderscoreEscapingWithSuffixes, + LabelNameUnderscoreSanitization: true, + LabelNamePreserveMultipleUnderscores: true, }, RemoteReadConfigs: []*RemoteReadConfig{ @@ -1842,6 +1844,48 @@ func TestOTLPPromoteScopeMetadata(t *testing.T) { }) } +func TestOTLPLabelUnderscoreSanitization(t *testing.T) { + t.Run("defaults to true", func(t *testing.T) { + conf, err := LoadFile(filepath.Join("testdata", "otlp_label_underscore_sanitization_defaults.good.yml"), false, promslog.NewNopLogger()) + require.NoError(t, err) + + // Test that default values are true + require.True(t, conf.OTLPConfig.LabelNameUnderscoreSanitization) + require.True(t, conf.OTLPConfig.LabelNamePreserveMultipleUnderscores) + }) + + t.Run("explicit enabled", func(t *testing.T) { + conf, err := LoadFile(filepath.Join("testdata", "otlp_label_underscore_sanitization_enabled.good.yml"), false, promslog.NewNopLogger()) + require.NoError(t, err) + + out, err := yaml.Marshal(conf) + require.NoError(t, err) + var got Config + require.NoError(t, yaml.UnmarshalStrict(out, &got)) + + require.True(t, got.OTLPConfig.LabelNameUnderscoreSanitization) + require.True(t, got.OTLPConfig.LabelNamePreserveMultipleUnderscores) + }) + + t.Run("explicit disabled", func(t *testing.T) { + conf, err := LoadFile(filepath.Join("testdata", "otlp_label_underscore_sanitization_disabled.good.yml"), false, promslog.NewNopLogger()) + require.NoError(t, err) + + // When explicitly set to false, they should be false + require.False(t, conf.OTLPConfig.LabelNameUnderscoreSanitization) + require.False(t, conf.OTLPConfig.LabelNamePreserveMultipleUnderscores) + }) + + t.Run("empty config uses defaults", func(t *testing.T) { + conf, err := LoadFile(filepath.Join("testdata", "otlp_empty.yml"), false, promslog.NewNopLogger()) + require.NoError(t, err) + + // Empty config should use default values (true) + require.True(t, conf.OTLPConfig.LabelNameUnderscoreSanitization) + require.True(t, conf.OTLPConfig.LabelNamePreserveMultipleUnderscores) + }) +} + func TestOTLPAllowUTF8(t *testing.T) { t.Run("good config - NoUTF8EscapingWithSuffixes", func(t *testing.T) { fpath := filepath.Join("testdata", "otlp_allow_utf8.good.yml") diff --git a/config/testdata/otlp_label_underscore_sanitization_defaults.good.yml b/config/testdata/otlp_label_underscore_sanitization_defaults.good.yml new file mode 100644 index 0000000000..3b1e9796d3 --- /dev/null +++ b/config/testdata/otlp_label_underscore_sanitization_defaults.good.yml @@ -0,0 +1,2 @@ +otlp: + promote_resource_attributes: ["service.name"] diff --git a/config/testdata/otlp_label_underscore_sanitization_disabled.good.yml b/config/testdata/otlp_label_underscore_sanitization_disabled.good.yml new file mode 100644 index 0000000000..f8fe4ea669 --- /dev/null +++ b/config/testdata/otlp_label_underscore_sanitization_disabled.good.yml @@ -0,0 +1,3 @@ +otlp: + label_name_underscore_sanitization: false + label_name_preserve_multiple_underscores: false diff --git a/config/testdata/otlp_label_underscore_sanitization_enabled.good.yml b/config/testdata/otlp_label_underscore_sanitization_enabled.good.yml new file mode 100644 index 0000000000..8ce347495e --- /dev/null +++ b/config/testdata/otlp_label_underscore_sanitization_enabled.good.yml @@ -0,0 +1,3 @@ +otlp: + label_name_underscore_sanitization: true + label_name_preserve_multiple_underscores: true diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md index b3ea571b80..451af231c5 100644 --- a/docs/configuration/configuration.md +++ b/docs/configuration/configuration.md @@ -222,6 +222,15 @@ otlp: # Enables promotion of OTel scope metadata (i.e. name, version, schema URL, and attributes) to metric labels. # This is disabled by default for backwards compatibility, but according to OTel spec, scope metadata _should_ be identifying, i.e. translated to metric labels. [ promote_scope_metadata: | default = false ] + # Controls whether to enable prepending of 'key_' to labels starting with '_'. + # Reserved labels starting with '__' are not modified. + # This is only relevant when translation_strategy uses underscore escaping + # (e.g., "UnderscoreEscapingWithSuffixes" or "UnderscoreEscapingWithoutSuffixes"). + [ label_name_underscore_sanitization: | default = true ] + # Enables preserving of multiple consecutive underscores in label names when + # translation_strategy uses underscore escaping. When true (default), multiple + # consecutive underscores are preserved during label name sanitization. + [ label_name_preserve_multiple_underscores: | default = true ] # Settings related to the remote read feature. remote_read: diff --git a/storage/remote/otlptranslator/prometheusremotewrite/helper.go b/storage/remote/otlptranslator/prometheusremotewrite/helper.go index b8c7817a84..9f73bb1a49 100644 --- a/storage/remote/otlptranslator/prometheusremotewrite/helper.go +++ b/storage/remote/otlptranslator/prometheusremotewrite/helper.go @@ -90,7 +90,7 @@ func (c *PrometheusConverter) createAttributes(resource pcommon.Resource, attrib labelNamer := otlptranslator.LabelNamer{ UTF8Allowed: settings.AllowUTF8, - UnderscoreLabelSanitization: settings.LabelNameUnderscoreLabelSanitization, + UnderscoreLabelSanitization: settings.LabelNameUnderscoreSanitization, PreserveMultipleUnderscores: settings.LabelNamePreserveMultipleUnderscores, } @@ -122,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, settings.AllowUTF8, settings.LabelNameUnderscoreSanitization, settings.LabelNamePreserveMultipleUnderscores) 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 7083ce0885..27daadc2bc 100644 --- a/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw.go +++ b/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw.go @@ -54,9 +54,9 @@ type Settings struct { // PromoteScopeMetadata controls whether to promote OTel scope metadata to metric labels. PromoteScopeMetadata bool EnableTypeAndUnitLabels bool - // LabelNameUnderscoreLabelSanitization controls whether to enable prepending of 'key' to labels + // LabelNameUnderscoreSanitization controls whether to enable prepending of 'key' to labels // starting with '_'. Reserved labels starting with `__` are not modified. - LabelNameUnderscoreLabelSanitization bool + LabelNameUnderscoreSanitization bool // LabelNamePreserveMultipleUnderscores enables preserving of multiple // consecutive underscores in label names when AllowUTF8 is false. LabelNamePreserveMultipleUnderscores bool @@ -310,12 +310,16 @@ 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, allowUTF8, underscoreSanitization, preserveMultipleUnderscores bool) error { if s == nil { return nil } - labelNamer := otlptranslator.LabelNamer{UTF8Allowed: allowUTF8} + labelNamer := otlptranslator.LabelNamer{ + UTF8Allowed: allowUTF8, + UnderscoreLabelSanitization: underscoreSanitization, + PreserveMultipleUnderscores: preserveMultipleUnderscores, + } 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 266fae86a3..6bb63635b5 100644 --- a/storage/remote/write_handler.go +++ b/storage/remote/write_handler.go @@ -649,15 +649,17 @@ func (rw *rwExporter) ConsumeMetrics(ctx context.Context, md pmetric.Metrics) er combinedAppender := otlptranslator.NewCombinedAppender(app, rw.logger, rw.ingestCTZeroSample, rw.metrics) converter := otlptranslator.NewPrometheusConverter(combinedAppender) annots, err := converter.FromMetrics(ctx, md, otlptranslator.Settings{ - AddMetricSuffixes: otlpCfg.TranslationStrategy.ShouldAddSuffixes(), - AllowUTF8: !otlpCfg.TranslationStrategy.ShouldEscape(), - PromoteResourceAttributes: otlptranslator.NewPromoteResourceAttributes(otlpCfg), - KeepIdentifyingResourceAttributes: otlpCfg.KeepIdentifyingResourceAttributes, - ConvertHistogramsToNHCB: otlpCfg.ConvertHistogramsToNHCB, - PromoteScopeMetadata: otlpCfg.PromoteScopeMetadata, - AllowDeltaTemporality: rw.allowDeltaTemporality, - LookbackDelta: rw.lookbackDelta, - EnableTypeAndUnitLabels: rw.enableTypeAndUnitLabels, + AddMetricSuffixes: otlpCfg.TranslationStrategy.ShouldAddSuffixes(), + AllowUTF8: !otlpCfg.TranslationStrategy.ShouldEscape(), + PromoteResourceAttributes: otlptranslator.NewPromoteResourceAttributes(otlpCfg), + KeepIdentifyingResourceAttributes: otlpCfg.KeepIdentifyingResourceAttributes, + ConvertHistogramsToNHCB: otlpCfg.ConvertHistogramsToNHCB, + PromoteScopeMetadata: otlpCfg.PromoteScopeMetadata, + AllowDeltaTemporality: rw.allowDeltaTemporality, + LookbackDelta: rw.lookbackDelta, + EnableTypeAndUnitLabels: rw.enableTypeAndUnitLabels, + LabelNameUnderscoreSanitization: otlpCfg.LabelNameUnderscoreSanitization, + LabelNamePreserveMultipleUnderscores: otlpCfg.LabelNamePreserveMultipleUnderscores, }) defer func() {