diff --git a/CHANGELOG.md b/CHANGELOG.md index a9072422a2..47053760c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * [CHANGE] Make setting out-of-order native histograms feature (`--enable-feature=ooo-native-histograms`) a no-op. Out-of-order native histograms are now always enabled when `out_of_order_time_window` is greater than zero and `--enable-feature=native-histograms` is set. #16207 * [FEATURE] OTLP translate: Add feature flag for optionally translating OTel explicit bucket histograms into native histograms with custom buckets. #15850 +* [FEATURE] OTLP translate: Add option to receive OTLP metrics without translating names or attributes. #16441 * [ENHANCEMENT] TSDB: add `prometheus_tsdb_wal_replay_unknown_refs_total` and `prometheus_tsdb_wbl_replay_unknown_refs_total` metrics to track unknown series references during WAL/WBL replay. #16166 * [BUGFIX] TSDB: fix unknown series errors and possible lost data during WAL replay when series are removed from the head due to inactivity and reappear before the next WAL checkpoint. #16060 diff --git a/config/config.go b/config/config.go index 09c79b3501..f140044baa 100644 --- a/config/config.go +++ b/config/config.go @@ -110,9 +110,9 @@ func Load(s string, logger *slog.Logger) (*Config, error) { switch cfg.OTLPConfig.TranslationStrategy { case UnderscoreEscapingWithSuffixes: case "": - case NoUTF8EscapingWithSuffixes: + case NoTranslation, NoUTF8EscapingWithSuffixes: if cfg.GlobalConfig.MetricNameValidationScheme == LegacyValidationConfig { - return nil, errors.New("OTLP translation strategy NoUTF8EscapingWithSuffixes is not allowed when UTF8 is disabled") + return nil, fmt.Errorf("OTLP translation strategy %q is not allowed when UTF8 is disabled", cfg.OTLPConfig.TranslationStrategy) } default: return nil, fmt.Errorf("unsupported OTLP translation strategy %q", cfg.OTLPConfig.TranslationStrategy) @@ -1509,6 +1509,21 @@ var ( // and label name characters that are not alphanumerics/underscores to underscores. // Unit and type suffixes may be appended to metric names, according to certain rules. UnderscoreEscapingWithSuffixes translationStrategyOption = "UnderscoreEscapingWithSuffixes" + // NoTranslation (EXPERIMENTAL): disables all translation of incoming metric + // and label names. This offers a way for the OTLP users to use native metric names, reducing confusion. + // + // WARNING: This setting has significant known risks and limitations (see + // https://prometheus.io/docs/practices/naming/ for details): + // * Impaired UX when using PromQL in plain YAML (e.g. alerts, rules, dashboard, autoscaling configuration). + // * Series collisions which in the best case may result in OOO errors, in the worst case a silently malformed + // time series. For instance, you may end up in situation of ingesting `foo.bar` series with unit + // `seconds` and a separate series `foo.bar` with unit `milliseconds`. + // + // As a result, this setting is experimental and currently, should not be used in + // production systems. + // + // TODO(ArthurSens): Mention `type-and-unit-labels` feature (https://github.com/prometheus/proposals/pull/39) once released, as potential mitigation of the above risks. + NoTranslation translationStrategyOption = "NoTranslation" ) // OTLPConfig is the configuration for writing to the OTLP endpoint. diff --git a/config/config_test.go b/config/config_test.go index 6d59c7220d..236b062898 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1677,7 +1677,7 @@ func TestOTLPConvertHistogramsToNHCB(t *testing.T) { } func TestOTLPAllowUTF8(t *testing.T) { - t.Run("good config", func(t *testing.T) { + t.Run("good config - NoUTF8EscapingWithSuffixes", func(t *testing.T) { fpath := filepath.Join("testdata", "otlp_allow_utf8.good.yml") verify := func(t *testing.T, conf *Config, err error) { t.Helper() @@ -1697,11 +1697,51 @@ func TestOTLPAllowUTF8(t *testing.T) { }) }) - t.Run("incompatible config", func(t *testing.T) { + t.Run("incompatible config - NoUTF8EscapingWithSuffixes", func(t *testing.T) { fpath := filepath.Join("testdata", "otlp_allow_utf8.incompatible.yml") verify := func(t *testing.T, err error) { t.Helper() - require.ErrorContains(t, err, `OTLP translation strategy NoUTF8EscapingWithSuffixes is not allowed when UTF8 is disabled`) + require.ErrorContains(t, err, `OTLP translation strategy "NoUTF8EscapingWithSuffixes" is not allowed when UTF8 is disabled`) + } + + t.Run("LoadFile", func(t *testing.T) { + _, err := LoadFile(fpath, false, promslog.NewNopLogger()) + verify(t, err) + }) + t.Run("Load", func(t *testing.T) { + content, err := os.ReadFile(fpath) + require.NoError(t, err) + _, err = Load(string(content), promslog.NewNopLogger()) + t.Log("err", err) + verify(t, err) + }) + }) + + t.Run("good config - NoTranslation", func(t *testing.T) { + fpath := filepath.Join("testdata", "otlp_no_translation.good.yml") + verify := func(t *testing.T, conf *Config, err error) { + t.Helper() + require.NoError(t, err) + require.Equal(t, NoTranslation, conf.OTLPConfig.TranslationStrategy) + } + + t.Run("LoadFile", func(t *testing.T) { + conf, err := LoadFile(fpath, false, promslog.NewNopLogger()) + verify(t, conf, err) + }) + t.Run("Load", func(t *testing.T) { + content, err := os.ReadFile(fpath) + require.NoError(t, err) + conf, err := Load(string(content), promslog.NewNopLogger()) + verify(t, conf, err) + }) + }) + + t.Run("incompatible config - NoTranslation", func(t *testing.T) { + fpath := filepath.Join("testdata", "otlp_no_translation.incompatible.yml") + verify := func(t *testing.T, err error) { + t.Helper() + require.ErrorContains(t, err, `OTLP translation strategy "NoTranslation" is not allowed when UTF8 is disabled`) } t.Run("LoadFile", func(t *testing.T) { diff --git a/config/testdata/otlp_no_translation.good.yml b/config/testdata/otlp_no_translation.good.yml new file mode 100644 index 0000000000..e5c4460842 --- /dev/null +++ b/config/testdata/otlp_no_translation.good.yml @@ -0,0 +1,2 @@ +otlp: + translation_strategy: NoTranslation diff --git a/config/testdata/otlp_no_translation.incompatible.yml b/config/testdata/otlp_no_translation.incompatible.yml new file mode 100644 index 0000000000..33c5a756f5 --- /dev/null +++ b/config/testdata/otlp_no_translation.incompatible.yml @@ -0,0 +1,4 @@ +global: + metric_name_validation_scheme: legacy +otlp: + translation_strategy: NoTranslation diff --git a/storage/remote/write_handler.go b/storage/remote/write_handler.go index cbd4225d08..fa77fc398f 100644 --- a/storage/remote/write_handler.go +++ b/storage/remote/write_handler.go @@ -579,8 +579,8 @@ func (rw *rwExporter) ConsumeMetrics(ctx context.Context, md pmetric.Metrics) er converter := otlptranslator.NewPrometheusConverter() annots, err := converter.FromMetrics(ctx, md, otlptranslator.Settings{ - AddMetricSuffixes: true, - AllowUTF8: otlpCfg.TranslationStrategy == config.NoUTF8EscapingWithSuffixes, + AddMetricSuffixes: otlpCfg.TranslationStrategy != config.NoTranslation, + AllowUTF8: otlpCfg.TranslationStrategy != config.UnderscoreEscapingWithSuffixes, PromoteResourceAttributes: otlpCfg.PromoteResourceAttributes, KeepIdentifyingResourceAttributes: otlpCfg.KeepIdentifyingResourceAttributes, ConvertHistogramsToNHCB: otlpCfg.ConvertHistogramsToNHCB, diff --git a/storage/remote/write_test.go b/storage/remote/write_test.go index a3b30b6425..9d32067a6d 100644 --- a/storage/remote/write_test.go +++ b/storage/remote/write_test.go @@ -382,7 +382,118 @@ func TestWriteStorageApplyConfig_PartialUpdate(t *testing.T) { func TestOTLPWriteHandler(t *testing.T) { exportRequest := generateOTLPWriteRequest() + timestamp := time.Now() + for _, testCase := range []struct { + name string + otlpCfg config.OTLPConfig + expectedSamples []mockSample + }{ + { + name: "NoTranslation", + otlpCfg: config.OTLPConfig{ + TranslationStrategy: config.NoTranslation, + }, + expectedSamples: []mockSample{ + { + l: labels.New(labels.Label{Name: "__name__", Value: "test.counter"}, + labels.Label{Name: "foo.bar", Value: "baz"}, + labels.Label{Name: "instance", Value: "test-instance"}, + labels.Label{Name: "job", Value: "test-service"}), + t: timestamp.UnixMilli(), + v: 10.0, + }, + { + l: labels.New( + labels.Label{Name: "__name__", Value: "target_info"}, + labels.Label{Name: "host.name", Value: "test-host"}, + labels.Label{Name: "instance", Value: "test-instance"}, + labels.Label{Name: "job", Value: "test-service"}, + ), + t: timestamp.UnixMilli(), + v: 1, + }, + }, + }, + { + name: "UnderscoreEscapingWithSuffixes", + otlpCfg: config.OTLPConfig{ + TranslationStrategy: config.UnderscoreEscapingWithSuffixes, + }, + expectedSamples: []mockSample{ + { + l: labels.New(labels.Label{Name: "__name__", Value: "test_counter_total"}, + labels.Label{Name: "foo_bar", Value: "baz"}, + labels.Label{Name: "instance", Value: "test-instance"}, + labels.Label{Name: "job", Value: "test-service"}), + t: timestamp.UnixMilli(), + v: 10.0, + }, + { + l: labels.New( + labels.Label{Name: "__name__", Value: "target_info"}, + labels.Label{Name: "host_name", Value: "test-host"}, + labels.Label{Name: "instance", Value: "test-instance"}, + labels.Label{Name: "job", Value: "test-service"}, + ), + t: timestamp.UnixMilli(), + v: 1, + }, + }, + }, + { + name: "NoUTF8EscapingWithSuffixes", + otlpCfg: config.OTLPConfig{ + TranslationStrategy: config.NoUTF8EscapingWithSuffixes, + }, + expectedSamples: []mockSample{ + { + l: labels.New(labels.Label{Name: "__name__", Value: "test.counter_total"}, + labels.Label{Name: "foo.bar", Value: "baz"}, + labels.Label{Name: "instance", Value: "test-instance"}, + labels.Label{Name: "job", Value: "test-service"}), + t: timestamp.UnixMilli(), + v: 10.0, + }, + { + l: labels.New( + labels.Label{Name: "__name__", Value: "target_info"}, + labels.Label{Name: "host.name", Value: "test-host"}, + labels.Label{Name: "instance", Value: "test-instance"}, + labels.Label{Name: "job", Value: "test-service"}, + ), + t: timestamp.UnixMilli(), + v: 1, + }, + }, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + appendable := handleOTLP(t, exportRequest, testCase.otlpCfg) + for _, sample := range testCase.expectedSamples { + requireContainsSample(t, appendable.samples, sample) + } + require.Len(t, appendable.samples, 12) // 1 (counter) + 1 (gauge) + 1 (target_info) + 7 (hist_bucket) + 2 (hist_sum, hist_count) + require.Len(t, appendable.histograms, 1) // 1 (exponential histogram) + require.Len(t, appendable.exemplars, 1) // 1 (exemplar) + }) + } +} + +func requireContainsSample(t *testing.T, actual []mockSample, expected mockSample) { + t.Helper() + + for _, got := range actual { + if labels.Equal(expected.l, got.l) && expected.t == got.t && expected.v == got.v { + return + } + } + require.Fail(t, fmt.Sprintf("Sample not found: \n"+ + "expected: %v\n"+ + "actual : %v", expected, actual)) +} + +func handleOTLP(t *testing.T, exportRequest pmetricotlp.ExportRequest, otlpCfg config.OTLPConfig) *mockAppendable { buf, err := exportRequest.MarshalProto() require.NoError(t, err) @@ -393,19 +504,16 @@ func TestOTLPWriteHandler(t *testing.T) { appendable := &mockAppendable{} handler := NewOTLPWriteHandler(nil, nil, appendable, func() config.Config { return config.Config{ - OTLPConfig: config.DefaultOTLPConfig, + OTLPConfig: otlpCfg, } }, OTLPOptions{}) - recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, req) resp := recorder.Result() require.Equal(t, http.StatusOK, resp.StatusCode) - require.Len(t, appendable.samples, 12) // 1 (counter) + 1 (gauge) + 1 (target_info) + 7 (hist_bucket) + 2 (hist_sum, hist_count) - require.Len(t, appendable.histograms, 1) // 1 (exponential histogram) - require.Len(t, appendable.exemplars, 1) // 1 (exemplar) + return appendable } func generateOTLPWriteRequest() pmetricotlp.ExportRequest { @@ -426,7 +534,7 @@ func generateOTLPWriteRequest() pmetricotlp.ExportRequest { // Generate One Counter counterMetric := scopeMetric.Metrics().AppendEmpty() - counterMetric.SetName("test-counter") + counterMetric.SetName("test.counter") counterMetric.SetDescription("test-counter-description") counterMetric.SetEmptySum() counterMetric.Sum().SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) @@ -446,7 +554,7 @@ func generateOTLPWriteRequest() pmetricotlp.ExportRequest { // Generate One Gauge gaugeMetric := scopeMetric.Metrics().AppendEmpty() - gaugeMetric.SetName("test-gauge") + gaugeMetric.SetName("test.gauge") gaugeMetric.SetDescription("test-gauge-description") gaugeMetric.SetEmptyGauge() @@ -457,7 +565,7 @@ func generateOTLPWriteRequest() pmetricotlp.ExportRequest { // Generate One Histogram histogramMetric := scopeMetric.Metrics().AppendEmpty() - histogramMetric.SetName("test-histogram") + histogramMetric.SetName("test.histogram") histogramMetric.SetDescription("test-histogram-description") histogramMetric.SetEmptyHistogram() histogramMetric.Histogram().SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) @@ -472,7 +580,7 @@ func generateOTLPWriteRequest() pmetricotlp.ExportRequest { // Generate One Exponential-Histogram exponentialHistogramMetric := scopeMetric.Metrics().AppendEmpty() - exponentialHistogramMetric.SetName("test-exponential-histogram") + exponentialHistogramMetric.SetName("test.exponential.histogram") exponentialHistogramMetric.SetDescription("test-exponential-histogram-description") exponentialHistogramMetric.SetEmptyExponentialHistogram() exponentialHistogramMetric.ExponentialHistogram().SetAggregationTemporality(pmetric.AggregationTemporalityCumulative)