Skip to content

Commit 6f2e20d

Browse files
authored
[chore]: datadog receiver: Tags translation (#33922)
**Description**: This PR is a follow up to the former #33631 extending the existing tags translation structure. This will be required for the follow up PRs adding support for v1 and v2 series endpoints, service checks, as well as sketches. The full version of the code can be found in the cedwards/datadog-metrics-receiver-full branch, or in Grafana Alloy: https://github.com/grafana/alloy/tree/main/internal/etc/datadogreceiver **Link to tracking Issue:** #18278 **Testing**: Unit tests have been added. More thorough tests will be included in follow-up PRs as the remaining functionality is added. **Notes**: - Adding `[chore]` to the title of the PR because https://github.com/grafana/opentelemetry-collector-contrib/blob/ab4d726aaaa07aad702ff3b312a8e261f2b38021/.chloggen/datadogreceiver_metrics.yaml#L1-L27 already exists. --------- Signed-off-by: Jesus Vazquez <[email protected]>
1 parent 344e4f2 commit 6f2e20d

File tree

3 files changed

+300
-28
lines changed

3 files changed

+300
-28
lines changed

receiver/datadogreceiver/tags.go

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package datadogreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/datadogreceiver"
5+
6+
import (
7+
"fmt"
8+
"strings"
9+
"sync"
10+
11+
"go.opentelemetry.io/collector/pdata/pcommon"
12+
semconv "go.opentelemetry.io/collector/semconv/v1.16.0"
13+
)
14+
15+
// See:
16+
// https://docs.datadoghq.com/opentelemetry/schema_semantics/semantic_mapping/
17+
// https://github.com/DataDog/opentelemetry-mapping-go/blob/main/pkg/otlp/attributes/attributes.go
18+
var datadogKnownResourceAttributes = map[string]string{
19+
"env": semconv.AttributeDeploymentEnvironment,
20+
"service": semconv.AttributeServiceName,
21+
"version": semconv.AttributeServiceVersion,
22+
23+
// Container-related attributes
24+
"container_id": semconv.AttributeContainerID,
25+
"container_name": semconv.AttributeContainerName,
26+
"image_name": semconv.AttributeContainerImageName,
27+
"image_tag": semconv.AttributeContainerImageTag,
28+
"runtime": semconv.AttributeContainerRuntime,
29+
30+
// Cloud-related attributes
31+
"cloud_provider": semconv.AttributeCloudProvider,
32+
"region": semconv.AttributeCloudRegion,
33+
"zone": semconv.AttributeCloudAvailabilityZone,
34+
35+
// ECS-related attributes
36+
"task_family": semconv.AttributeAWSECSTaskFamily,
37+
"task_arn": semconv.AttributeAWSECSTaskARN,
38+
"ecs_cluster_name": semconv.AttributeAWSECSClusterARN,
39+
"task_version": semconv.AttributeAWSECSTaskRevision,
40+
"ecs_container_name": semconv.AttributeAWSECSContainerARN,
41+
42+
// K8-related attributes
43+
"kube_container_name": semconv.AttributeK8SContainerName,
44+
"kube_cluster_name": semconv.AttributeK8SClusterName,
45+
"kube_deployment": semconv.AttributeK8SDeploymentName,
46+
"kube_replica_set": semconv.AttributeK8SReplicaSetName,
47+
"kube_stateful_set": semconv.AttributeK8SStatefulSetName,
48+
"kube_daemon_set": semconv.AttributeK8SDaemonSetName,
49+
"kube_job": semconv.AttributeK8SJobName,
50+
"kube_cronjob": semconv.AttributeK8SCronJobName,
51+
"kube_namespace": semconv.AttributeK8SNamespaceName,
52+
"pod_name": semconv.AttributeK8SPodName,
53+
54+
// Other
55+
"process_id": semconv.AttributeProcessPID,
56+
"error.stacktrace": semconv.AttributeExceptionStacktrace,
57+
"error.msg": semconv.AttributeExceptionMessage,
58+
}
59+
60+
// translateDatadogTagToKeyValuePair translates a Datadog tag to a key value pair
61+
func translateDatadogTagToKeyValuePair(tag string) (key string, value string) {
62+
if tag == "" {
63+
return "", ""
64+
}
65+
66+
key, val, ok := strings.Cut(tag, ":")
67+
if !ok {
68+
// Datadog allows for two tag formats, one of which includes a key such as 'env',
69+
// followed by a value. Datadog also supports inputTags without the key, but OTel seems
70+
// to only support key:value pairs.
71+
// The following is a workaround to map unnamed inputTags to key:value pairs and its subject to future
72+
// changes if OTel supports unnamed inputTags in the future or if there is a better way to do this.
73+
key = fmt.Sprintf("unnamed_%s", tag)
74+
val = tag
75+
}
76+
return key, val
77+
}
78+
79+
// translateDatadogKeyToOTel translates a Datadog key to an OTel key
80+
func translateDatadogKeyToOTel(k string) string {
81+
if otelKey, ok := datadogKnownResourceAttributes[strings.ToLower(k)]; ok {
82+
return otelKey
83+
}
84+
return k
85+
}
86+
87+
type StringPool struct {
88+
sync.RWMutex
89+
pool map[string]string
90+
}
91+
92+
func newStringPool() *StringPool {
93+
return &StringPool{
94+
pool: make(map[string]string),
95+
}
96+
}
97+
98+
func (s *StringPool) Intern(str string) string {
99+
s.RLock()
100+
interned, ok := s.pool[str]
101+
s.RUnlock()
102+
103+
if ok {
104+
return interned
105+
}
106+
107+
s.Lock()
108+
// Double check if another goroutine has added the string after releasing the read lock
109+
interned, ok = s.pool[str]
110+
if !ok {
111+
interned = str
112+
s.pool[str] = str
113+
}
114+
s.Unlock()
115+
116+
return interned
117+
}
118+
119+
func tagsToAttributes(tags []string, host string, stringPool *StringPool) (pcommon.Map, pcommon.Map, pcommon.Map) {
120+
resourceAttrs := pcommon.NewMap()
121+
scopeAttrs := pcommon.NewMap()
122+
dpAttrs := pcommon.NewMap()
123+
124+
if host != "" {
125+
resourceAttrs.PutStr(semconv.AttributeHostName, host)
126+
}
127+
128+
var key, val string
129+
for _, tag := range tags {
130+
key, val = translateDatadogTagToKeyValuePair(tag)
131+
if attr, ok := datadogKnownResourceAttributes[key]; ok {
132+
val = stringPool.Intern(val) // No need to intern the key if we already have it
133+
resourceAttrs.PutStr(attr, val)
134+
} else {
135+
key = stringPool.Intern(translateDatadogKeyToOTel(key))
136+
val = stringPool.Intern(val)
137+
dpAttrs.PutStr(key, val)
138+
}
139+
}
140+
141+
return resourceAttrs, scopeAttrs, dpAttrs
142+
}

receiver/datadogreceiver/tags_test.go

+156
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package datadogreceiver
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"go.opentelemetry.io/collector/pdata/pcommon"
11+
)
12+
13+
func TestGetMetricAttributes(t *testing.T) {
14+
cases := []struct {
15+
name string
16+
tags []string
17+
host string
18+
expectedResourceAttrs pcommon.Map
19+
expectedScopeAttrs pcommon.Map
20+
expectedDpAttrs pcommon.Map
21+
}{
22+
{
23+
name: "empty",
24+
tags: []string{},
25+
host: "",
26+
expectedResourceAttrs: pcommon.NewMap(),
27+
expectedScopeAttrs: pcommon.NewMap(),
28+
expectedDpAttrs: pcommon.NewMap(),
29+
},
30+
{
31+
name: "host",
32+
tags: []string{},
33+
host: "host",
34+
expectedResourceAttrs: newMapFromKV(t, map[string]any{
35+
"host.name": "host",
36+
}),
37+
expectedScopeAttrs: pcommon.NewMap(),
38+
expectedDpAttrs: pcommon.NewMap(),
39+
},
40+
{
41+
name: "provides both host and tags where some tag keys have to replaced by otel conventions",
42+
tags: []string{"env:prod", "service:my-service", "version:1.0"},
43+
host: "host",
44+
expectedResourceAttrs: newMapFromKV(t, map[string]any{
45+
"host.name": "host",
46+
"deployment.environment": "prod",
47+
"service.name": "my-service",
48+
"service.version": "1.0",
49+
}),
50+
expectedScopeAttrs: pcommon.NewMap(),
51+
expectedDpAttrs: pcommon.NewMap(),
52+
},
53+
{
54+
name: "provides host, tags and unnamed tags",
55+
tags: []string{"env:prod", "foo"},
56+
host: "host",
57+
expectedResourceAttrs: newMapFromKV(t, map[string]any{
58+
"host.name": "host",
59+
"deployment.environment": "prod",
60+
}),
61+
expectedScopeAttrs: pcommon.NewMap(),
62+
expectedDpAttrs: newMapFromKV(t, map[string]any{
63+
"unnamed_foo": "foo",
64+
}),
65+
},
66+
}
67+
68+
for _, c := range cases {
69+
t.Run(c.name, func(t *testing.T) {
70+
pool := newStringPool()
71+
resourceAttrs, scopeAttrs, dpAttrs := tagsToAttributes(c.tags, c.host, pool)
72+
73+
assert.Equal(t, c.expectedResourceAttrs.Len(), resourceAttrs.Len())
74+
c.expectedResourceAttrs.Range(func(k string, _ pcommon.Value) bool {
75+
ev, _ := c.expectedResourceAttrs.Get(k)
76+
av, ok := resourceAttrs.Get(k)
77+
assert.True(t, ok)
78+
assert.Equal(t, ev, av)
79+
return true
80+
})
81+
82+
assert.Equal(t, c.expectedScopeAttrs.Len(), scopeAttrs.Len())
83+
c.expectedScopeAttrs.Range(func(k string, _ pcommon.Value) bool {
84+
ev, _ := c.expectedScopeAttrs.Get(k)
85+
av, ok := scopeAttrs.Get(k)
86+
assert.True(t, ok)
87+
assert.Equal(t, ev, av)
88+
return true
89+
})
90+
91+
assert.Equal(t, c.expectedDpAttrs.Len(), dpAttrs.Len())
92+
c.expectedDpAttrs.Range(func(k string, _ pcommon.Value) bool {
93+
ev, _ := c.expectedDpAttrs.Get(k)
94+
av, ok := dpAttrs.Get(k)
95+
assert.True(t, ok)
96+
assert.Equal(t, ev, av)
97+
return true
98+
})
99+
})
100+
101+
}
102+
103+
}
104+
105+
func newMapFromKV(t *testing.T, kv map[string]any) pcommon.Map {
106+
m := pcommon.NewMap()
107+
err := m.FromRaw(kv)
108+
assert.NoError(t, err)
109+
return m
110+
}
111+
112+
func TestDatadogTagToKeyValuePair(t *testing.T) {
113+
cases := []struct {
114+
name string
115+
input string
116+
expectedKey string
117+
expectedValue string
118+
}{
119+
{
120+
name: "empty",
121+
input: "",
122+
expectedKey: "",
123+
expectedValue: "",
124+
},
125+
{
126+
name: "kv tag",
127+
input: "foo:bar",
128+
expectedKey: "foo",
129+
expectedValue: "bar",
130+
},
131+
{
132+
name: "unnamed tag",
133+
input: "foo",
134+
expectedKey: "unnamed_foo",
135+
expectedValue: "foo",
136+
},
137+
}
138+
139+
for _, c := range cases {
140+
t.Run(c.name, func(t *testing.T) {
141+
key, value := translateDatadogTagToKeyValuePair(c.input)
142+
assert.Equal(t, c.expectedKey, key, "Expected key %s, got %s", c.expectedKey, key)
143+
assert.Equal(t, c.expectedValue, value, "Expected value %s, got %s", c.expectedValue, value)
144+
})
145+
}
146+
147+
}
148+
149+
func TestTranslateDataDogKeyToOtel(t *testing.T) {
150+
// make sure all known keys are translated
151+
for k, v := range datadogKnownResourceAttributes {
152+
t.Run(k, func(t *testing.T) {
153+
assert.Equal(t, v, translateDatadogKeyToOTel(k))
154+
})
155+
}
156+
}

receiver/datadogreceiver/traces_translator.go

+2-28
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func toTraces(payload *pb.TracerPayload, req *http.Request) ptrace.Traces {
7373
}
7474

7575
for k, v := range payload.Tags {
76-
if k = translateDataDogKeyToOtel(k); v != "" {
76+
if k = translateDatadogKeyToOTel(k); v != "" {
7777
sharedAttributes.PutStr(k, v)
7878
}
7979
}
@@ -110,7 +110,7 @@ func toTraces(payload *pb.TracerPayload, req *http.Request) ptrace.Traces {
110110
newSpan.Attributes().PutStr(attributeDatadogSpanID, strconv.FormatUint(span.SpanID, 10))
111111
newSpan.Attributes().PutStr(attributeDatadogTraceID, strconv.FormatUint(span.TraceID, 10))
112112
for k, v := range span.GetMeta() {
113-
if k = translateDataDogKeyToOtel(k); len(k) > 0 {
113+
if k = translateDatadogKeyToOTel(k); len(k) > 0 {
114114
newSpan.Attributes().PutStr(k, v)
115115
}
116116
}
@@ -155,32 +155,6 @@ func toTraces(payload *pb.TracerPayload, req *http.Request) ptrace.Traces {
155155
return results
156156
}
157157

158-
func translateDataDogKeyToOtel(k string) string {
159-
switch strings.ToLower(k) {
160-
case "env":
161-
return semconv.AttributeDeploymentEnvironment
162-
case "version":
163-
return semconv.AttributeServiceVersion
164-
case "container_id":
165-
return semconv.AttributeContainerID
166-
case "container_name":
167-
return semconv.AttributeContainerName
168-
case "image_name":
169-
return semconv.AttributeContainerImageName
170-
case "image_tag":
171-
return semconv.AttributeContainerImageTag
172-
case "process_id":
173-
return semconv.AttributeProcessPID
174-
case "error.stacktrace":
175-
return semconv.AttributeExceptionStacktrace
176-
case "error.msg":
177-
return semconv.AttributeExceptionMessage
178-
default:
179-
return k
180-
}
181-
182-
}
183-
184158
var bufferPool = sync.Pool{
185159
New: func() any {
186160
return new(bytes.Buffer)

0 commit comments

Comments
 (0)