Add configuration parameters

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>
This commit is contained in:
Arve Knudsen 2025-10-16 13:41:53 +02:00
parent 7cf4b5da55
commit dd3a607d2d
10 changed files with 254 additions and 27 deletions

View File

@ -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.

View File

@ -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")

View File

@ -0,0 +1,2 @@
otlp:
promote_resource_attributes: ["service.name"]

View File

@ -0,0 +1,3 @@
otlp:
label_name_underscore_sanitization: false
label_name_preserve_multiple_underscores: false

View File

@ -0,0 +1,3 @@
otlp:
label_name_underscore_sanitization: true
label_name_preserve_multiple_underscores: true

View File

@ -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: <boolean> | 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: <boolean> | 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: <boolean> | default = true ]
# Settings related to the remote read feature.
remote_read:

View File

@ -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
}

View File

@ -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)

View File

@ -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 {

View File

@ -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() {