Skip to content

Commit 7e9bb8c

Browse files
committed
Add Datadog propagator
1 parent 7eccae2 commit 7e9bb8c

5 files changed

Lines changed: 293 additions & 0 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DD_ORIGIN = "_dd_origin"

ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
2525
from opentelemetry.trace.status import StatusCanonicalCode
2626

27+
# pylint:disable=relative-beyond-top-level
28+
from .constants import DD_ORIGIN
29+
2730
logger = logging.getLogger(__name__)
2831

2932

@@ -128,6 +131,11 @@ def _translate_to_datadog(self, spans):
128131

129132
datadog_span.set_tags(span.attributes)
130133

134+
# add origin to root span
135+
origin = _get_origin(span)
136+
if origin and parent_id == 0:
137+
datadog_span.set_tag(DD_ORIGIN, origin)
138+
131139
# span events and span links are not supported
132140

133141
datadog_spans.append(datadog_span)
@@ -202,3 +210,9 @@ def _get_exc_info(span):
202210
"""Parse span status description for exception type and value"""
203211
exc_type, exc_val = span.status.description.split(":", 1)
204212
return exc_type, exc_val.strip()
213+
214+
215+
def _get_origin(span):
216+
ctx = span.get_context()
217+
origin = ctx.trace_state.get(DD_ORIGIN)
218+
return origin
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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+
15+
import logging
16+
import os
17+
import typing
18+
19+
from opentelemetry import propagators, trace
20+
from opentelemetry.context import Context
21+
from opentelemetry.trace.propagation import (
22+
get_span_from_context,
23+
set_span_in_context,
24+
)
25+
from opentelemetry.trace.propagation.httptextformat import (
26+
Getter,
27+
HTTPTextFormat,
28+
HTTPTextFormatT,
29+
Setter,
30+
)
31+
32+
from .constants import DD_ORIGIN
33+
34+
35+
class DatadogFormat(HTTPTextFormat):
36+
"""Propagator for the Datadog HTTP header format.
37+
"""
38+
39+
TRACE_ID_KEY = "x-datadog-trace-id"
40+
PARENT_ID_KEY = "x-datadog-parent-id"
41+
SAMPLING_PRIORITY_KEY = "x-datadog-sampling-priority"
42+
ORIGIN_KEY = "x-datadog-origin"
43+
44+
def extract(
45+
self,
46+
get_from_carrier: Getter[HTTPTextFormatT],
47+
carrier: HTTPTextFormatT,
48+
context: typing.Optional[Context] = None,
49+
) -> Context:
50+
trace_id = extract_first_element(
51+
get_from_carrier(carrier, self.TRACE_ID_KEY)
52+
)
53+
54+
span_id = extract_first_element(
55+
get_from_carrier(carrier, self.PARENT_ID_KEY)
56+
)
57+
58+
origin = extract_first_element(
59+
get_from_carrier(carrier, self.ORIGIN_KEY)
60+
)
61+
62+
# TODO: add sampling
63+
64+
if trace_id is None or span_id is None:
65+
return set_span_in_context(trace.INVALID_SPAN, context)
66+
67+
span_context = trace.SpanContext(
68+
trace_id=int(trace_id),
69+
span_id=int(span_id),
70+
is_remote=True,
71+
trace_state=trace.TraceState({DD_ORIGIN: origin}),
72+
)
73+
74+
return set_span_in_context(trace.DefaultSpan(span_context), context)
75+
76+
def inject(
77+
self,
78+
set_in_carrier: Setter[HTTPTextFormatT],
79+
carrier: HTTPTextFormatT,
80+
context: typing.Optional[Context] = None,
81+
) -> None:
82+
span = get_span_from_context(context=context)
83+
set_in_carrier(
84+
carrier, self.TRACE_ID_KEY, format_trace_id(span.context.trace_id),
85+
)
86+
set_in_carrier(
87+
carrier, self.PARENT_ID_KEY, format_span_id(span.context.span_id)
88+
)
89+
if DD_ORIGIN in span.context.trace_state:
90+
set_in_carrier(
91+
carrier, self.ORIGIN_KEY, span.context.trace_state[DD_ORIGIN]
92+
)
93+
94+
95+
def format_trace_id(trace_id: int) -> str:
96+
"""Format the trace id according to b3 specification."""
97+
return str(trace_id & 0xFFFFFFFFFFFFFFFF)
98+
99+
100+
def format_span_id(span_id: int) -> str:
101+
"""Format the span id according to b3 specification."""
102+
return str(span_id)
103+
104+
105+
def extract_first_element(
106+
items: typing.Iterable[HTTPTextFormatT],
107+
) -> typing.Optional[HTTPTextFormatT]:
108+
if items is None:
109+
return None
110+
return next(iter(items), None)

ext/opentelemetry-ext-datadog/tests/test_datadog_exporter.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,3 +403,40 @@ def test_span_processor_scheduled_delay(self):
403403
self.assertEqual(len(datadog_spans), 1)
404404

405405
tracer_provider.shutdown()
406+
407+
def test_origin(self):
408+
context = trace_api.SpanContext(
409+
trace_id=0x000000000000000000000000DEADBEEF,
410+
span_id=trace_api.INVALID_SPAN,
411+
is_remote=True,
412+
trace_state=trace_api.TraceState(
413+
{datadog.constants.DD_ORIGIN: "origin-service"}
414+
),
415+
)
416+
417+
root_span = trace.Span(name="root", context=context, parent=None)
418+
child_span = trace.Span(
419+
name="child", context=context, parent=root_span
420+
)
421+
root_span.start()
422+
child_span.start()
423+
child_span.end()
424+
root_span.end()
425+
426+
# pylint: disable=protected-access
427+
exporter = datadog.DatadogSpanExporter()
428+
datadog_spans = [
429+
span.to_dict()
430+
for span in exporter._translate_to_datadog([root_span, child_span])
431+
]
432+
433+
self.assertEqual(len(datadog_spans), 2)
434+
435+
actual = [
436+
span["meta"].get(datadog.constants.DD_ORIGIN)
437+
if "meta" in span
438+
else None
439+
for span in datadog_spans
440+
]
441+
expected = ["origin-service", None]
442+
self.assertListEqual(actual, expected)
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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+
15+
import unittest
16+
17+
from opentelemetry import trace as trace_api
18+
from opentelemetry.ext.datadog import propagator
19+
from opentelemetry.ext.datadog.constants import DD_ORIGIN
20+
from opentelemetry.sdk import trace
21+
from opentelemetry.trace.propagation import (
22+
get_span_from_context,
23+
set_span_in_context,
24+
)
25+
26+
FORMAT = propagator.DatadogFormat()
27+
28+
29+
def get_as_list(dict_object, key):
30+
value = dict_object.get(key)
31+
return [value] if value is not None else []
32+
33+
34+
class TestDatadogFormat(unittest.TestCase):
35+
@classmethod
36+
def setUpClass(cls):
37+
cls.serialized_trace_id = propagator.format_trace_id(
38+
trace.generate_trace_id()
39+
)
40+
cls.serialized_parent_id = propagator.format_span_id(
41+
trace.generate_span_id()
42+
)
43+
cls.serialized_origin = "origin-service"
44+
45+
def test_malformed_headers(self):
46+
"""Test with no Datadog headers"""
47+
malformed_trace_id_key = FORMAT.TRACE_ID_KEY + "-x"
48+
malformed_parent_id_key = FORMAT.PARENT_ID_KEY + "-x"
49+
context = get_span_from_context(
50+
FORMAT.extract(
51+
get_as_list,
52+
{
53+
malformed_trace_id_key: self.serialized_trace_id,
54+
malformed_parent_id_key: self.serialized_parent_id,
55+
},
56+
)
57+
).get_context()
58+
59+
self.assertNotEqual(context.trace_id, int(self.serialized_trace_id))
60+
self.assertNotEqual(context.span_id, int(self.serialized_parent_id))
61+
self.assertFalse(context.is_remote)
62+
63+
def test_missing_trace_id(self):
64+
"""If a trace id is missing, populate an invalid trace id."""
65+
carrier = {
66+
FORMAT.PARENT_ID_KEY: self.serialized_parent_id,
67+
}
68+
69+
ctx = FORMAT.extract(get_as_list, carrier)
70+
span_context = get_span_from_context(ctx).get_context()
71+
self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID)
72+
73+
def test_missing_parent_id(self):
74+
"""If a parent id is missing, populate an invalid trace id."""
75+
carrier = {
76+
FORMAT.TRACE_ID_KEY: self.serialized_trace_id,
77+
}
78+
79+
ctx = FORMAT.extract(get_as_list, carrier)
80+
span_context = get_span_from_context(ctx).get_context()
81+
self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID)
82+
83+
def test_context_propagation(self):
84+
"""Test the propagation of Datadog headers."""
85+
parent_context = get_span_from_context(
86+
FORMAT.extract(
87+
get_as_list,
88+
{
89+
FORMAT.TRACE_ID_KEY: self.serialized_trace_id,
90+
FORMAT.PARENT_ID_KEY: self.serialized_parent_id,
91+
FORMAT.ORIGIN_KEY: self.serialized_origin,
92+
},
93+
)
94+
).get_context()
95+
96+
self.assertEqual(
97+
parent_context.trace_id, int(self.serialized_trace_id)
98+
)
99+
self.assertEqual(
100+
parent_context.span_id, int(self.serialized_parent_id)
101+
)
102+
self.assertEqual(
103+
parent_context.trace_state.get(DD_ORIGIN), self.serialized_origin
104+
)
105+
self.assertTrue(parent_context.is_remote)
106+
107+
child = trace.Span(
108+
"child",
109+
trace_api.SpanContext(
110+
parent_context.trace_id,
111+
trace.generate_span_id(),
112+
is_remote=False,
113+
trace_flags=parent_context.trace_flags,
114+
trace_state=parent_context.trace_state,
115+
),
116+
parent=parent_context,
117+
)
118+
119+
child_carrier = {}
120+
child_context = set_span_in_context(child)
121+
FORMAT.inject(dict.__setitem__, child_carrier, context=child_context)
122+
123+
self.assertEqual(
124+
child_carrier[FORMAT.TRACE_ID_KEY], self.serialized_trace_id
125+
)
126+
self.assertEqual(
127+
child_carrier[FORMAT.PARENT_ID_KEY], str(child.context.span_id)
128+
)
129+
self.assertEqual(
130+
child_carrier.get(FORMAT.ORIGIN_KEY), self.serialized_origin
131+
)

0 commit comments

Comments
 (0)