diff --git a/comp/otelcol/otlp/components/exporter/datadogexporter/traces_exporter.go b/comp/otelcol/otlp/components/exporter/datadogexporter/traces_exporter.go index c08ba36f7837..0226ef1d2b28 100644 --- a/comp/otelcol/otlp/components/exporter/datadogexporter/traces_exporter.go +++ b/comp/otelcol/otlp/components/exporter/datadogexporter/traces_exporter.go @@ -88,9 +88,9 @@ func (exp *traceExporter) consumeTraces( } switch src.Kind { case source.HostnameKind: - hosts[src.Identifier] = struct{}{} + hosts[src.Identifier] = struct{}{} //nolint:staticcheck // SA1019: intentional during Step 1 of the Source.Identifier migration (datadog-agent#51116); this call site migrates to SourceIdentifier.Primary in Step 2 case source.AWSECSFargateKind: - ecsFargateArns[src.Identifier] = struct{}{} + ecsFargateArns[src.Identifier] = struct{}{} //nolint:staticcheck // SA1019: intentional during Step 1 of the Source.Identifier migration (datadog-agent#51116); this call site migrates to SourceIdentifier.Primary in Step 2 case source.InvalidKind: } } diff --git a/comp/otelcol/otlp/components/exporter/serializerexporter/consumer.go b/comp/otelcol/otlp/components/exporter/serializerexporter/consumer.go index 2d86f594e329..efd6f8594857 100644 --- a/comp/otelcol/otlp/components/exporter/serializerexporter/consumer.go +++ b/comp/otelcol/otlp/components/exporter/serializerexporter/consumer.go @@ -11,6 +11,7 @@ import ( "fmt" "io" "net/http" + "slices" "strings" "time" @@ -23,6 +24,7 @@ import ( "github.com/DataDog/datadog-agent/comp/core/telemetry/def" "github.com/DataDog/datadog-agent/pkg/metrics" + "github.com/DataDog/datadog-agent/pkg/opentelemetry-mapping-go/otlp/attributes/source" otlpmetrics "github.com/DataDog/datadog-agent/pkg/opentelemetry-mapping-go/otlp/metrics" pb "github.com/DataDog/datadog-agent/pkg/proto/pbgo/trace" "github.com/DataDog/datadog-agent/pkg/serializer" @@ -98,7 +100,7 @@ type serializerConsumer struct { apmReceiverAddr string ipath ingestionPath hosts map[string]struct{} - ecsFargateTags map[string]struct{} + fargateTagSets map[tagSetKey][]string } // ingestionPath specifies which ingestion path is using the serializer exporter @@ -209,8 +211,13 @@ func (c *serializerConsumer) addTelemetryMetric(agentHostname string, params exp for host := range c.hosts { coatUsageMetric.Set(1.0, buildInfo.Version, buildInfo.Command, host, "") } - for ecsFargateTag := range c.ecsFargateTags { - taskArn := strings.Split(ecsFargateTag, ":")[1] + for _, tags := range c.fargateTagSets { + prefix := string(source.AWSECSFargateKind) + ":" + idx := slices.IndexFunc(tags, func(t string) bool { return strings.HasPrefix(t, prefix) }) + if idx == -1 { + continue + } + taskArn := strings.TrimPrefix(tags[idx], prefix) coatUsageMetric.Set(1.0, buildInfo.Version, buildInfo.Command, "", taskArn) } case agentOTLPIngest: @@ -324,7 +331,13 @@ func (c *serializerConsumer) ConsumeHost(host string) { c.hosts[host] = struct{}{} } -// ConsumeTag implements the metrics.TagsConsumer interface. -func (c *serializerConsumer) ConsumeTag(tag string) { - c.ecsFargateTags[tag] = struct{}{} +// ConsumeTagSet implements the metrics.TagSetConsumer interface. +func (c *serializerConsumer) ConsumeTagSet(metricSuffix string, tags []string) { + if metricSuffix != "fargate" { + return + } + sorted := slices.Clone(tags) + slices.Sort(sorted) + dedupKey := tagSetKey{metricSuffix: metricSuffix, sortedTags: strings.Join(sorted, ",")} + c.fargateTagSets[dedupKey] = sorted } diff --git a/comp/otelcol/otlp/components/exporter/serializerexporter/consumer_collector.go b/comp/otelcol/otlp/components/exporter/serializerexporter/consumer_collector.go index 98184bad2fde..2d1f4eaefee7 100644 --- a/comp/otelcol/otlp/components/exporter/serializerexporter/consumer_collector.go +++ b/comp/otelcol/otlp/components/exporter/serializerexporter/consumer_collector.go @@ -7,23 +7,31 @@ package serializerexporter import ( "fmt" + "slices" "strings" - "go.opentelemetry.io/collector/component" - "go.opentelemetry.io/collector/exporter" - telemetry "github.com/DataDog/datadog-agent/comp/core/telemetry/def" "github.com/DataDog/datadog-agent/pkg/metrics" - "github.com/DataDog/datadog-agent/pkg/opentelemetry-mapping-go/otlp/attributes/source" "github.com/DataDog/datadog-agent/pkg/tagset" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/exporter" ) +// tagSetKey namespaces a ConsumeTagSet dedup key by metricSuffix, so that two +// different workload types can never collide in seenTagSets even if their +// tag content happens to coincide. sortedTags is derived from tags themselves +// (sorted and joined) so that two calls with identical tags always dedup +type tagSetKey struct { + metricSuffix string + sortedTags string +} + // collectorConsumer is a consumer OSS collector uses to send metrics to the DataDog. type collectorConsumer struct { *serializerConsumer - seenHosts map[string]struct{} - seenTags map[string]struct{} - buildInfo component.BuildInfo + seenHosts map[string]struct{} + seenTagSets map[tagSetKey][]string // value: full "key:value" tag slice for the metric + buildInfo component.BuildInfo // getPushTime returns a Unix time in nanoseconds, representing the time pushing metrics. // It will be overwritten in tests. getPushTime func() uint64 @@ -41,17 +49,13 @@ func (c *collectorConsumer) addRuntimeTelemetryMetric(_ string, languageTags []s series = append(series, runningMetric) } - var nonFargateTags []string - for tag := range c.seenTags { - if strings.HasPrefix(tag, string(source.AWSECSFargateKind)+":") { - series = append(series, exporterFargateMetrics(timestamp, append(buildTags, tag))) - } else { - nonFargateTags = append(nonFargateTags, tag) - } + for k, tags := range c.seenTagSets { + allTags := append(slices.Clone(buildTags), tags...) + series = append(series, exporterWorkloadMetrics(k.metricSuffix, timestamp, allTags)) } - if (len(c.seenHosts) > 0 && len(c.seenTags) == 0) || len(nonFargateTags) > 0 { - tags := append(buildTags, nonFargateTags...) - series = append(series, exporterDefaultMetrics("metrics", "", timestamp, tags)) + + if len(c.seenHosts) > 0 && len(c.seenTagSets) == 0 { + series = append(series, exporterDefaultMetrics("metrics", "", timestamp, buildTags)) } for _, lang := range languageTags { @@ -70,9 +74,12 @@ func (c *collectorConsumer) ConsumeHost(host string) { c.seenHosts[host] = struct{}{} } -// ConsumeTag implements the metrics.TagsConsumer interface. -func (c *collectorConsumer) ConsumeTag(tag string) { - c.seenTags[tag] = struct{}{} +// ConsumeTagSet implements the metrics.TagSetConsumer interface. +func (c *collectorConsumer) ConsumeTagSet(metricSuffix string, tags []string) { + sorted := slices.Clone(tags) + slices.Sort(sorted) + dedupKey := tagSetKey{metricSuffix: metricSuffix, sortedTags: strings.Join(sorted, ",")} + c.seenTagSets[dedupKey] = sorted } // exporterDefaultMetrics creates built-in metrics to report that an exporter is running @@ -93,10 +100,13 @@ func exporterDefaultMetrics(exporterType string, hostname string, timestamp uint return metrics } -// exporterFargateMetrics creates a built-in metric to report that a Fargate exporter is running. -func exporterFargateMetrics(timestamp uint64, tags []string) *metrics.Serie { +// exporterWorkloadMetrics creates a built-in metric to report that a +// workload-specific exporter (e.g. Fargate, Azure Container Apps) is +// running. The resulting metric name is +// "otel.datadog_exporter.metrics.running.". +func exporterWorkloadMetrics(metricSuffix string, timestamp uint64, tags []string) *metrics.Serie { return &metrics.Serie{ - Name: "otel.datadog_exporter.metrics.running.fargate", + Name: "otel.datadog_exporter.metrics.running." + metricSuffix, Points: []metrics.Point{ { Ts: float64(timestamp), diff --git a/comp/otelcol/otlp/components/exporter/serializerexporter/consumer_collector_test.go b/comp/otelcol/otlp/components/exporter/serializerexporter/consumer_collector_test.go index 1b59374d48d4..8a5017e417de 100644 --- a/comp/otelcol/otlp/components/exporter/serializerexporter/consumer_collector_test.go +++ b/comp/otelcol/otlp/components/exporter/serializerexporter/consumer_collector_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/component" ) @@ -17,21 +18,44 @@ func newTestCollectorConsumer(buildInfo component.BuildInfo) *collectorConsumer return &collectorConsumer{ serializerConsumer: s, seenHosts: make(map[string]struct{}), - seenTags: make(map[string]struct{}), + seenTagSets: make(map[tagSetKey][]string), buildInfo: buildInfo, getPushTime: func() uint64 { return uint64(2e9) }, } } -func TestExporterFargateMetrics(t *testing.T) { - tags := []string{"version:1.0", "command:otelcontribcol"} - serie := exporterFargateMetrics(uint64(2e9), tags) +func TestExporterWorkloadMetrics(t *testing.T) { + tests := []struct { + name string + metricSuffix string + tags []string + wantName string + }{ + { + name: "fargate", + metricSuffix: "fargate", + tags: []string{"version:1.0", "command:otelcontribcol", "task_arn:arn:aws:ecs:us-east-1:123:task/cluster/abc"}, + wantName: "otel.datadog_exporter.metrics.running.fargate", + }, + { + name: "azurecontainerapps", + metricSuffix: "azurecontainerapps", + tags: []string{"version:1.0", "command:otelcontribcol", "replica_name:replica-1"}, + wantName: "otel.datadog_exporter.metrics.running.azurecontainerapps", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + serie := exporterWorkloadMetrics(tt.metricSuffix, uint64(2e9), tt.tags) - assert.Equal(t, "otel.datadog_exporter.metrics.running.fargate", serie.Name) - assert.Equal(t, 1, len(serie.Points)) - assert.Equal(t, float64(2e9), serie.Points[0].Ts) - assert.Equal(t, 1.0, serie.Points[0].Value) - assert.Equal(t, "", serie.Host) + assert.Equal(t, tt.wantName, serie.Name) + assert.Equal(t, 1, len(serie.Points)) + assert.Equal(t, float64(2e9), serie.Points[0].Ts) + assert.Equal(t, 1.0, serie.Points[0].Value) + assert.Equal(t, "", serie.Host) + }) + } } func TestAddRuntimeTelemetryMetric_NoTags(t *testing.T) { @@ -64,7 +88,8 @@ func TestAddRuntimeTelemetryMetric_HostSource(t *testing.T) { func TestAddRuntimeTelemetryMetric_FargateTags(t *testing.T) { buildInfo := component.BuildInfo{Version: "1.0", Command: "otelcontribcol"} c := newTestCollectorConsumer(buildInfo) - c.ConsumeTag("task_arn:arn:aws:ecs:us-east-1:123:task/cluster/abc") + tag := "task_arn:arn:aws:ecs:us-east-1:123:task/cluster/abc" + c.ConsumeTagSet("fargate", []string{tag}) c.addRuntimeTelemetryMetric("", nil) @@ -79,7 +104,8 @@ func TestAddRuntimeTelemetryMetric_HostAndFargate(t *testing.T) { buildInfo := component.BuildInfo{Version: "1.0", Command: "otelcontribcol"} c := newTestCollectorConsumer(buildInfo) c.ConsumeHost("my-hostname") - c.ConsumeTag("task_arn:arn:aws:ecs:us-east-1:123:task/cluster/abc") + tag := "task_arn:arn:aws:ecs:us-east-1:123:task/cluster/abc" + c.ConsumeTagSet("fargate", []string{tag}) c.addRuntimeTelemetryMetric("", nil) @@ -91,3 +117,33 @@ func TestAddRuntimeTelemetryMetric_HostAndFargate(t *testing.T) { assert.ElementsMatch(t, metricsByName["otel.datadog_exporter.metrics.running.fargate"], []string{""}) assert.Len(t, c.series, 2) } + +func TestAzureContainerAppsMetric(t *testing.T) { + buildInfo := component.BuildInfo{} + c := newTestCollectorConsumer(buildInfo) + tags := []string{ + "replica_name:replica-1", + "name:my-app", + "subscription_id:sub-123", + "resource_group:my-rg", + } + reordered := []string{ + "resource_group:my-rg", + "subscription_id:sub-123", + "name:my-app", + "replica_name:replica-1", + } + c.ConsumeTagSet("azurecontainerapps", tags) + // Same tags, different order should dedup + c.ConsumeTagSet("azurecontainerapps", reordered) + c.addRuntimeTelemetryMetric("", nil) + + require.Len(t, c.series, 1, "expected exactly one series (ACA only)") + found := c.series[0] + assert.Equal(t, "otel.datadog_exporter.metrics.running.azurecontainerapps", found.Name) + tagStrs := found.Tags.UnsafeToReadOnlySliceString() + assert.Contains(t, tagStrs, "replica_name:replica-1") + assert.Contains(t, tagStrs, "name:my-app") + assert.Contains(t, tagStrs, "subscription_id:sub-123") + assert.Contains(t, tagStrs, "resource_group:my-rg") +} diff --git a/comp/otelcol/otlp/components/exporter/serializerexporter/exporter.go b/comp/otelcol/otlp/components/exporter/serializerexporter/exporter.go index 4ff66ceea8b9..a04289078dda 100644 --- a/comp/otelcol/otlp/components/exporter/serializerexporter/exporter.go +++ b/comp/otelcol/otlp/components/exporter/serializerexporter/exporter.go @@ -151,7 +151,11 @@ func (f SourceProviderFunc) Source(ctx context.Context) (source.Source, error) { return source.Source{}, err } - return source.Source{Kind: source.HostnameKind, Identifier: hostnameIdentifier}, nil + return source.Source{ + Kind: source.HostnameKind, + Identifier: hostnameIdentifier, + SourceIdentifier: source.SourceIdentifier{Primary: hostnameIdentifier}, + }, nil } // Exporter translate OTLP metrics into the Datadog format and sends diff --git a/comp/otelcol/otlp/components/exporter/serializerexporter/exporter_test.go b/comp/otelcol/otlp/components/exporter/serializerexporter/exporter_test.go index 9907710191a8..a1fb8255bd2e 100644 --- a/comp/otelcol/otlp/components/exporter/serializerexporter/exporter_test.go +++ b/comp/otelcol/otlp/components/exporter/serializerexporter/exporter_test.go @@ -501,7 +501,7 @@ func TestRunningMetricForPayloadContents(t *testing.T) { return &collectorConsumer{ serializerConsumer: &serializerConsumer{}, seenHosts: make(map[string]struct{}), - seenTags: make(map[string]struct{}), + seenTagSets: make(map[tagSetKey][]string), getPushTime: func() uint64 { return 0 }, } } @@ -1185,7 +1185,7 @@ func initSyncSerializerForTest(t testing.TB, logger *zap.Logger, cfg *ExporterCo if err != nil { return "" } - return s.Identifier + return s.Identifier //nolint:staticcheck // SA1019: intentional during Step 1 of the Source.Identifier migration (datadog-agent#51116); this call site migrates to SourceIdentifier.Primary in Step 2 }), fx.Provide(newOrchestratorinterfaceimpl), fx.Provide(serializer.NewSerializer), diff --git a/comp/otelcol/otlp/components/exporter/serializerexporter/factory.go b/comp/otelcol/otlp/components/exporter/serializerexporter/factory.go index 3fbc487749ad..e25cd21a0d0d 100644 --- a/comp/otelcol/otlp/components/exporter/serializerexporter/factory.go +++ b/comp/otelcol/otlp/components/exporter/serializerexporter/factory.go @@ -121,7 +121,7 @@ func newFactoryForAgentWithType( apmReceiverAddr: apmReceiverAddr, ipath: ipath, hosts: make(map[string]struct{}), - ecsFargateTags: make(map[string]struct{}), + fargateTagSets: make(map[tagSetKey][]string), } }, options: options, @@ -172,7 +172,7 @@ func NewFactoryForOSSExporter(typ component.Type, statsIn chan []byte) exp.Facto return &collectorConsumer{ serializerConsumer: s, seenHosts: make(map[string]struct{}), - seenTags: make(map[string]struct{}), + seenTagSets: make(map[tagSetKey][]string), buildInfo: buildInfo, getPushTime: func() uint64 { return uint64(time.Now().Unix()) }, } diff --git a/comp/otelcol/otlp/components/exporter/serializerexporter/serializer.go b/comp/otelcol/otlp/components/exporter/serializerexporter/serializer.go index a017136b9da1..874311c0396b 100644 --- a/comp/otelcol/otlp/components/exporter/serializerexporter/serializer.go +++ b/comp/otelcol/otlp/components/exporter/serializerexporter/serializer.go @@ -188,7 +188,7 @@ func initSerializerInternal(logger *zap.Logger, cfg *ExporterConfig, sourceProvi if err != nil { return "" } - return s.Identifier + return s.Identifier //nolint:staticcheck // SA1019: intentional during Step 1 of the Source.Identifier migration (datadog-agent#51116); this call site migrates to SourceIdentifier.Primary in Step 2 }), fx.Provide(newOrchestratorinterfaceimpl), fx.Provide(serializer.NewSerializer), diff --git a/pkg/opentelemetry-mapping-go/inframetadata/reporter.go b/pkg/opentelemetry-mapping-go/inframetadata/reporter.go index 576053f35255..750de6964516 100644 --- a/pkg/opentelemetry-mapping-go/inframetadata/reporter.go +++ b/pkg/opentelemetry-mapping-go/inframetadata/reporter.go @@ -121,7 +121,7 @@ func (r *Reporter) hostname(res pcommon.Resource) (string, bool) { // The resource does not identify a host (e.g. serverless resource) return "", false } - return src.Identifier, true + return src.Identifier, true //nolint:staticcheck // SA1019: intentional during Step 1 of the Source.Identifier migration (datadog-agent#51116); this call site migrates to SourceIdentifier.Primary in Step 2 } // ConsumeResource for host metadata reporting purposes. diff --git a/pkg/opentelemetry-mapping-go/otlp/attributes/BUILD.bazel b/pkg/opentelemetry-mapping-go/otlp/attributes/BUILD.bazel index 7e145deeb2ec..f5303d185a49 100644 --- a/pkg/opentelemetry-mapping-go/otlp/attributes/BUILD.bazel +++ b/pkg/opentelemetry-mapping-go/otlp/attributes/BUILD.bazel @@ -28,6 +28,7 @@ go_library( "@io_opentelemetry_go_otel//semconv/v1.12.0:v1_12_0", "@io_opentelemetry_go_otel//semconv/v1.17.0:v1_17_0", "@io_opentelemetry_go_otel//semconv/v1.27.0:v1_27_0", + "@io_opentelemetry_go_otel//semconv/v1.40.0:v1_40_0", "@io_opentelemetry_go_otel//semconv/v1.6.1:v1_6_1", "@io_opentelemetry_go_otel_metric//:metric", ], @@ -60,6 +61,7 @@ go_test( "@io_opentelemetry_go_otel//semconv/v1.12.0:v1_12_0", "@io_opentelemetry_go_otel//semconv/v1.17.0:v1_17_0", "@io_opentelemetry_go_otel//semconv/v1.27.0:v1_27_0", + "@io_opentelemetry_go_otel//semconv/v1.40.0:v1_40_0", "@io_opentelemetry_go_otel//semconv/v1.6.1:v1_6_1", "@io_opentelemetry_go_otel_sdk_metric//:metric", "@io_opentelemetry_go_otel_sdk_metric//metricdata", diff --git a/pkg/opentelemetry-mapping-go/otlp/attributes/attributes.go b/pkg/opentelemetry-mapping-go/otlp/attributes/attributes.go index 2105580f1cd7..b9ba9cd2444f 100644 --- a/pkg/opentelemetry-mapping-go/otlp/attributes/attributes.go +++ b/pkg/opentelemetry-mapping-go/otlp/attributes/attributes.go @@ -16,6 +16,7 @@ package attributes import ( + "errors" "fmt" "strings" @@ -88,6 +89,16 @@ var ( string(semconv1_27.K8SPodNameKey): "pod_name", } + // AzureContainerAppsMappings is intentionally separate from ContainerMappings + // to avoid adding these broad attributes (e.g. service.name -> name) as + // tags on non-ACA workloads. + AzureContainerAppsMappings = map[string]string{ + AttributeAzureContainerAppInstanceID: "replica_name", + string(semconv1_27.ServiceNameKey): "name", + string(semconv1_27.CloudAccountIDKey): "subscription_id", + AttributeAzureResourceGroupName: "resource_group", + } + containerDDTags = (func() map[string]struct{} { m := make(map[string]struct{}, len(ContainerMappings)) for _, ddKey := range ContainerMappings { @@ -726,14 +737,18 @@ func GetHost(resourceAttrs pcommon.Map, fallbackHost string) string { src, srcok := SourceFromAttrs(resourceAttrs, nil) if !srcok { if v := GetOTelAttrVal(resourceAttrs, false, "_dd.hostname"); v != "" { - src = source.Source{Kind: source.HostnameKind, Identifier: v} + src = source.Source{ + Kind: source.HostnameKind, + Identifier: v, + SourceIdentifier: source.SourceIdentifier{Primary: v}, + } srcok = true } } if srcok { switch src.Kind { case source.HostnameKind: - return src.Identifier + return src.Identifier //nolint:staticcheck // SA1019: intentional during Step 1 of the Source.Identifier migration (datadog-agent#51116); this call site migrates to SourceIdentifier.Primary in Step 2 default: // We are not on a hostname (serverless), hence the hostname is empty return "" @@ -741,3 +756,41 @@ func GetHost(resourceAttrs pcommon.Map, fallbackHost string) string { } return fallbackHost } + +// azureResourceID holds the fields extracted from an Azure ARM resource ID. +type azureResourceID struct { + SubscriptionID string + ResourceGroup string + ResourceName string +} + +// parseAzureResourceID parses the cloud.resource_id string for Azure resources +// that match: /subscriptions/{sub}/resourceGroups/{rg}/providers/{provider}/{type}/{name} +// (e.g. Azure Container Apps: .../containerApps/{name}). +func parseAzureResourceID(resourceID string) (azureResourceID, error) { + if resourceID == "" { + return azureResourceID{}, errors.New("empty resource ID") + } + + parts := strings.Split(resourceID, "/") + // Example: /subscriptions/11111111.../resourceGroups/rg-name/providers/Microsoft.Web/sites/site-name + // parts[0] = "" + // parts[1] = "subscriptions" + // parts[2] = "11111111..." + // parts[3] = "resourceGroups" + // parts[4] = "rg-name" + // parts[5] = "providers" + // parts[6] = "Microsoft.App" (or Microsoft.Web) + // parts[7] = "containerApps" (or sites) + // parts[8] = "site-name" + + if len(parts) < 9 { + return azureResourceID{}, errors.New("invalid Azure resource ID format: " + resourceID) + } + + return azureResourceID{ + SubscriptionID: parts[2], + ResourceGroup: parts[4], + ResourceName: parts[8], + }, nil +} diff --git a/pkg/opentelemetry-mapping-go/otlp/attributes/azure/azure.go b/pkg/opentelemetry-mapping-go/otlp/attributes/azure/azure.go index ac7ba7243f55..04de01f9eab9 100644 --- a/pkg/opentelemetry-mapping-go/otlp/attributes/azure/azure.go +++ b/pkg/opentelemetry-mapping-go/otlp/attributes/azure/azure.go @@ -22,7 +22,10 @@ import ( ) const ( - // AttributeResourceGroupName is the Azure resource group name attribute + // AttributeResourceGroupName is the Azure resource group name attribute, + // Not the same attribute as otlp/attributes.AttributeAzureResourceGroupName + // (azure.resource_group.name — newer OTel semconv v1.43.0). + // See https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/49079. AttributeResourceGroupName = "azure.resourcegroup.name" ) diff --git a/pkg/opentelemetry-mapping-go/otlp/attributes/source.go b/pkg/opentelemetry-mapping-go/otlp/attributes/source.go index 8b425ab95c43..1d643d232bfc 100644 --- a/pkg/opentelemetry-mapping-go/otlp/attributes/source.go +++ b/pkg/opentelemetry-mapping-go/otlp/attributes/source.go @@ -16,6 +16,8 @@ package attributes import ( "go.opentelemetry.io/collector/pdata/pcommon" + semconv1_27 "go.opentelemetry.io/otel/semconv/v1.27.0" + conventionsv140 "go.opentelemetry.io/otel/semconv/v1.40.0" conventions "go.opentelemetry.io/otel/semconv/v1.6.1" "github.com/DataDog/datadog-agent/pkg/opentelemetry-mapping-go/otlp/attributes/azure" @@ -32,6 +34,17 @@ const ( // AttributeHost is a literal host tag. // We check for this to avoid double tagging. AttributeHost = "host" + // AttributeAzureContainerAppInstanceID identifies an Azure Container Apps + // replica. This is generated by the ACA resource detector and is currently + // getting added to OTel semconv. Replace with the real semconv constant once released. + AttributeAzureContainerAppInstanceID = "azure.container_app.instance.id" + // AttributeAzureResourceGroupName is OTel semconv v1.43.0 + // (azure.resource_group.name), not yet available as a Go constant. + // Replace with the real semconv constant once + // go.opentelemetry.io/otel/semconv/v1.43.0 ships. + // Not the same attribute as azure/azure.go's AttributeResourceGroupName + // (azure.resourcegroup.name) — this is the newer semconv key + AttributeAzureResourceGroupName = "azure.resource_group.name" ) func getClusterName(attrs pcommon.Map) (string, bool) { @@ -114,6 +127,14 @@ func unsanitizedHostnameFromAttributes(attrs pcommon.Map) (string, bool) { return "", false } + // If on Azure Container Apps, we don't have a hostname + if cloudPlatform, ok := attrs.Get(string(conventions.CloudPlatformKey)); ok { + p := cloudPlatform.Str() + if p == conventionsv140.CloudPlatformAzureContainerApps.Value.AsString() || p == "azure_container_apps" { + return "", false + } + } + cloudProvider, ok := attrs.Get(string(conventions.CloudProviderKey)) switch { case ok && cloudProvider.Str() == conventions.CloudProviderAWS.Value.AsString(): @@ -153,7 +174,53 @@ type HostFromAttributesHandler interface { func SourceFromAttrs(attrs pcommon.Map, hostFromAttributesHandler HostFromAttributesHandler) (source.Source, bool) { if launchType, ok := attrs.Get(string(conventions.AWSECSLaunchtypeKey)); ok && launchType.Str() == conventions.AWSECSLaunchtypeFargate.Value.AsString() { if taskARN, ok := attrs.Get(string(conventions.AWSECSTaskARNKey)); ok { - return source.Source{Kind: source.AWSECSFargateKind, Identifier: taskARN.Str()}, true + return source.Source{ + Kind: source.AWSECSFargateKind, + Identifier: taskARN.Str(), + SourceIdentifier: source.SourceIdentifier{Primary: taskARN.Str()}, + }, true + } + } + + if cloudPlatform, ok := attrs.Get(string(conventions.CloudPlatformKey)); ok { + p := cloudPlatform.Str() + if p == conventionsv140.CloudPlatformAzureContainerApps.Value.AsString() || p == "azure_container_apps" { + dims := map[string]string{} + for otelKey, ddKey := range AzureContainerAppsMappings { + if v, ok := attrs.Get(otelKey); ok && v.Str() != "" { + dims[ddKey] = v.Str() + } + } + // Fallback: derive subscription_id, resource_group, and name from cloud.resource_id + if v, ok := attrs.Get(string(semconv1_27.CloudResourceIDKey)); ok && v.Str() != "" { + if parsed, err := parseAzureResourceID(v.Str()); err == nil { + if _, ok := dims["subscription_id"]; !ok && parsed.SubscriptionID != "" { + dims["subscription_id"] = parsed.SubscriptionID + } + if _, ok := dims["resource_group"]; !ok && parsed.ResourceGroup != "" { + dims["resource_group"] = parsed.ResourceGroup + } + if _, ok := dims["name"]; !ok && parsed.ResourceName != "" { + dims["name"] = parsed.ResourceName + } + } + } + if dims["name"] != "" && dims["resource_group"] != "" && dims["subscription_id"] != "" { + // The actual unique identifier for an Azure Container App is its subscription id, + // resource group, and name combination + primary := dims["replica_name"] + if primary == "" { + primary = dims["name"] + } + return source.Source{ + Kind: source.AzureContainerAppsKind, + Identifier: primary, + SourceIdentifier: source.SourceIdentifier{ + Primary: primary, + Dimensions: dims, + }, + }, true + } } } @@ -161,7 +228,11 @@ func SourceFromAttrs(attrs pcommon.Map, hostFromAttributesHandler HostFromAttrib if hostFromAttributesHandler != nil { hostFromAttributesHandler.OnHost(host) } - return source.Source{Kind: source.HostnameKind, Identifier: host}, true + return source.Source{ + Kind: source.HostnameKind, + Identifier: host, + SourceIdentifier: source.SourceIdentifier{Primary: host}, + }, true } return source.Source{}, false diff --git a/pkg/opentelemetry-mapping-go/otlp/attributes/source/source_provider.go b/pkg/opentelemetry-mapping-go/otlp/attributes/source/source_provider.go index 9708e694b7c8..543a4c4f6b9d 100644 --- a/pkg/opentelemetry-mapping-go/otlp/attributes/source/source_provider.go +++ b/pkg/opentelemetry-mapping-go/otlp/attributes/source/source_provider.go @@ -30,6 +30,8 @@ const ( HostnameKind Kind = "host" // AWSECSFargateKind is a serverless source on AWS ECS Fargate. AWSECSFargateKind Kind = "task_arn" + // AzureContainerAppsKind is a serverless source on Azure Container Apps. + AzureContainerAppsKind Kind = "azure_container_apps" ) // Source represents a telemetry source. @@ -37,12 +39,30 @@ type Source struct { // Kind of source (serverless v. host). Kind Kind // Identifier that uniquely determines the source. - Identifier string + // + // Deprecated: use SourceIdentifier.Primary instead for any new call + // site. This field remains for existing callers during migration + // (tracked in datadog-agent#51116); it will be removed once all + // callers have moved to SourceIdentifier. + Identifier string + SourceIdentifier SourceIdentifier } // Tag associated to a source. -func (s *Source) Tag() string { - return fmt.Sprintf("%s:%s", s.Kind, s.Identifier) +func (s Source) Tag() string { + identifier := s.Identifier + if identifier == "" { + identifier = s.SourceIdentifier.Primary + } + return fmt.Sprintf("%s:%s", s.Kind, identifier) +} + +// SourceIdentifier holds the identity of a telemetry source, generalizing the +// single-string Source.Identifier to support workloads that need more than +// one identifying attribute. +type SourceIdentifier struct { + Primary string + Dimensions map[string]string } // Provider identifies a source. diff --git a/pkg/opentelemetry-mapping-go/otlp/attributes/source_test.go b/pkg/opentelemetry-mapping-go/otlp/attributes/source_test.go index 08cc80e8c066..dca4e7ace87f 100644 --- a/pkg/opentelemetry-mapping-go/otlp/attributes/source_test.go +++ b/pkg/opentelemetry-mapping-go/otlp/attributes/source_test.go @@ -19,6 +19,8 @@ import ( "github.com/stretchr/testify/assert" "go.opentelemetry.io/collector/pdata/pcommon" + semconv1_27 "go.opentelemetry.io/otel/semconv/v1.27.0" + conventionsv140 "go.opentelemetry.io/otel/semconv/v1.40.0" conventions "go.opentelemetry.io/otel/semconv/v1.6.1" "github.com/DataDog/datadog-agent/pkg/opentelemetry-mapping-go/otlp/attributes/azure" @@ -58,8 +60,12 @@ func TestSourceFromAttrs(t *testing.T) { string(conventions.HostIDKey): testHostID, string(conventions.HostNameKey): testHostName, }), - ok: true, - src: source.Source{Kind: source.HostnameKind, Identifier: testLiteralHost}, + ok: true, + src: source.Source{ + Kind: source.HostnameKind, + Identifier: testLiteralHost, + SourceIdentifier: source.SourceIdentifier{Primary: testLiteralHost}, + }, }, { name: "custom hostname", @@ -71,8 +77,12 @@ func TestSourceFromAttrs(t *testing.T) { string(conventions.HostIDKey): testHostID, string(conventions.HostNameKey): testHostName, }), - ok: true, - src: source.Source{Kind: source.HostnameKind, Identifier: testCustomName}, + ok: true, + src: source.Source{ + Kind: source.HostnameKind, + Identifier: testCustomName, + SourceIdentifier: source.SourceIdentifier{Primary: testCustomName}, + }, }, { name: "container ID", @@ -87,8 +97,12 @@ func TestSourceFromAttrs(t *testing.T) { string(conventions.HostIDKey): testHostID, string(conventions.HostNameKey): testHostName, }), - ok: true, - src: source.Source{Kind: source.HostnameKind, Identifier: testHostID}, + ok: true, + src: source.Source{ + Kind: source.HostnameKind, + Identifier: testHostID, + SourceIdentifier: source.SourceIdentifier{Primary: testHostID}, + }, }, { name: "ECS Fargate", @@ -100,8 +114,143 @@ func TestSourceFromAttrs(t *testing.T) { string(conventions.AWSECSTaskRevisionKey): "example-task-revision", string(conventions.AWSECSLaunchtypeKey): conventions.AWSECSLaunchtypeFargate.Value.AsString(), }), - ok: true, - src: source.Source{Kind: source.AWSECSFargateKind, Identifier: "example-task-ARN"}, + ok: true, + src: source.Source{ + Kind: source.AWSECSFargateKind, + Identifier: "example-task-ARN", + SourceIdentifier: source.SourceIdentifier{Primary: "example-task-ARN"}, + }, + }, + { + name: "Azure Container Apps (semconv v1.40.0 or later)", + attrs: testutils.NewAttributeMap(map[string]string{ + string(conventions.CloudProviderKey): conventions.CloudProviderAzure.Value.AsString(), + string(conventions.CloudPlatformKey): conventionsv140.CloudPlatformAzureContainerApps.Value.AsString(), + AttributeAzureContainerAppInstanceID: "replica-1", + string(conventions.ServiceNameKey): "my-app", + string(semconv1_27.CloudAccountIDKey): "sub-123", + AttributeAzureResourceGroupName: "my-rg", + }), + ok: true, + src: source.Source{ + Kind: source.AzureContainerAppsKind, + Identifier: "replica-1", + SourceIdentifier: source.SourceIdentifier{ + Primary: "replica-1", + Dimensions: map[string]string{ + "replica_name": "replica-1", + "name": "my-app", + "subscription_id": "sub-123", + "resource_group": "my-rg", + }, + }, + }, + }, + { + name: "Azure Container Apps (legacy platform value)", + attrs: testutils.NewAttributeMap(map[string]string{ + string(conventions.CloudProviderKey): conventions.CloudProviderAzure.Value.AsString(), + string(conventions.CloudPlatformKey): "azure_container_apps", + AttributeAzureContainerAppInstanceID: "replica-1", + string(conventions.ServiceNameKey): "my-app", + string(semconv1_27.CloudAccountIDKey): "sub-123", + AttributeAzureResourceGroupName: "my-rg", + }), + ok: true, + src: source.Source{ + Kind: source.AzureContainerAppsKind, + Identifier: "replica-1", + SourceIdentifier: source.SourceIdentifier{ + Primary: "replica-1", + Dimensions: map[string]string{ + "replica_name": "replica-1", + "name": "my-app", + "subscription_id": "sub-123", + "resource_group": "my-rg", + }, + }, + }, + }, + { + name: "Azure Container Apps (name, subscription_id, resource_group all from cloud.resource_id fallback)", + attrs: testutils.NewAttributeMap(map[string]string{ + string(conventions.CloudProviderKey): conventions.CloudProviderAzure.Value.AsString(), + string(conventions.CloudPlatformKey): conventionsv140.CloudPlatformAzureContainerApps.Value.AsString(), + AttributeAzureContainerAppInstanceID: "replica-1", + string(semconv1_27.CloudResourceIDKey): "/subscriptions/sub-123/resourceGroups/my-rg/providers/Microsoft.App/containerApps/my-app", + }), + ok: true, + src: source.Source{ + Kind: source.AzureContainerAppsKind, + Identifier: "replica-1", + SourceIdentifier: source.SourceIdentifier{ + Primary: "replica-1", + Dimensions: map[string]string{ + "replica_name": "replica-1", + "name": "my-app", + "subscription_id": "sub-123", + "resource_group": "my-rg", + }, + }, + }, + }, + { + name: "Azure Container Apps (resource_group from cloud.resource_id, name and subscription_id from primary attrs take precedence)", + attrs: testutils.NewAttributeMap(map[string]string{ + string(conventions.CloudProviderKey): conventions.CloudProviderAzure.Value.AsString(), + string(conventions.CloudPlatformKey): conventionsv140.CloudPlatformAzureContainerApps.Value.AsString(), + AttributeAzureContainerAppInstanceID: "replica-1", + string(conventions.ServiceNameKey): "my-app", + string(semconv1_27.CloudAccountIDKey): "sub-123", + string(semconv1_27.CloudResourceIDKey): "/subscriptions/sub-999/resourceGroups/my-rg/providers/Microsoft.App/containerApps/other-name", + }), + ok: true, + src: source.Source{ + Kind: source.AzureContainerAppsKind, + Identifier: "replica-1", + SourceIdentifier: source.SourceIdentifier{ + Primary: "replica-1", + Dimensions: map[string]string{ + "replica_name": "replica-1", + "name": "my-app", + "subscription_id": "sub-123", + "resource_group": "my-rg", + }, + }, + }, + }, + { + name: "Azure Container Apps (no replica name, falls back to name for Primary)", + attrs: testutils.NewAttributeMap(map[string]string{ + string(conventions.CloudProviderKey): conventions.CloudProviderAzure.Value.AsString(), + string(conventions.CloudPlatformKey): conventionsv140.CloudPlatformAzureContainerApps.Value.AsString(), + string(conventions.ServiceNameKey): "my-app", + string(semconv1_27.CloudAccountIDKey): "sub-123", + AttributeAzureResourceGroupName: "my-rg", + }), + ok: true, + src: source.Source{ + Kind: source.AzureContainerAppsKind, + Identifier: "my-app", + SourceIdentifier: source.SourceIdentifier{ + Primary: "my-app", + Dimensions: map[string]string{ + "name": "my-app", + "subscription_id": "sub-123", + "resource_group": "my-rg", + }, + }, + }, + }, + { + name: "Azure Container Apps (missing identifying attributes, falls through unidentified)", + attrs: testutils.NewAttributeMap(map[string]string{ + string(conventions.CloudProviderKey): conventions.CloudProviderAzure.Value.AsString(), + string(conventions.CloudPlatformKey): conventionsv140.CloudPlatformAzureContainerApps.Value.AsString(), + AttributeAzureResourceGroupName: "my-rg", + }), + ok: false, + src: source.Source{}, }, { name: "GCP", @@ -111,8 +260,12 @@ func TestSourceFromAttrs(t *testing.T) { string(conventions.HostNameKey): testGCPHostname, string(conventions.CloudAccountIDKey): testCloudAccount, }), - ok: true, - src: source.Source{Kind: source.HostnameKind, Identifier: testGCPIntegrationHostname}, + ok: true, + src: source.Source{ + Kind: source.HostnameKind, + Identifier: testGCPIntegrationHostname, + SourceIdentifier: source.SourceIdentifier{Primary: testGCPIntegrationHostname}, + }, }, { name: "GCP, no account id", @@ -129,8 +282,12 @@ func TestSourceFromAttrs(t *testing.T) { string(conventions.HostIDKey): testHostID, string(conventions.HostNameKey): testHostName, }), - ok: true, - src: source.Source{Kind: source.HostnameKind, Identifier: testHostID}, + ok: true, + src: source.Source{ + Kind: source.HostnameKind, + Identifier: testHostID, + SourceIdentifier: source.SourceIdentifier{Primary: testHostID}, + }, }, { name: "host id v. hostname", @@ -138,8 +295,12 @@ func TestSourceFromAttrs(t *testing.T) { string(conventions.HostIDKey): testHostID, string(conventions.HostNameKey): testHostName, }), - ok: true, - src: source.Source{Kind: source.HostnameKind, Identifier: testHostID}, + ok: true, + src: source.Source{ + Kind: source.HostnameKind, + Identifier: testHostID, + SourceIdentifier: source.SourceIdentifier{Primary: testHostID}, + }, }, { name: "no hostname", @@ -168,7 +329,11 @@ func TestLiteralHostNonString(t *testing.T) { attrs.PutInt(AttributeHost, 1000) src, ok := SourceFromAttrs(attrs, nil) assert.True(t, ok) - assert.Equal(t, source.Source{Kind: source.HostnameKind, Identifier: "1000"}, src) + assert.Equal(t, source.Source{ + Kind: source.HostnameKind, + Identifier: "1000", + SourceIdentifier: source.SourceIdentifier{Primary: "1000"}, + }, src) } func TestGetClusterName(t *testing.T) { diff --git a/pkg/opentelemetry-mapping-go/otlp/logs/transform.go b/pkg/opentelemetry-mapping-go/otlp/logs/transform.go index addb193c65ee..c68195931d01 100644 --- a/pkg/opentelemetry-mapping-go/otlp/logs/transform.go +++ b/pkg/opentelemetry-mapping-go/otlp/logs/transform.go @@ -236,13 +236,13 @@ func flattenAttribute(key string, val pcommon.Value, depth int) map[string]any { func extractHostNameAndServiceName(resourceAttrs pcommon.Map, logAttrs pcommon.Map) (host string, service string) { if src, ok := attributes.SourceFromAttrs(resourceAttrs, nil); ok && src.Kind == source.HostnameKind { - host = src.Identifier + host = src.Identifier //nolint:staticcheck // SA1019: intentional during Step 1 of the Source.Identifier migration (datadog-agent#51116); this call site migrates to SourceIdentifier.Primary in Step 2 } // HACK: Check for host in log record attributes if not present in resource attributes. // This is not aligned with the specification and will be removed in the future. if host == "" { if src, ok := attributes.SourceFromAttrs(logAttrs, nil); ok && src.Kind == source.HostnameKind { - host = src.Identifier + host = src.Identifier //nolint:staticcheck // SA1019: intentional during Step 1 of the Source.Identifier migration (datadog-agent#51116); this call site migrates to SourceIdentifier.Primary in Step 2 } } if s, ok := resourceAttrs.Get(string(conventions.ServiceNameKey)); ok { diff --git a/pkg/opentelemetry-mapping-go/otlp/logs/translator.go b/pkg/opentelemetry-mapping-go/otlp/logs/translator.go index 21eece9b6fc1..3b2586b77e9c 100644 --- a/pkg/opentelemetry-mapping-go/otlp/logs/translator.go +++ b/pkg/opentelemetry-mapping-go/otlp/logs/translator.go @@ -78,7 +78,7 @@ func NewTranslatorWithHTTPClient(set component.TelemetrySettings, attributesTran func (t *Translator) hostNameAndServiceNameFromResource(ctx context.Context, res pcommon.Resource, hostFromAttributesHandler attributes.HostFromAttributesHandler) (host string, service string) { if src, ok := t.attributesTranslator.ResourceToSource(ctx, res, signalTypeSet, hostFromAttributesHandler); ok && src.Kind == source.HostnameKind { - host = src.Identifier + host = src.Identifier //nolint:staticcheck // SA1019: intentional during Step 1 of the Source.Identifier migration (datadog-agent#51116); this call site migrates to SourceIdentifier.Primary in Step 2 } if s, ok := res.Attributes().Get(string(conventions.ServiceNameKey)); ok { service = s.AsString() @@ -88,7 +88,7 @@ func (t *Translator) hostNameAndServiceNameFromResource(ctx context.Context, res func (t *Translator) hostFromAttributes(ctx context.Context, attrs pcommon.Map) string { if src, ok := t.attributesTranslator.AttributesToSource(ctx, attrs); ok && src.Kind == source.HostnameKind { - return src.Identifier + return src.Identifier //nolint:staticcheck // SA1019: intentional during Step 1 of the Source.Identifier migration (datadog-agent#51116); this call site migrates to SourceIdentifier.Primary in Step 2 } return "" } diff --git a/pkg/opentelemetry-mapping-go/otlp/metrics/consumer.go b/pkg/opentelemetry-mapping-go/otlp/metrics/consumer.go index 93572ac57400..98c56449cda6 100644 --- a/pkg/opentelemetry-mapping-go/otlp/metrics/consumer.go +++ b/pkg/opentelemetry-mapping-go/otlp/metrics/consumer.go @@ -148,7 +148,23 @@ type HostConsumer interface { // It is an optional interface that can be implemented by a Consumer. // Consumed tags are used for running metrics, and should represent // some resource running a Collector (e.g. Fargate task). +// +// Deprecated: use TagSetConsumer instead. This interface remains for +// consumers that haven't migrated yet; translators no longer call it. type TagsConsumer interface { // ConsumeTag consumes a tag ConsumeTag(tag string) } + +// TagSetConsumer is a multi-tag source consumer. +// It is an optional interface that can be implemented by a Consumer. +// Use it for any source that needs one or more tags on its own dedicated +// running metric (e.g. Fargate, Azure Container Apps). +type TagSetConsumer interface { + // ConsumeTagSet consumes a multi-tag source for running metric emission. + // metricSuffix names the workload-specific metric: the resulting metric + // is "otel.datadog_exporter.metrics.running." (e.g. "fargate", + // "azurecontainerapps"). + // tags is the full slice of "key:value" strings to attach to the metric. + ConsumeTagSet(metricSuffix string, tags []string) +} diff --git a/pkg/opentelemetry-mapping-go/otlp/metrics/metrics_translator.go b/pkg/opentelemetry-mapping-go/otlp/metrics/metrics_translator.go index 445dae20bbb9..a7dd659a7cf1 100644 --- a/pkg/opentelemetry-mapping-go/otlp/metrics/metrics_translator.go +++ b/pkg/opentelemetry-mapping-go/otlp/metrics/metrics_translator.go @@ -96,7 +96,7 @@ var _ source.Provider = (*noSourceProvider)(nil) type noSourceProvider struct{} func (*noSourceProvider) Source(context.Context) (source.Source, error) { - return source.Source{Kind: source.HostnameKind, Identifier: ""}, nil + return source.Source{Kind: source.HostnameKind, Identifier: "", SourceIdentifier: source.SourceIdentifier{Primary: ""}}, nil } // defaultTranslator is the default metrics translator implementation. @@ -387,6 +387,15 @@ func getQuantileTag(quantile float64) string { return "quantile:" + formatFloat(quantile) } +// tagsFromDimensions converts a Source.SourceIdentifier.Dimensions map into a "key:value" tag slice +func tagsFromDimensions(dims map[string]string) []string { + tags := make([]string, 0, len(dims)) + for k, v := range dims { + tags = append(tags, k+":"+v) + } + return tags +} + // resolveSource determines the source from resource attributes, falling back to the fallbackSourceProvider if no source is found. func resolveSource(ctx context.Context, attributesTranslator *attributes.Translator, res pcommon.Resource, fallbackSourceProvider source.Provider, hostFromAttributesHandler attributes.HostFromAttributesHandler) (source.Source, error) { src, hasSource := attributesTranslator.ResourceToSource(ctx, res, signalTypeSet, hostFromAttributesHandler) @@ -516,7 +525,7 @@ func (t *defaultTranslator) MapMetrics(ctx context.Context, md pmetric.Metrics, var host string if src.Kind == source.HostnameKind { - host = src.Identifier + host = src.Identifier //nolint:staticcheck // SA1019: intentional during Step 1 of the Source.Identifier migration (datadog-agent#51116); this call site migrates to SourceIdentifier.Primary in Step 2 // Don't consume the host yet, first check if we have any nonAPM metrics. } @@ -611,8 +620,12 @@ func (t *defaultTranslator) MapMetrics(ctx context.Context, md pmetric.Metrics, c.ConsumeHost(host) } case source.AWSECSFargateKind: - if c, ok := consumer.(TagsConsumer); ok { - c.ConsumeTag(src.Tag()) + if c, ok := consumer.(TagSetConsumer); ok { + c.ConsumeTagSet("fargate", []string{src.Tag()}) + } + case source.AzureContainerAppsKind: + if c, ok := consumer.(TagSetConsumer); ok { + c.ConsumeTagSet("azurecontainerapps", tagsFromDimensions(src.SourceIdentifier.Dimensions)) } } } diff --git a/pkg/opentelemetry-mapping-go/otlp/metrics/metrics_translator_test.go b/pkg/opentelemetry-mapping-go/otlp/metrics/metrics_translator_test.go index df131b7bafde..b1efe3a39546 100644 --- a/pkg/opentelemetry-mapping-go/otlp/metrics/metrics_translator_test.go +++ b/pkg/opentelemetry-mapping-go/otlp/metrics/metrics_translator_test.go @@ -96,8 +96,9 @@ type testProvider string func (t testProvider) Source(context.Context) (source.Source, error) { return source.Source{ - Kind: source.HostnameKind, - Identifier: string(t), + Kind: source.HostnameKind, + Identifier: string(t), + SourceIdentifier: source.SourceIdentifier{Primary: string(t)}, }, nil } diff --git a/pkg/opentelemetry-mapping-go/otlp/metrics/minimal_translator.go b/pkg/opentelemetry-mapping-go/otlp/metrics/minimal_translator.go index 239e96098da3..e4c12eeb3f5d 100644 --- a/pkg/opentelemetry-mapping-go/otlp/metrics/minimal_translator.go +++ b/pkg/opentelemetry-mapping-go/otlp/metrics/minimal_translator.go @@ -90,7 +90,7 @@ func (t *minimalTranslator) MapMetrics(ctx context.Context, md pmetric.Metrics, var host string if src.Kind == source.HostnameKind { - host = src.Identifier + host = src.Identifier //nolint:staticcheck // SA1019: intentional during Step 1 of the Source.Identifier migration (datadog-agent#51116); this call site migrates to SourceIdentifier.Primary in Step 2 // Don't consume the host yet, first check if we have any nonAPM metrics. } @@ -151,8 +151,12 @@ func (t *minimalTranslator) MapMetrics(ctx context.Context, md pmetric.Metrics, c.ConsumeHost(host) } case source.AWSECSFargateKind: - if c, ok := consumer.(TagsConsumer); ok { - c.ConsumeTag(src.Tag()) + if c, ok := consumer.(TagSetConsumer); ok { + c.ConsumeTagSet("fargate", []string{src.Tag()}) + } + case source.AzureContainerAppsKind: + if c, ok := consumer.(TagSetConsumer); ok { + c.ConsumeTagSet("azurecontainerapps", tagsFromDimensions(src.SourceIdentifier.Dimensions)) } } } diff --git a/pkg/trace/api/otlp.go b/pkg/trace/api/otlp.go index 73ebb19d6ba4..7255c6f98611 100644 --- a/pkg/trace/api/otlp.go +++ b/pkg/trace/api/otlp.go @@ -301,7 +301,7 @@ func (o *OTLPReceiver) receiveResourceSpansV2(ctx context.Context, rspans ptrace if srcok { switch src.Kind { case source.HostnameKind: - hostname = src.Identifier + hostname = src.Identifier //nolint:staticcheck // SA1019: intentional during Step 1 of the Source.Identifier migration (datadog-agent#51116); this call site migrates to SourceIdentifier.Primary in Step 2 default: // We are not on a hostname (serverless), hence the hostname is empty hostname = "" @@ -309,7 +309,11 @@ func (o *OTLPReceiver) receiveResourceSpansV2(ctx context.Context, rspans ptrace } else { // fallback hostname hostname = o.conf.Hostname - src = source.Source{Kind: source.HostnameKind, Identifier: hostname} + src = source.Source{ + Kind: source.HostnameKind, + Identifier: hostname, + SourceIdentifier: source.SourceIdentifier{Primary: hostname}, + } } // Create a single accessor for all resource-level semantic lookups below, avoiding @@ -417,7 +421,11 @@ func (o *OTLPReceiver) receiveResourceSpansV1(ctx context.Context, rspans ptrace hostFromMap := func(m map[string]string, key string) { // hostFromMap sets the hostname to m[key] if it is set. if v, ok := m[key]; ok { - src = source.Source{Kind: source.HostnameKind, Identifier: v} + src = source.Source{ + Kind: source.HostnameKind, + Identifier: v, + SourceIdentifier: source.SourceIdentifier{Primary: v}, + } srcok = true } } @@ -503,7 +511,7 @@ func (o *OTLPReceiver) receiveResourceSpansV1(ctx context.Context, rspans ptrace if srcok { switch src.Kind { case source.HostnameKind: - hostname = src.Identifier + hostname = src.Identifier //nolint:staticcheck // SA1019: intentional during Step 1 of the Source.Identifier migration (datadog-agent#51116); this call site migrates to SourceIdentifier.Primary in Step 2 default: // We are not on a hostname (serverless), hence the hostname is empty hostname = "" @@ -511,7 +519,11 @@ func (o *OTLPReceiver) receiveResourceSpansV1(ctx context.Context, rspans ptrace } else { // fallback hostname hostname = o.conf.Hostname - src = source.Source{Kind: source.HostnameKind, Identifier: hostname} + src = source.Source{ + Kind: source.HostnameKind, + Identifier: hostname, + SourceIdentifier: source.SourceIdentifier{Primary: hostname}, + } } p.TracerPayload = &pb.TracerPayload{ Hostname: hostname, diff --git a/pkg/trace/api/otlp_test.go b/pkg/trace/api/otlp_test.go index 6b6616b057b9..7bf38e1253f8 100644 --- a/pkg/trace/api/otlp_test.go +++ b/pkg/trace/api/otlp_test.go @@ -1393,7 +1393,7 @@ func testOTLPHostname(enableReceiveResourceSpansV2 bool, t *testing.T) { }, }).Traces().ResourceSpans().At(0), http.Header{}, nil) assert.Equal(t, src.Kind, source.HostnameKind) - assert.Equal(t, src.Identifier, tt.out) + assert.Equal(t, src.Identifier, tt.out) //nolint:staticcheck // SA1019: intentional during Step 1 of the Source.Identifier migration (datadog-agent#51116); this call site migrates to SourceIdentifier.Primary in Step 2 timeout := time.After(500 * time.Millisecond) select { case <-timeout: diff --git a/pkg/trace/transform/transform.go b/pkg/trace/transform/transform.go index a75987736a14..b0b272e2e7c4 100644 --- a/pkg/trace/transform/transform.go +++ b/pkg/trace/transform/transform.go @@ -202,14 +202,18 @@ func GetOTelHostname(span ptrace.Span, res pcommon.Resource, tr *attributes.Tran src, srcok := tr.ResourceToSource(ctx, res, SignalTypeSet, nil) if !srcok { if v := GetOTelAttrValInResAndSpanAttrs(span, res, false, "_dd.hostname"); v != "" { - src = source.Source{Kind: source.HostnameKind, Identifier: v} + src = source.Source{ + Kind: source.HostnameKind, + Identifier: v, + SourceIdentifier: source.SourceIdentifier{Primary: v}, + } srcok = true } } if srcok { switch src.Kind { case source.HostnameKind: - return src.Identifier + return src.Identifier //nolint:staticcheck // SA1019: intentional during Step 1 of the Source.Identifier migration (datadog-agent#51116); this call site migrates to SourceIdentifier.Primary in Step 2 default: // We are not on a hostname (serverless), hence the hostname is empty return ""