Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fa014bf
Add AzureContainerAppsKind OTel source detection
kathiehuang May 20, 2026
bd31574
Add semconv v1.40.0 to bazel build file
kathiehuang May 20, 2026
1b4d0f6
Handle AzureContainerAppsKind in metrics and trace translators
kathiehuang May 20, 2026
79018ec
Check FaaSInstanceID instead of ServiceInstanceID and remove containe…
kathiehuang Jul 1, 2026
021dec7
Refactor Source.Identifier from string to struct with Primary and Dim…
kathiehuang Jul 8, 2026
8319847
Add AzureContainerAppsMappings and create Azure Container App source …
kathiehuang Jul 9, 2026
4325af0
Add TagSetConsumer interface and wire Azure Container Apps into metri…
kathiehuang Jul 9, 2026
27e751a
Generalize TagSetConsumer so Fargate and future workloads can use it too
kathiehuang Jul 10, 2026
d67e1c1
Migrate Fargate and ACA running metrics onto generalized TagSetConsumer
kathiehuang Jul 10, 2026
c517a39
Derive redundant TagSetConsumer key param internally
kathiehuang Jul 10, 2026
c29ca7f
Skip Azure Container App source emission for TagsConsumer-only consumers
kathiehuang Jul 10, 2026
268be4e
Fix tag-slice aliasing and clarify comments
kathiehuang Jul 10, 2026
14f5e78
Identify ACA source even if replica name is unset
kathiehuang Jul 13, 2026
21583cd
Move conversion of Source.Identifier.Dimensions map to sorted tag sli…
kathiehuang Jul 13, 2026
c4d6e4a
Remove TagsConsumer interface completely in favor of TagSetConsumer
kathiehuang Jul 13, 2026
b3d8ef7
Extract task_arn prefix and skip emission if not found
kathiehuang Jul 13, 2026
72b5b53
Add SourceIdentifier as a new field on Source, revert Source.Identifi…
kathiehuang Jul 15, 2026
43c2b8e
Add nolint comments for source.Identifier callers
kathiehuang Jul 15, 2026
1f167fa
More nolint comments
kathiehuang Jul 15, 2026
4aa33b0
Fall back to SourceIdentifier.Primary when Identifier is empty in Sou…
kathiehuang Jul 16, 2026
dfa6738
Submit metric only if all identifying attributes are present
kathiehuang Jul 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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:
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"fmt"
"io"
"net/http"
"slices"
"strings"
"time"

Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))
}
Comment thread
Copilot marked this conversation as resolved.
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 {
Expand All @@ -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
}
Comment thread
Copilot marked this conversation as resolved.

// exporterDefaultMetrics creates built-in metrics to report that an exporter is running
Expand All @@ -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.<metricSuffix>".
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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/collector/component"
)

Expand All @@ -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) {
Expand Down Expand Up @@ -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)

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

Expand All @@ -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")
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
}
}
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()) },
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
2 changes: 1 addition & 1 deletion pkg/opentelemetry-mapping-go/inframetadata/reporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions pkg/opentelemetry-mapping-go/otlp/attributes/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
Expand Down Expand Up @@ -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",
Expand Down
Loading
Loading