From e066c9cdd3ebc1fd288e1e74479cc3f6fa9caa61 Mon Sep 17 00:00:00 2001
From: mackjmr <63265430+mackjmr@users.noreply.github.com>
Date: Fri, 5 Nov 2021 19:34:34 +0100
Subject: [PATCH] [exporter/datadog]: Add container tags to attributes package
 (#6086)

* add container tags to attributes package

* Make changes based on feedback

* Change comment based on Feedback

* Fix lint issues
---
 .../internal/attributes/attributes.go         | 65 +++++++++++++++++--
 .../internal/attributes/attributes_test.go    | 29 +++++++++
 exporter/datadogexporter/translate_traces.go  | 22 +------
 3 files changed, 91 insertions(+), 25 deletions(-)

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