diff --git a/.chloggen/owilliams_newconfig.yaml b/.chloggen/owilliams_newconfig.yaml new file mode 100644 index 0000000000000..299ae292520d4 --- /dev/null +++ b/.chloggen/owilliams_newconfig.yaml @@ -0,0 +1,27 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: bug_fix + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: prometheusexporter, prometheusremotewriteexporter + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Connect pkg.translator.prometheus.PermissiveLabelSanitization with relevant logic. + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [43077] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + +# If your change doesn't affect end users or the exported elements of any package, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [] diff --git a/exporter/prometheusexporter/README.md b/exporter/prometheusexporter/README.md index 5e26aeabc225e..06c4ec9cdfa68 100644 --- a/exporter/prometheusexporter/README.md +++ b/exporter/prometheusexporter/README.md @@ -64,6 +64,10 @@ exporters: Given the example, metrics will be available at `https://1.2.3.4:1234/metrics`. +### Feature gates + +There are also flags that control translation behavior. [See the documentation for the Prometheus translator module](../../pkg/translator/prometheus/) for more information. + ## Metric names and labels normalization By Default, OpenTelemetry metric names and attributes are normalized to be compliant with [Prometheus naming rules](https://prometheus.io/docs/practices/naming/). @@ -103,4 +107,4 @@ After this, grouping or selecting becomes as simple as: app_ads_ad_requests_total{namespace="my-namespace"} sum by (namespace) (app_ads_ad_requests_total) -``` \ No newline at end of file +``` diff --git a/exporter/prometheusexporter/collector.go b/exporter/prometheusexporter/collector.go index d7034a52b4a0d..7943da5639c4b 100644 --- a/exporter/prometheusexporter/collector.go +++ b/exporter/prometheusexporter/collector.go @@ -92,7 +92,8 @@ func configureMetricNamer(config *Config) otlptranslator.MetricNamer { func configureLabelNamer(config *Config) otlptranslator.LabelNamer { _, utf8Allowed := getTranslationConfiguration(config) return otlptranslator.LabelNamer{ - UTF8Allowed: utf8Allowed, + UTF8Allowed: utf8Allowed, + PreserveMultipleUnderscores: !prometheustranslator.DropSanitizationGate.IsEnabled(), } } diff --git a/exporter/prometheusremotewriteexporter/exporter.go b/exporter/prometheusremotewriteexporter/exporter.go index c1f8fc6eee3f9..74253d97ce045 100644 --- a/exporter/prometheusremotewriteexporter/exporter.go +++ b/exporter/prometheusremotewriteexporter/exporter.go @@ -33,6 +33,7 @@ import ( "go.uber.org/zap" "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusremotewriteexporter/internal/metadata" + prometheustranslator "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus" "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheusremotewrite" ) @@ -305,7 +306,9 @@ func (prwe *prwExporter) PushMetrics(ctx context.Context, md pmetric.Metrics) er } func validateAndSanitizeExternalLabels(cfg *Config) (map[string]string, error) { - namer := otlptranslator.LabelNamer{} + namer := otlptranslator.LabelNamer{ + UnderscoreLabelSanitization: !prometheustranslator.DropSanitizationGate.IsEnabled(), + } sanitizedLabels := make(map[string]string) for key, value := range cfg.ExternalLabels { if key == "" || value == "" { diff --git a/exporter/prometheusremotewriteexporter/go.mod b/exporter/prometheusremotewriteexporter/go.mod index d3310170b9d1d..fe1bb542aaf63 100644 --- a/exporter/prometheusremotewriteexporter/go.mod +++ b/exporter/prometheusremotewriteexporter/go.mod @@ -9,6 +9,7 @@ require ( github.com/open-telemetry/opentelemetry-collector-contrib/internal/common v0.136.0 github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.136.0 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/resourcetotelemetry v0.136.0 + github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus v0.136.0 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheusremotewrite v0.136.0 github.com/prometheus/otlptranslator v1.0.0 github.com/prometheus/prometheus v0.305.1-0.20250808193045-294f36e80261 @@ -93,7 +94,6 @@ require ( github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect - github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus v0.136.0 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect diff --git a/pkg/translator/prometheus/README.md b/pkg/translator/prometheus/README.md index 4e9b0d77cd58e..110ffbb43137e 100644 --- a/pkg/translator/prometheus/README.md +++ b/pkg/translator/prometheus/README.md @@ -13,7 +13,7 @@ | Case | Transformation | Example | |----------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------| | Unsupported characters and extraneous underscores | Replace unsupported characters with underscores (`_`). Drop redundant, leading and trailing underscores. | `(lambda).function.executions(#)` → `lambda_function_executions` | -| Standard unit | Convert the unit from [Unified Code for Units of Measure](http://unitsofmeasure.org/ucum.html) to Prometheus standard and append | `system.filesystem.usage` with unit `By` → `system_filesystem_usage_bytes` | +| Standard unit | Convert the unit from [Unified Code for Units of Measure](http://ucum.org/ucum/) to Prometheus standard and append | `system.filesystem.usage` with unit `By` → `system_filesystem_usage_bytes` | | Non-standard unit (unit is surrounded with `{}`) | Drop the unit | `system.network.dropped` with unit `{packets}` → `system_network_dropped` | | Non-standard unit (unit is **not** surrounded with `{}`) | Append the unit, if not already present, after sanitization (all non-alphanumeric chars are dropped) | `system.network.dropped` with unit `packets` → `system_network_dropped_packets` | | Percentages (unit is `1`) | Append `_ratio` (for gauges only) | `system.memory.utilization` with unit `1` → `system_memory_utilization_ratio` | @@ -82,7 +82,7 @@ OpenTelemetry *Attributes* are converted to Prometheus labels and normalized to The following transformations are performed on OpenTelemetry *Attributes* to produce Prometheus labels: * Drop unsupported characters and replace with underscores (`_`) -* Prefix label with `key_` if it doesn't start with a letter, except if it's already prefixed with double-underscore (`__`) +* Prefix label with `key_` if it doesn't start with a letter, except if it's already prefixed with double-underscore (`__`). This is to provide compatibility with OpenMetrics 1.0. By default, labels that start with a simple underscore (`_`) are prefixed with `key`, which is strictly unnecessary to follow Prometheus labels naming rules. This behavior can be disabled with the feature `pkg.translator.prometheus.PermissiveLabelSanitization`, which must be activated with the feature gate option of the collector: diff --git a/pkg/translator/prometheus/normalize_label.go b/pkg/translator/prometheus/normalize_label.go index 12e45e7f775ec..9ca6d0db0332d 100644 --- a/pkg/translator/prometheus/normalize_label.go +++ b/pkg/translator/prometheus/normalize_label.go @@ -10,7 +10,7 @@ import ( "go.opentelemetry.io/collector/featuregate" ) -var dropSanitizationGate = featuregate.GlobalRegistry().MustRegister( +var DropSanitizationGate = featuregate.GlobalRegistry().MustRegister( "pkg.translator.prometheus.PermissiveLabelSanitization", featuregate.StageAlpha, featuregate.WithRegisterDescription("Controls whether to change labels starting with '_' to 'key_'."), @@ -36,7 +36,7 @@ func NormalizeLabel(label string) string { // If label starts with a number, prepend with "key_" if unicode.IsDigit(rune(label[0])) { label = "key_" + label - } else if strings.HasPrefix(label, "_") && !strings.HasPrefix(label, "__") && !dropSanitizationGate.IsEnabled() { + } else if strings.HasPrefix(label, "_") && !strings.HasPrefix(label, "__") && !DropSanitizationGate.IsEnabled() { label = "key" + label } diff --git a/pkg/translator/prometheus/normalize_label_test.go b/pkg/translator/prometheus/normalize_label_test.go index 84578614d0eae..75771c4a0f3ae 100644 --- a/pkg/translator/prometheus/normalize_label_test.go +++ b/pkg/translator/prometheus/normalize_label_test.go @@ -12,7 +12,7 @@ import ( ) func TestSanitize(t *testing.T) { - defer testutil.SetFeatureGateForTest(t, dropSanitizationGate, false)() + defer testutil.SetFeatureGateForTest(t, DropSanitizationGate, false)() require.Empty(t, NormalizeLabel("")) require.Equal(t, "key_test", NormalizeLabel("_test")) @@ -23,7 +23,7 @@ func TestSanitize(t *testing.T) { } func TestSanitizeDropSanitization(t *testing.T) { - defer testutil.SetFeatureGateForTest(t, dropSanitizationGate, true)() + defer testutil.SetFeatureGateForTest(t, DropSanitizationGate, true)() require.Empty(t, NormalizeLabel("")) require.Equal(t, "_test", NormalizeLabel("_test")) diff --git a/pkg/translator/prometheusremotewrite/helper.go b/pkg/translator/prometheusremotewrite/helper.go index e489eda50698b..f5b520cda64f5 100644 --- a/pkg/translator/prometheusremotewrite/helper.go +++ b/pkg/translator/prometheusremotewrite/helper.go @@ -24,6 +24,8 @@ import ( "go.opentelemetry.io/collector/pdata/pmetric" conventions "go.opentelemetry.io/otel/semconv/v1.25.0" "go.uber.org/multierr" + + prometheustranslator "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus" ) const ( @@ -529,7 +531,7 @@ func addResourceTargetInfo(resource pcommon.Resource, settings Settings, timesta name = settings.Namespace + "_" + name } - labels, err := createAttributes(resource, attributes, settings.ExternalLabels, identifyingAttrs, false, otlptranslator.LabelNamer{}, model.MetricNameLabel, name) + labels, err := createAttributes(resource, attributes, settings.ExternalLabels, identifyingAttrs, false, otlptranslator.LabelNamer{PreserveMultipleUnderscores: !prometheustranslator.DropSanitizationGate.IsEnabled()}, model.MetricNameLabel, name) if err != nil { return err } diff --git a/pkg/translator/prometheusremotewrite/helper_test.go b/pkg/translator/prometheusremotewrite/helper_test.go index 61b2b6d3ff81a..6319ba54a66fb 100644 --- a/pkg/translator/prometheusremotewrite/helper_test.go +++ b/pkg/translator/prometheusremotewrite/helper_test.go @@ -244,13 +244,14 @@ func Test_timeSeriesSignature(t *testing.T) { // collision happens. It does not check whether labels are not sorted func Test_createLabelSet(t *testing.T) { tests := []struct { - name string - resource pcommon.Resource - orig pcommon.Map - externalLabels map[string]string - extras []string - want []prompb.Label - expectErr bool + name string + resource pcommon.Resource + orig pcommon.Map + externalLabels map[string]string + extras []string + want []prompb.Label + expectErr bool + underscoreLabelSanitization bool }{ { name: "labels_clean", @@ -302,6 +303,15 @@ func Test_createLabelSet(t *testing.T) { extras: []string{label31 + dirty1, value31, label32, value32}, want: getPromLabels(label11+"_", value11, "_"+label12, value12, label31+"_", value31, label32, value32), }, + { + name: "labels_dirty_with_sanitization", + resource: pcommon.NewResource(), + orig: lbs1Dirty, + externalLabels: map[string]string{}, + extras: []string{label31 + dirty1, value31, label32, value32}, + want: getPromLabels(label11+"_", value11, "key_"+label12, value12, label31+"_", value31, label32, value32), + underscoreLabelSanitization: true, + }, { name: "no_original_case", resource: pcommon.NewResource(), @@ -367,11 +377,22 @@ func Test_createLabelSet(t *testing.T) { extras: []string{label31, value31, label32, value32}, want: getPromLabels(label11, value11, label12, value12, label51, value51, label41, value41, label31, value31, label32, value32), }, + { + name: "sanitize_labels_starts_with_underscore_with_sanitization", + resource: pcommon.NewResource(), + orig: lbs3, + externalLabels: exlbs1, + extras: []string{label31, value31, label32, value32}, + want: getPromLabels(label11, value11, label12, value12, "key"+label51, value51, label41, value41, label31, value31, label32, value32), + underscoreLabelSanitization: true, + }, } - // run tests for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := createAttributes(tt.resource, tt.orig, tt.externalLabels, nil, true, otlptranslator.LabelNamer{}, tt.extras...) + labelNamer := otlptranslator.LabelNamer{ + UnderscoreLabelSanitization: tt.underscoreLabelSanitization, + } + got, err := createAttributes(tt.resource, tt.orig, tt.externalLabels, nil, true, labelNamer, tt.extras...) if tt.expectErr { require.Error(t, err) return diff --git a/pkg/translator/prometheusremotewrite/metrics_to_prw.go b/pkg/translator/prometheusremotewrite/metrics_to_prw.go index 43bf1cfe7fce4..49798afb1409b 100644 --- a/pkg/translator/prometheusremotewrite/metrics_to_prw.go +++ b/pkg/translator/prometheusremotewrite/metrics_to_prw.go @@ -15,6 +15,8 @@ import ( "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/pmetric" "go.uber.org/multierr" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus" ) type Settings struct { @@ -53,7 +55,7 @@ func newPrometheusConverter(settings Settings) *prometheusConverter { unique: map[uint64]*prompb.TimeSeries{}, conflicts: map[uint64][]*prompb.TimeSeries{}, metricNamer: otlptranslator.MetricNamer{WithMetricSuffixes: settings.AddMetricSuffixes, Namespace: settings.Namespace}, - labelNamer: otlptranslator.LabelNamer{}, + labelNamer: otlptranslator.LabelNamer{UnderscoreLabelSanitization: !prometheus.DropSanitizationGate.IsEnabled()}, unitNamer: otlptranslator.UnitNamer{}, } } diff --git a/pkg/translator/prometheusremotewrite/metrics_to_prw_v2.go b/pkg/translator/prometheusremotewrite/metrics_to_prw_v2.go index 3dffe1a9dced8..9532e907cb802 100644 --- a/pkg/translator/prometheusremotewrite/metrics_to_prw_v2.go +++ b/pkg/translator/prometheusremotewrite/metrics_to_prw_v2.go @@ -15,6 +15,8 @@ import ( "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/pmetric" "go.uber.org/multierr" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus" ) // FromMetricsV2 converts pmetric.Metrics to Prometheus remote write format 2.0. @@ -57,7 +59,7 @@ func newPrometheusConverterV2(settings Settings) *prometheusConverterV2 { conflicts: map[uint64][]*writev2.TimeSeries{}, symbolTable: writev2.NewSymbolTable(), metricNamer: otlptranslator.MetricNamer{WithMetricSuffixes: settings.AddMetricSuffixes, Namespace: settings.Namespace}, - labelNamer: otlptranslator.LabelNamer{}, + labelNamer: otlptranslator.LabelNamer{UnderscoreLabelSanitization: !prometheus.DropSanitizationGate.IsEnabled()}, unitNamer: otlptranslator.UnitNamer{}, } }