diff --git a/exporter/datadogexporter/internal/attributes/attributes.go b/exporter/datadogexporter/internal/attributes/attributes.go index 9d13138d0460..448af20bb70c 100644 --- a/exporter/datadogexporter/internal/attributes/attributes.go +++ b/exporter/datadogexporter/internal/attributes/attributes.go @@ -16,6 +16,7 @@ package attributes import ( "fmt" + "strings" "go.opentelemetry.io/collector/model/pdata" conventions "go.opentelemetry.io/collector/model/semconv/v1.5.0" @@ -31,6 +32,12 @@ var ( conventions.AttributeServiceName: "service", conventions.AttributeServiceVersion: "version", + // Containers + conventions.AttributeContainerID: "container_id", + conventions.AttributeContainerName: "container_name", + conventions.AttributeContainerImageName: "image_name", + conventions.AttributeContainerImageTag: "image_tag", + // Cloud conventions // https://www.datadoghq.com/blog/tagging-best-practices/ conventions.AttributeCloudProvider: "cloud_provider", @@ -39,19 +46,50 @@ var ( // ECS conventions // https://github.com/DataDog/datadog-agent/blob/e081bed/pkg/tagger/collectors/ecs_extract.go - conventions.AttributeAWSECSTaskFamily: "task_family", - conventions.AttributeAWSECSClusterARN: "ecs_cluster_name", - "aws.ecs.task.revision": "task_version", + conventions.AttributeAWSECSTaskFamily: "task_family", + conventions.AttributeAWSECSTaskARN: "task_arn", + conventions.AttributeAWSECSClusterARN: "ecs_cluster_name", + conventions.AttributeAWSECSTaskRevision: "task_version", + conventions.AttributeAWSECSContainerARN: "ecs_container_name", // Kubernetes resource name (via semantic conventions) // https://github.com/DataDog/datadog-agent/blob/e081bed/pkg/util/kubernetes/const.go - conventions.AttributeK8SPodName: "pod_name", + conventions.AttributeK8SContainerName: "kube_container_name", + conventions.AttributeK8SClusterName: "kube_cluster_name", conventions.AttributeK8SDeploymentName: "kube_deployment", conventions.AttributeK8SReplicaSetName: "kube_replica_set", conventions.AttributeK8SStatefulSetName: "kube_stateful_set", conventions.AttributeK8SDaemonSetName: "kube_daemon_set", conventions.AttributeK8SJobName: "kube_job", conventions.AttributeK8SCronJobName: "kube_cronjob", + conventions.AttributeK8SNamespaceName: "kube_namespace", + conventions.AttributeK8SPodName: "pod_name", + } + + // containerTagsAttributes contains a set of attributes that will be extracted as Datadog container tags. + containerTagsAttributes = []string{ + conventions.AttributeContainerID, + conventions.AttributeContainerName, + conventions.AttributeContainerImageName, + conventions.AttributeContainerImageTag, + conventions.AttributeK8SContainerName, + conventions.AttributeK8SClusterName, + conventions.AttributeK8SDeploymentName, + conventions.AttributeK8SReplicaSetName, + conventions.AttributeK8SStatefulSetName, + conventions.AttributeK8SDaemonSetName, + conventions.AttributeK8SJobName, + conventions.AttributeK8SCronJobName, + conventions.AttributeK8SNamespaceName, + conventions.AttributeK8SPodName, + conventions.AttributeCloudProvider, + conventions.AttributeCloudRegion, + conventions.AttributeCloudAvailabilityZone, + conventions.AttributeAWSECSTaskFamily, + conventions.AttributeAWSECSTaskARN, + conventions.AttributeAWSECSClusterARN, + conventions.AttributeAWSECSTaskRevision, + conventions.AttributeAWSECSContainerARN, } // Kubernetes mappings defines the mapping between Kubernetes conventions (both general and Datadog specific) @@ -120,3 +158,22 @@ func TagsFromAttributes(attrs pdata.AttributeMap) []string { return tags } + +// ContainerTagFromAttributes extracts the value of _dd.tags.container from the given +// set of attributes. +func ContainerTagFromAttributes(attr map[string]string) string { + var str strings.Builder + for _, key := range containerTagsAttributes { + val, ok := attr[key] + if !ok { + continue + } + if str.Len() > 0 { + str.WriteByte(',') + } + str.WriteString(conventionsMapping[key]) + str.WriteByte(':') + str.WriteString(val) + } + return str.String() +} diff --git a/exporter/datadogexporter/internal/attributes/attributes_test.go b/exporter/datadogexporter/internal/attributes/attributes_test.go index dee964bc56da..fc52bc0e0285 100644 --- a/exporter/datadogexporter/internal/attributes/attributes_test.go +++ b/exporter/datadogexporter/internal/attributes/attributes_test.go @@ -52,3 +52,32 @@ func TestTagsFromAttributesEmpty(t *testing.T) { assert.Equal(t, []string{}, TagsFromAttributes(attrs)) } + +func TestContainerTagFromAttributes(t *testing.T) { + attributeMap := map[string]string{ + conventions.AttributeContainerName: "sample_app", + conventions.AttributeContainerImageTag: "sample_app_image_tag", + conventions.AttributeK8SContainerName: "kube_sample_app", + conventions.AttributeK8SReplicaSetName: "sample_replica_set", + conventions.AttributeK8SDaemonSetName: "sample_daemonset_name", + conventions.AttributeK8SPodName: "sample_pod_name", + conventions.AttributeCloudProvider: "sample_cloud_provider", + conventions.AttributeCloudRegion: "sample_region", + conventions.AttributeCloudAvailabilityZone: "sample_zone", + conventions.AttributeAWSECSTaskFamily: "sample_task_family", + conventions.AttributeAWSECSClusterARN: "sample_ecs_cluster_name", + conventions.AttributeAWSECSContainerARN: "sample_ecs_container_name", + "custom_tag": "example_custom_tag", + "": "empty_string_key", + "empty_string_val": "", + } + + assert.Equal(t, "container_name:sample_app,image_tag:sample_app_image_tag,kube_container_name:kube_sample_app,kube_replica_set:sample_replica_set,kube_daemon_set:sample_daemonset_name,pod_name:sample_pod_name,cloud_provider:sample_cloud_provider,region:sample_region,zone:sample_zone,task_family:sample_task_family,ecs_cluster_name:sample_ecs_cluster_name,ecs_container_name:sample_ecs_container_name", ContainerTagFromAttributes(attributeMap)) +} + +func TestContainerTagFromAttributesEmpty(t *testing.T) { + var empty string + attributeMap := map[string]string{} + + assert.Equal(t, empty, ContainerTagFromAttributes(attributeMap)) +} diff --git a/exporter/datadogexporter/translate_traces.go b/exporter/datadogexporter/translate_traces.go index fc35b2935389..8137ee37f0f4 100644 --- a/exporter/datadogexporter/translate_traces.go +++ b/exporter/datadogexporter/translate_traces.go @@ -19,7 +19,6 @@ import ( "encoding/json" "fmt" "strconv" - "strings" "time" "github.com/DataDog/datadog-agent/pkg/trace/exportable/pb" @@ -390,29 +389,10 @@ func aggregateSpanTags(span pdata.Span, datadogTags map[string]string) map[strin }) // we don't want to normalize these tags since `_dd` is a special case - spanTags[tagContainersTags] = buildDatadogContainerTags(spanTags) + spanTags[tagContainersTags] = attributes.ContainerTagFromAttributes(spanTags) return spanTags } -// buildDatadogContainerTags returns container and orchestrator tags belonging to containerID -// as a comma delimeted list for datadog's special container tag key -func buildDatadogContainerTags(spanTags map[string]string) string { - var b strings.Builder - - if val, ok := spanTags[conventions.AttributeContainerID]; ok { - b.WriteString(fmt.Sprintf("%s:%s,", "container_id", val)) - } - if val, ok := spanTags[conventions.AttributeK8SPodName]; ok { - b.WriteString(fmt.Sprintf("%s:%s,", "pod_name", val)) - } - - if val, ok := spanTags[conventions.AttributeAWSECSTaskARN]; ok { - b.WriteString(fmt.Sprintf("%s:%s,", "task_arn", val)) - } - - return strings.TrimSuffix(b.String(), ",") -} - // inferDatadogTypes returns a string for the datadog type based on metadata // in the otel span. DB semantic conventions state that what datadog // would mark as a db or cache span type, otel marks as a CLIENT span kind, but