Skip to content

Commit f0b691e

Browse files
committed
Add Datadog propagator
1 parent 5913d4a commit f0b691e

2 files changed

Lines changed: 216 additions & 0 deletions

File tree

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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+
from opentelemetry.trace.propagation.tracecontexthttptextformat import (
32+
TraceContextHTTPTextFormat,
33+
)
34+
35+
36+
class DatadogFormat(HTTPTextFormat):
37+
"""Propagator for the Datadog HTTP header format.
38+
"""
39+
40+
TRACE_ID_KEY = "x-datadog-trace-id"
41+
PARENT_ID_KEY = "x-datadog-parent-id"
42+
SAMPLING_PRIORITY_KEY = "x-datadog-sampling-priority"
43+
ORIGIN_KEY = "x-datadog-origin"
44+
45+
def extract(
46+
self,
47+
get_from_carrier: Getter[HTTPTextFormatT],
48+
carrier: HTTPTextFormatT,
49+
context: typing.Optional[Context] = None,
50+
) -> Context:
51+
trace_id = (
52+
extract_first_element(get_from_carrier(carrier, self.TRACE_ID_KEY))
53+
)
54+
55+
span_id = (
56+
extract_first_element(get_from_carrier(carrier, self.PARENT_ID_KEY))
57+
)
58+
59+
# TODO: add sampling
60+
# TODO: add origin
61+
62+
if trace_id is None or span_id is None:
63+
return set_span_in_context(trace.INVALID_SPAN, context)
64+
65+
span_context = trace.SpanContext(
66+
trace_id=int(trace_id), span_id=int(span_id), is_remote=True,
67+
)
68+
69+
return set_span_in_context(trace.DefaultSpan(span_context), context)
70+
71+
def inject(
72+
self,
73+
set_in_carrier: Setter[HTTPTextFormatT],
74+
carrier: HTTPTextFormatT,
75+
context: typing.Optional[Context] = None,
76+
) -> None:
77+
span = get_span_from_context(context=context)
78+
set_in_carrier(
79+
carrier, self.TRACE_ID_KEY, format_trace_id(span.context.trace_id),
80+
)
81+
set_in_carrier(
82+
carrier, self.PARENT_ID_KEY, format_span_id(span.context.span_id)
83+
)
84+
85+
86+
def format_trace_id(trace_id: int) -> str:
87+
"""Format the trace id according to b3 specification."""
88+
return str(trace_id & 0xFFFFFFFFFFFFFFFF)
89+
90+
91+
def format_span_id(span_id: int) -> str:
92+
"""Format the span id according to b3 specification."""
93+
return str(span_id)
94+
95+
96+
def extract_first_element(
97+
items: typing.Iterable[HTTPTextFormatT],
98+
) -> typing.Optional[HTTPTextFormatT]:
99+
if items is None:
100+
return None
101+
return next(iter(items), None)
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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 typing
16+
import unittest
17+
18+
from opentelemetry import trace as trace_api
19+
from opentelemetry.ext.datadog import propagator
20+
from opentelemetry.sdk import trace as 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+
44+
def test_malformed_headers(self):
45+
"""Test with no Datadog headers"""
46+
malformed_trace_id_key = FORMAT.TRACE_ID_KEY + "-x"
47+
malformed_parent_id_key = FORMAT.PARENT_ID_KEY + "-x"
48+
context = get_span_from_context(
49+
FORMAT.extract(
50+
get_as_list,
51+
{
52+
malformed_trace_id_key: self.serialized_trace_id,
53+
malformed_parent_id_key: self.serialized_parent_id,
54+
},
55+
)
56+
).get_context()
57+
58+
self.assertNotEqual(context.trace_id, int(self.serialized_trace_id))
59+
self.assertNotEqual(context.span_id, int(self.serialized_parent_id))
60+
self.assertFalse(context.is_remote)
61+
62+
def test_missing_trace_id(self):
63+
"""If a trace id is missing, populate an invalid trace id."""
64+
carrier = {
65+
FORMAT.PARENT_ID_KEY: self.serialized_parent_id,
66+
}
67+
68+
ctx = FORMAT.extract(get_as_list, carrier)
69+
span_context = get_span_from_context(ctx).get_context()
70+
self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID)
71+
72+
def test_missing_parent_id(self):
73+
"""If a parent id is missing, populate an invalid trace id."""
74+
carrier = {
75+
FORMAT.TRACE_ID_KEY: self.serialized_trace_id,
76+
}
77+
78+
ctx = FORMAT.extract(get_as_list, carrier)
79+
span_context = get_span_from_context(ctx).get_context()
80+
self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID)
81+
82+
def test_context_propagation(self):
83+
"""Test the propagation of Datadog headers."""
84+
parent_context = get_span_from_context(
85+
FORMAT.extract(
86+
get_as_list,
87+
{
88+
FORMAT.TRACE_ID_KEY: self.serialized_trace_id,
89+
FORMAT.PARENT_ID_KEY: self.serialized_parent_id,
90+
},
91+
)
92+
).get_context()
93+
94+
self.assertEqual(parent_context.trace_id, int(self.serialized_trace_id))
95+
self.assertEqual(parent_context.span_id, int(self.serialized_parent_id))
96+
self.assertTrue(parent_context.is_remote)
97+
98+
child = trace.Span(
99+
"child",
100+
trace_api.SpanContext(
101+
parent_context.trace_id,
102+
trace.generate_span_id(),
103+
is_remote=False,
104+
trace_flags=parent_context.trace_flags,
105+
trace_state=parent_context.trace_state,
106+
),
107+
parent=parent_context,
108+
)
109+
110+
child_carrier = {}
111+
child_context = set_span_in_context(child)
112+
FORMAT.inject(dict.__setitem__, child_carrier, context=child_context)
113+
114+
self.assertEqual(child_carrier[FORMAT.TRACE_ID_KEY], self.serialized_trace_id)
115+
self.assertEqual(child_carrier[FORMAT.PARENT_ID_KEY], str(child.context.span_id))

0 commit comments

Comments
 (0)