Skip to content

Commit 16c70e3

Browse files
mx-psipmatyjasek-sumo
authored andcommitted
[datadogexporter] Improve support of semantic conventions for K8s, Azure and ECS (#2623)
TL;DR: - Add support for the new Azure resource detector - Improve support for Kubernetes semantic conventions - Improve support for Kubernetes labels - Improve support for ECS semantic conventions ### Azure - The `host.id` (Azure VM ID) is sent as a host alias now, to replicate the Datadog Agent behavior. - The cluster name is taken from the resource group ID. ### ECS - Task family, cluster ARN and task revision are mapped to Datadog conventions. `container.id` priority to be taken as a hostname is lowered since we only want this as a last resort. ### Kubernetes - OpenTelemetry Kubernetes semantic conventions are mapped to Datadog conventions. - Kubernetes labels conventions (both Datadog-specific and general recommendations) are mapped to Datadog conventions. - We now take the Kubernetes cluster name on Azure (see above) and AWS.
1 parent e52ac29 commit 16c70e3

File tree

10 files changed

+323
-14
lines changed

10 files changed

+323
-14
lines changed

exporter/datadogexporter/attributes/attributes.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,53 @@
1515
package attributes
1616

1717
import (
18+
"fmt"
19+
1820
"go.opentelemetry.io/collector/consumer/pdata"
1921
"go.opentelemetry.io/collector/translator/conventions"
2022
)
2123

24+
var (
25+
// conventionsMappings defines the mapping between OpenTelemetry semantic conventions
26+
// and Datadog Agent conventions
27+
conventionsMapping = map[string]string{
28+
// ECS conventions
29+
// https://github.com/DataDog/datadog-agent/blob/e081bed/pkg/tagger/collectors/ecs_extract.go
30+
conventions.AttributeAWSECSTaskFamily: "task_family",
31+
conventions.AttributeAWSECSClusterARN: "ecs_cluster_name",
32+
"aws.ecs.task.revision": "task_version",
33+
34+
// Kubernetes resource name (via semantic conventions)
35+
// https://github.com/DataDog/datadog-agent/blob/e081bed/pkg/util/kubernetes/const.go
36+
conventions.AttributeK8sPod: "pod_name",
37+
conventions.AttributeK8sDeployment: "kube_deployment",
38+
conventions.AttributeK8sReplicaSet: "kube_replica_set",
39+
conventions.AttributeK8sStatefulSet: "kube_stateful_set",
40+
conventions.AttributeK8sDaemonSet: "kube_daemon_set",
41+
conventions.AttributeK8sJob: "kube_job",
42+
conventions.AttributeK8sCronJob: "kube_cronjob",
43+
}
44+
45+
// Kubernetes mappings defines the mapping between Kubernetes conventions (both general and Datadog specific)
46+
// and Datadog Agent conventions. The Datadog Agent conventions can be found at
47+
// https://github.com/DataDog/datadog-agent/blob/e081bed/pkg/tagger/collectors/const.go and
48+
// https://github.com/DataDog/datadog-agent/blob/e081bed/pkg/util/kubernetes/const.go
49+
kubernetesMapping = map[string]string{
50+
// Standard Datadog labels
51+
"tags.datadoghq.com/env": "env",
52+
"tags.datadoghq.com/service": "service",
53+
"tags.datadoghq.com/version": "version",
54+
55+
// Standard Kubernetes labels
56+
"app.kubernetes.io/name": "kube_app_name",
57+
"app.kubernetes.io/instance": "kube_app_instance",
58+
"app.kubernetes.io/version": "kube_app_version",
59+
"app.kuberenetes.io/component": "kube_app_component",
60+
"app.kubernetes.io/part-of": "kube_app_part_of",
61+
"app.kubernetes.io/managed-by": "kube_app_managed_by",
62+
}
63+
)
64+
2265
// TagsFromAttributes converts a selected list of attributes
2366
// to a tag list that can be added to metrics.
2467
func TagsFromAttributes(attrs pdata.AttributeMap) []string {
@@ -47,6 +90,16 @@ func TagsFromAttributes(attrs pdata.AttributeMap) []string {
4790
case conventions.AttributeOSType:
4891
systemAttributes.OSType = value.StringVal()
4992
}
93+
94+
// conventions mapping
95+
if datadogKey, found := conventionsMapping[key]; found && value.StringVal() != "" {
96+
tags = append(tags, fmt.Sprintf("%s:%s", datadogKey, value.StringVal()))
97+
}
98+
99+
// Kubernetes labels mapping
100+
if datadogKey, found := kubernetesMapping[key]; found && value.StringVal() != "" {
101+
tags = append(tags, fmt.Sprintf("%s:%s", datadogKey, value.StringVal()))
102+
}
50103
})
51104

52105
tags = append(tags, processAttributes.extractTags()...)

exporter/datadogexporter/attributes/attributes_test.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,18 @@ func TestTagsFromAttributes(t *testing.T) {
3232
conventions.AttributeProcessID: pdata.NewAttributeValueInt(1),
3333
conventions.AttributeProcessOwner: pdata.NewAttributeValueString("root"),
3434
conventions.AttributeOSType: pdata.NewAttributeValueString("LINUX"),
35+
conventions.AttributeK8sDaemonSet: pdata.NewAttributeValueString("daemon_set_name"),
36+
conventions.AttributeAWSECSClusterARN: pdata.NewAttributeValueString("cluster_arn"),
37+
"tags.datadoghq.com/service": pdata.NewAttributeValueString("service_name"),
3538
}
3639
attrs := pdata.NewAttributeMap().InitFromMap(attributeMap)
3740

38-
assert.Equal(t, []string{
41+
assert.ElementsMatch(t, []string{
3942
fmt.Sprintf("%s:%s", conventions.AttributeProcessExecutableName, "otelcol"),
4043
fmt.Sprintf("%s:%s", conventions.AttributeOSType, "LINUX"),
44+
fmt.Sprintf("%s:%s", "kube_daemon_set", "daemon_set_name"),
45+
fmt.Sprintf("%s:%s", "ecs_cluster_name", "cluster_arn"),
46+
fmt.Sprintf("%s:%s", "service", "service_name"),
4147
}, TagsFromAttributes(attrs))
4248
}
4349

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright The OpenTelemetry Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
package azure
15+
16+
import (
17+
"strings"
18+
19+
"go.opentelemetry.io/collector/consumer/pdata"
20+
"go.opentelemetry.io/collector/translator/conventions"
21+
)
22+
23+
const (
24+
// AttributeResourceGroupName is the Azure resource group name attribute
25+
AttributeResourceGroupName = "azure.resourcegroup.name"
26+
)
27+
28+
// HostInfo has the Azure host information
29+
type HostInfo struct {
30+
HostAliases []string
31+
}
32+
33+
// HostInfoFromAttributes gets Azure host info from attributes following
34+
// OpenTelemetry semantic conventions
35+
func HostInfoFromAttributes(attrs pdata.AttributeMap) (hostInfo *HostInfo) {
36+
hostInfo = &HostInfo{}
37+
38+
// Add Azure VM ID as a host alias if available for compatibility with Azure integration
39+
if vmID, ok := attrs.Get(conventions.AttributeHostID); ok {
40+
hostInfo.HostAliases = append(hostInfo.HostAliases, vmID.StringVal())
41+
}
42+
43+
return
44+
}
45+
46+
// HostnameFromAttributes gets the Azure hostname from attributes
47+
func HostnameFromAttributes(attrs pdata.AttributeMap) (string, bool) {
48+
if hostname, ok := attrs.Get(conventions.AttributeHostName); ok {
49+
return hostname.StringVal(), true
50+
}
51+
52+
return "", false
53+
}
54+
55+
// ClusterNameFromAttributes gets the Azure cluster name from attributes
56+
func ClusterNameFromAttributes(attrs pdata.AttributeMap) (string, bool) {
57+
// Get cluster name from resource group
58+
// https://github.com/DataDog/datadog-agent/blob/aad29b8/pkg/util/azure/azure.go#L51
59+
if resourceGroup, ok := attrs.Get(AttributeResourceGroupName); ok {
60+
splitAll := strings.Split(resourceGroup.StringVal(), "_")
61+
if len(splitAll) < 4 || strings.ToLower(splitAll[0]) != "mc" {
62+
return "", false // Failed to parse
63+
}
64+
return splitAll[len(splitAll)-2], true
65+
}
66+
67+
return "", false
68+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright The OpenTelemetry Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
package azure
15+
16+
import (
17+
"testing"
18+
19+
"github.com/stretchr/testify/assert"
20+
"github.com/stretchr/testify/require"
21+
"go.opentelemetry.io/collector/translator/conventions"
22+
23+
"github.com/open-telemetry/opentelemetry-collector-contrib/exporter/datadogexporter/testutils"
24+
)
25+
26+
var (
27+
testVMID = "02aab8a4-74ef-476e-8182-f6d2ba4166a6"
28+
testHostname = "test-hostname"
29+
testAttrs = testutils.NewAttributeMap(map[string]string{
30+
conventions.AttributeCloudProvider: conventions.AttributeCloudProviderAzure,
31+
conventions.AttributeHostName: testHostname,
32+
conventions.AttributeCloudRegion: "location",
33+
conventions.AttributeHostID: testVMID,
34+
conventions.AttributeCloudAccount: "subscriptionID",
35+
AttributeResourceGroupName: "resourceGroup",
36+
})
37+
testEmpty = testutils.NewAttributeMap(map[string]string{})
38+
)
39+
40+
func TestHostInfoFromAttributes(t *testing.T) {
41+
hostInfo := HostInfoFromAttributes(testAttrs)
42+
require.NotNil(t, hostInfo)
43+
assert.ElementsMatch(t, hostInfo.HostAliases, []string{testVMID})
44+
45+
emptyHostInfo := HostInfoFromAttributes(testEmpty)
46+
require.NotNil(t, emptyHostInfo)
47+
assert.ElementsMatch(t, emptyHostInfo.HostAliases, []string{})
48+
}
49+
50+
func TestHostnameFromAttributes(t *testing.T) {
51+
hostname, ok := HostnameFromAttributes(testAttrs)
52+
assert.True(t, ok)
53+
assert.Equal(t, hostname, testHostname)
54+
55+
_, ok = HostnameFromAttributes(testEmpty)
56+
assert.False(t, ok)
57+
}
58+
59+
func TestClusterNameFromAttributes(t *testing.T) {
60+
cluster, ok := ClusterNameFromAttributes(testutils.NewAttributeMap(map[string]string{
61+
AttributeResourceGroupName: "MC_aks-kenafeh_aks-kenafeh-eu_westeurope",
62+
}))
63+
assert.True(t, ok)
64+
assert.Equal(t, cluster, "aks-kenafeh-eu")
65+
66+
cluster, ok = ClusterNameFromAttributes(testutils.NewAttributeMap(map[string]string{
67+
AttributeResourceGroupName: "mc_foo-bar-aks-k8s-rg_foo-bar-aks-k8s_westeurope",
68+
}))
69+
assert.True(t, ok)
70+
assert.Equal(t, cluster, "foo-bar-aks-k8s")
71+
72+
_, ok = ClusterNameFromAttributes(testutils.NewAttributeMap(map[string]string{
73+
AttributeResourceGroupName: "unexpected-resource-group-name-format",
74+
}))
75+
assert.False(t, ok)
76+
77+
_, ok = ClusterNameFromAttributes(testutils.NewAttributeMap(map[string]string{}))
78+
assert.False(t, ok)
79+
}

exporter/datadogexporter/metadata/ec2/ec2.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ import (
2525
)
2626

2727
var (
28-
defaultPrefixes = [3]string{"ip-", "domu", "ec2amaz-"}
29-
ec2TagPrefix = "ec2.tag."
28+
defaultPrefixes = [3]string{"ip-", "domu", "ec2amaz-"}
29+
ec2TagPrefix = "ec2.tag."
30+
clusterTagPrefix = ec2TagPrefix + "kubernetes.io/cluster/"
3031
)
3132

3233
type HostInfo struct {
@@ -123,3 +124,16 @@ func HostInfoFromAttributes(attrs pdata.AttributeMap) (hostInfo *HostInfo) {
123124

124125
return
125126
}
127+
128+
// ClusterNameFromAttributes gets the AWS cluster name from attributes
129+
func ClusterNameFromAttributes(attrs pdata.AttributeMap) (clusterName string, ok bool) {
130+
// Get cluster name from tag keys
131+
// https://github.com/DataDog/datadog-agent/blob/1c94b11/pkg/util/ec2/ec2.go#L238
132+
attrs.ForEach(func(k string, _ pdata.AttributeValue) {
133+
if strings.HasPrefix(k, clusterTagPrefix) {
134+
clusterName = strings.Split(k, "/")[2]
135+
ok = true
136+
}
137+
})
138+
return
139+
}

exporter/datadogexporter/metadata/ec2/ec2_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,14 @@ func TestHostInfoFromAttributes(t *testing.T) {
8181
assert.Equal(t, hostInfo.EC2Hostname, testIP)
8282
assert.ElementsMatch(t, hostInfo.EC2Tags, []string{"tag1:val1", "tag2:val2"})
8383
}
84+
85+
func TestClusterNameFromAttributes(t *testing.T) {
86+
cluster, ok := ClusterNameFromAttributes(testutils.NewAttributeMap(map[string]string{
87+
"ec2.tag.kubernetes.io/cluster/clustername": "dummy_value",
88+
}))
89+
assert.True(t, ok)
90+
assert.Equal(t, cluster, "clustername")
91+
92+
_, ok = ClusterNameFromAttributes(testutils.NewAttributeMap(map[string]string{}))
93+
assert.False(t, ok)
94+
}

exporter/datadogexporter/metadata/host.go

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"go.uber.org/zap"
2121

2222
"github.com/open-telemetry/opentelemetry-collector-contrib/exporter/datadogexporter/config"
23+
"github.com/open-telemetry/opentelemetry-collector-contrib/exporter/datadogexporter/metadata/azure"
2324
"github.com/open-telemetry/opentelemetry-collector-contrib/exporter/datadogexporter/metadata/ec2"
2425
"github.com/open-telemetry/opentelemetry-collector-contrib/exporter/datadogexporter/metadata/gcp"
2526
"github.com/open-telemetry/opentelemetry-collector-contrib/exporter/datadogexporter/metadata/system"
@@ -66,6 +67,21 @@ func GetHost(logger *zap.Logger, cfg *config.Config) *string {
6667
return &hostname
6768
}
6869

70+
func getClusterName(attrs pdata.AttributeMap) (string, bool) {
71+
if k8sClusterName, ok := attrs.Get(conventions.AttributeK8sCluster); ok {
72+
return k8sClusterName.StringVal(), true
73+
}
74+
75+
cloudProvider, ok := attrs.Get(conventions.AttributeCloudProvider)
76+
if ok && cloudProvider.StringVal() == conventions.AttributeCloudProviderAzure {
77+
return azure.ClusterNameFromAttributes(attrs)
78+
} else if ok && cloudProvider.StringVal() == conventions.AttributeCloudProviderAWS {
79+
return ec2.ClusterNameFromAttributes(attrs)
80+
}
81+
82+
return "", false
83+
}
84+
6985
// HostnameFromAttributes tries to get a valid hostname from attributes by checking, in order:
7086
//
7187
// 1. a custom Datadog hostname provided by the "datadog.host.name" attribute
@@ -84,23 +100,19 @@ func HostnameFromAttributes(attrs pdata.AttributeMap) (string, bool) {
84100

85101
// Kubernetes: node-cluster if cluster name is available, else node
86102
if k8sNodeName, ok := attrs.Get(AttributeK8sNodeName); ok {
87-
if k8sClusterName, ok := attrs.Get(conventions.AttributeK8sCluster); ok {
88-
return k8sNodeName.StringVal() + "-" + k8sClusterName.StringVal(), true
103+
if k8sClusterName, ok := getClusterName(attrs); ok {
104+
return k8sNodeName.StringVal() + "-" + k8sClusterName, true
89105
}
90106
return k8sNodeName.StringVal(), true
91107
}
92108

93-
// container id (e.g. from Docker)
94-
if containerID, ok := attrs.Get(conventions.AttributeContainerID); ok {
95-
return containerID.StringVal(), true
96-
}
97-
98-
// handle AWS case separately to have similar behavior to the Datadog Agent
99109
cloudProvider, ok := attrs.Get(conventions.AttributeCloudProvider)
100110
if ok && cloudProvider.StringVal() == conventions.AttributeCloudProviderAWS {
101111
return ec2.HostnameFromAttributes(attrs)
102112
} else if ok && cloudProvider.StringVal() == conventions.AttributeCloudProviderGCP {
103113
return gcp.HostnameFromAttributes(attrs)
114+
} else if ok && cloudProvider.StringVal() == conventions.AttributeCloudProviderAzure {
115+
return azure.HostnameFromAttributes(attrs)
104116
}
105117

106118
// host id from cloud provider
@@ -113,5 +125,10 @@ func HostnameFromAttributes(attrs pdata.AttributeMap) (string, bool) {
113125
return hostName.StringVal(), true
114126
}
115127

128+
// container id (e.g. from Docker)
129+
if containerID, ok := attrs.Get(conventions.AttributeContainerID); ok {
130+
return containerID.StringVal(), true
131+
}
132+
116133
return "", false
117134
}

0 commit comments

Comments
 (0)