diff --git a/config/config.go b/config/config.go index a94342d841..dfa3c7cd61 100644 --- a/config/config.go +++ b/config/config.go @@ -1431,6 +1431,7 @@ var ( type OTLPConfig struct { PromoteResourceAttributes []string `yaml:"promote_resource_attributes,omitempty"` TranslationStrategy translationStrategyOption `yaml:"translation_strategy,omitempty"` + ServiceNameInTargetInfo bool `yaml:"service_name_in_target_info,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. diff --git a/config/config_test.go b/config/config_test.go index 77cbf9b2eb..2fed3a06ee 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1554,6 +1554,20 @@ func TestOTLPSanitizeResourceAttributes(t *testing.T) { }) } +func TestOTLPAllowServiceNameInTargetInfo(t *testing.T) { + t.Run("good config", func(t *testing.T) { + want, err := LoadFile(filepath.Join("testdata", "otlp_allow_service_name_in_target_info.good.yml"), false, promslog.NewNopLogger()) + require.NoError(t, err) + + out, err := yaml.Marshal(want) + require.NoError(t, err) + var got Config + require.NoError(t, yaml.UnmarshalStrict(out, &got)) + + require.Equal(t, true, got.OTLPConfig.ServiceNameInTargetInfo) + }) +} + func TestOTLPAllowUTF8(t *testing.T) { t.Run("good config", func(t *testing.T) { fpath := filepath.Join("testdata", "otlp_allow_utf8.good.yml") diff --git a/config/testdata/otlp_allow_service_name_in_target_info.good.yml b/config/testdata/otlp_allow_service_name_in_target_info.good.yml new file mode 100644 index 0000000000..3bd389d2eb --- /dev/null +++ b/config/testdata/otlp_allow_service_name_in_target_info.good.yml @@ -0,0 +1,2 @@ +otlp: + service_name_in_target_info: true diff --git a/storage/remote/otlptranslator/prometheusremotewrite/helper.go b/storage/remote/otlptranslator/prometheusremotewrite/helper.go index 89e00f3759..e15ca4b56b 100644 --- a/storage/remote/otlptranslator/prometheusremotewrite/helper.go +++ b/storage/remote/otlptranslator/prometheusremotewrite/helper.go @@ -22,6 +22,7 @@ import ( "fmt" "log" "math" + "slices" "sort" "strconv" "unicode/utf8" @@ -116,7 +117,7 @@ var seps = []byte{'\xff'} // if logOnOverwrite is true, the overwrite is logged. Resulting label names are sanitized. // If settings.PromoteResourceAttributes is not empty, it's a set of resource attributes that should be promoted to labels. func createAttributes(resource pcommon.Resource, attributes pcommon.Map, settings Settings, - logOnOverwrite bool, extras ...string) []prompb.Label { + ignoreAttrs []string, logOnOverwrite bool, extras ...string) []prompb.Label { resourceAttrs := resource.Attributes() serviceName, haveServiceName := resourceAttrs.Get(conventions.AttributeServiceName) instance, haveInstanceID := resourceAttrs.Get(conventions.AttributeServiceInstanceID) @@ -146,7 +147,9 @@ func createAttributes(resource pcommon.Resource, attributes pcommon.Map, setting // XXX: Should we always drop service namespace/service name/service instance ID from the labels // (as they get mapped to other Prometheus labels)? attributes.Range(func(key string, value pcommon.Value) bool { - labels = append(labels, prompb.Label{Name: key, Value: value.AsString()}) + if !slices.Contains(ignoreAttrs, key) { + labels = append(labels, prompb.Label{Name: key, Value: value.AsString()}) + } return true }) sort.Stable(ByLabelName(labels)) @@ -248,7 +251,7 @@ func (c *PrometheusConverter) addHistogramDataPoints(ctx context.Context, dataPo pt := dataPoints.At(x) timestamp := convertTimeStamp(pt.Timestamp()) - baseLabels := createAttributes(resource, pt.Attributes(), settings, false) + baseLabels := createAttributes(resource, pt.Attributes(), settings, nil, false) // If the sum is unset, it indicates the _sum metric point should be // omitted @@ -448,7 +451,7 @@ func (c *PrometheusConverter) addSummaryDataPoints(ctx context.Context, dataPoin pt := dataPoints.At(x) timestamp := convertTimeStamp(pt.Timestamp()) - baseLabels := createAttributes(resource, pt.Attributes(), settings, false) + baseLabels := createAttributes(resource, pt.Attributes(), settings, nil, false) // treat sum as a sample in an individual TimeSeries sum := &prompb.Sample{ @@ -597,7 +600,11 @@ func addResourceTargetInfo(resource pcommon.Resource, settings Settings, timesta } settings.PromoteResourceAttributes = nil - labels := createAttributes(resource, attributes, settings, false, model.MetricNameLabel, name) + if settings.ServiceNameInTargetInfo { + // Do not pass identifying attributes as ignoreAttrs below. + identifyingAttrs = nil + } + labels := createAttributes(resource, attributes, settings, identifyingAttrs, false, model.MetricNameLabel, name) haveIdentifier := false for _, l := range labels { if l.Name == model.JobLabel || l.Name == model.InstanceLabel { diff --git a/storage/remote/otlptranslator/prometheusremotewrite/helper_test.go b/storage/remote/otlptranslator/prometheusremotewrite/helper_test.go index b3b93dcc06..b4bc704d4e 100644 --- a/storage/remote/otlptranslator/prometheusremotewrite/helper_test.go +++ b/storage/remote/otlptranslator/prometheusremotewrite/helper_test.go @@ -49,15 +49,44 @@ func TestCreateAttributes(t *testing.T) { } attrs := pcommon.NewMap() attrs.PutStr("metric-attr", "metric value") + attrs.PutStr("metric-attr-other", "metric value other") testCases := []struct { name string promoteResourceAttributes []string + ignoreAttrs []string expectedLabels []prompb.Label }{ { name: "Successful conversion without resource attribute promotion", promoteResourceAttributes: nil, + expectedLabels: []prompb.Label{ + { + Name: "__name__", + Value: "test_metric", + }, + { + Name: "instance", + Value: "service ID", + }, + { + Name: "job", + Value: "service name", + }, + { + Name: "metric_attr", + Value: "metric value", + }, + { + Name: "metric_attr_other", + Value: "metric value other", + }, + }, + }, + { + name: "Successful conversion with some attributes ignored", + promoteResourceAttributes: nil, + ignoreAttrs: []string{"metric-attr-other"}, expectedLabels: []prompb.Label{ { Name: "__name__", @@ -97,6 +126,10 @@ func TestCreateAttributes(t *testing.T) { Name: "metric_attr", Value: "metric value", }, + { + Name: "metric_attr_other", + Value: "metric value other", + }, { Name: "existent_attr", Value: "resource value", @@ -127,6 +160,10 @@ func TestCreateAttributes(t *testing.T) { Name: "metric_attr", Value: "metric value", }, + { + Name: "metric_attr_other", + Value: "metric value other", + }, }, }, { @@ -153,6 +190,10 @@ func TestCreateAttributes(t *testing.T) { Name: "metric_attr", Value: "metric value", }, + { + Name: "metric_attr_other", + Value: "metric value other", + }, }, }, } @@ -161,7 +202,7 @@ func TestCreateAttributes(t *testing.T) { settings := Settings{ PromoteResourceAttributes: tc.promoteResourceAttributes, } - lbls := createAttributes(resource, attrs, settings, false, model.MetricNameLabel, "test_metric") + lbls := createAttributes(resource, attrs, settings, tc.ignoreAttrs, false, model.MetricNameLabel, "test_metric") assert.ElementsMatch(t, lbls, tc.expectedLabels) }) diff --git a/storage/remote/otlptranslator/prometheusremotewrite/histograms.go b/storage/remote/otlptranslator/prometheusremotewrite/histograms.go index dfe0c9f90e..8349d4f907 100644 --- a/storage/remote/otlptranslator/prometheusremotewrite/histograms.go +++ b/storage/remote/otlptranslator/prometheusremotewrite/histograms.go @@ -54,6 +54,7 @@ func (c *PrometheusConverter) addExponentialHistogramDataPoints(ctx context.Cont resource, pt.Attributes(), settings, + nil, true, model.MetricNameLabel, promName, diff --git a/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw.go b/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw.go index 63862c4a70..1979b9185a 100644 --- a/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw.go +++ b/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw.go @@ -39,6 +39,7 @@ type Settings struct { AddMetricSuffixes bool AllowUTF8 bool PromoteResourceAttributes []string + ServiceNameInTargetInfo bool } // PrometheusConverter converts from OTel write format to Prometheus remote write format. diff --git a/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw_test.go b/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw_test.go index 64d0ebd6f5..2bb29ecd7a 100644 --- a/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw_test.go +++ b/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw_test.go @@ -28,41 +28,73 @@ import ( "go.opentelemetry.io/collector/pdata/pmetric" "go.opentelemetry.io/collector/pdata/pmetric/pmetricotlp" + "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/prompb" prometheustranslator "github.com/prometheus/prometheus/storage/remote/otlptranslator/prometheus" ) func TestFromMetrics(t *testing.T) { - t.Run("successful", func(t *testing.T) { - converter := NewPrometheusConverter() - payload := createExportRequest(5, 128, 128, 2, 0) - var expMetadata []prompb.MetricMetadata - resourceMetricsSlice := payload.Metrics().ResourceMetrics() - for i := 0; i < resourceMetricsSlice.Len(); i++ { - scopeMetricsSlice := resourceMetricsSlice.At(i).ScopeMetrics() - for j := 0; j < scopeMetricsSlice.Len(); j++ { - metricSlice := scopeMetricsSlice.At(j).Metrics() - for k := 0; k < metricSlice.Len(); k++ { - metric := metricSlice.At(k) - promName := prometheustranslator.BuildCompliantName(metric, "", false, false) - expMetadata = append(expMetadata, prompb.MetricMetadata{ - Type: otelMetricTypeToPromMetricType(metric), - MetricFamilyName: promName, - Help: metric.Description(), - Unit: metric.Unit(), - }) + + for _, serviceNameInTargetInfo := range []bool{false, true} { + t.Run(fmt.Sprintf("successful/keepIdentifyingAttributes=%v", serviceNameInTargetInfo), func(t *testing.T) { + converter := NewPrometheusConverter() + payload := createExportRequest(5, 128, 128, 2, 0) + var expMetadata []prompb.MetricMetadata + resourceMetricsSlice := payload.Metrics().ResourceMetrics() + for i := 0; i < resourceMetricsSlice.Len(); i++ { + scopeMetricsSlice := resourceMetricsSlice.At(i).ScopeMetrics() + for j := 0; j < scopeMetricsSlice.Len(); j++ { + metricSlice := scopeMetricsSlice.At(j).Metrics() + for k := 0; k < metricSlice.Len(); k++ { + metric := metricSlice.At(k) + promName := prometheustranslator.BuildCompliantName(metric, "", false, false) + expMetadata = append(expMetadata, prompb.MetricMetadata{ + Type: otelMetricTypeToPromMetricType(metric), + MetricFamilyName: promName, + Help: metric.Description(), + Unit: metric.Unit(), + }) + } } } - } - annots, err := converter.FromMetrics(context.Background(), payload.Metrics(), Settings{}) - require.NoError(t, err) - require.Empty(t, annots) + annots, err := converter.FromMetrics( + context.Background(), + payload.Metrics(), + Settings{ServiceNameInTargetInfo: serviceNameInTargetInfo}, + ) + require.NoError(t, err) + require.Empty(t, annots) - if diff := cmp.Diff(expMetadata, converter.Metadata()); diff != "" { - t.Errorf("mismatch (-want +got):\n%s", diff) - } - }) + if diff := cmp.Diff(expMetadata, converter.Metadata()); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + + ts := converter.TimeSeries() + require.Len(t, ts, 1408+1) // +1 for the target_info. + + target_info_count := 0 + for _, s := range ts { + b := labels.NewScratchBuilder(2) + lbls := s.ToLabels(&b, nil) + if lbls.Get(labels.MetricName) == "target_info" { + target_info_count++ + require.Equal(t, "test-namespace/test-service", lbls.Get("job")) + require.Equal(t, "id1234", lbls.Get("instance")) + if serviceNameInTargetInfo { + require.Equal(t, "test-service", lbls.Get("service_name")) + require.Equal(t, "test-namespace", lbls.Get("service_namespace")) + require.Equal(t, "id1234", lbls.Get("service_instance_id")) + } else { + require.False(t, lbls.Has("service_name")) + require.False(t, lbls.Has("service_namespace")) + require.False(t, lbls.Has("service_instance_id")) + } + } + } + require.Equal(t, 1, target_info_count) + }) + } t.Run("context cancellation", func(t *testing.T) { converter := NewPrometheusConverter() @@ -169,6 +201,15 @@ func createExportRequest(resourceAttributeCount, histogramCount, nonHistogramCou rm := request.Metrics().ResourceMetrics().AppendEmpty() generateAttributes(rm.Resource().Attributes(), "resource", resourceAttributeCount) + // Fake some resource attributes. + for k, v := range map[string]string{ + "service.name": "test-service", + "service.namespace": "test-namespace", + "service.instance.id": "id1234", + } { + rm.Resource().Attributes().PutStr(k, v) + } + metrics := rm.ScopeMetrics().AppendEmpty().Metrics() ts := pcommon.NewTimestampFromTime(time.Now()) diff --git a/storage/remote/otlptranslator/prometheusremotewrite/number_data_points.go b/storage/remote/otlptranslator/prometheusremotewrite/number_data_points.go index 035c0676c4..6cdab450e1 100644 --- a/storage/remote/otlptranslator/prometheusremotewrite/number_data_points.go +++ b/storage/remote/otlptranslator/prometheusremotewrite/number_data_points.go @@ -40,6 +40,7 @@ func (c *PrometheusConverter) addGaugeNumberDataPoints(ctx context.Context, data resource, pt.Attributes(), settings, + nil, true, model.MetricNameLabel, name, @@ -75,6 +76,7 @@ func (c *PrometheusConverter) addSumNumberDataPoints(ctx context.Context, dataPo resource, pt.Attributes(), settings, + nil, true, model.MetricNameLabel, name, diff --git a/storage/remote/write_handler.go b/storage/remote/write_handler.go index f719ea6a1f..67c7f7627b 100644 --- a/storage/remote/write_handler.go +++ b/storage/remote/write_handler.go @@ -515,6 +515,7 @@ func (h *otlpWriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { AddMetricSuffixes: true, AllowUTF8: otlpCfg.TranslationStrategy == config.NoUTF8EscapingWithSuffixes, PromoteResourceAttributes: otlpCfg.PromoteResourceAttributes, + ServiceNameInTargetInfo: otlpCfg.ServiceNameInTargetInfo, }) if err != nil { h.logger.Warn("Error translating OTLP metrics to Prometheus write request", "err", err)