Skip to content

Commit 5eb0533

Browse files
authored
adding support for Jaeger propagator (#1219)
1 parent 553bdfc commit 5eb0533

File tree

4 files changed

+351
-2
lines changed

4 files changed

+351
-2
lines changed

opentelemetry-sdk/CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
([#1440](https://github.com/open-telemetry/opentelemetry-python/pull/1440))
99
- Add `fields` to propagators
1010
([#1374](https://github.com/open-telemetry/opentelemetry-python/pull/1374))
11+
- Added support for Jaeger propagator
12+
([#1219](https://github.com/open-telemetry/opentelemetry-python/pull/1219))
1113
- Add support for OTEL_SPAN_{ATTRIBUTE_COUNT_LIMIT,EVENT_COUNT_LIMIT,LINK_COUNT_LIMIT}
1214
([#1377](https://github.com/open-telemetry/opentelemetry-python/pull/1377))
1315

@@ -27,8 +29,8 @@ Released 2020-11-25
2729
([#1373](https://github.com/open-telemetry/opentelemetry-python/pull/1373))
2830
- Rename Meter class to Accumulator in Metrics SDK
2931
([#1372](https://github.com/open-telemetry/opentelemetry-python/pull/1372))
30-
- Fix `ParentBased` sampler for implicit parent spans. Fix also `trace_state`
31-
erasure for dropped spans or spans sampled by the `TraceIdRatioBased` sampler.
32+
- Fix `ParentBased` sampler for implicit parent spans. Fix also `trace_state`
33+
erasure for dropped spans or spans sampled by the `TraceIdRatioBased` sampler.
3234
([#1394](https://github.com/open-telemetry/opentelemetry-python/pull/1394))
3335

3436
## Version 0.15b0
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
16+
import typing
17+
18+
from opentelemetry.trace.propagation.textmap import TextMapPropagatorT
19+
20+
21+
def extract_first_element(
22+
items: typing.Iterable[TextMapPropagatorT],
23+
) -> typing.Optional[TextMapPropagatorT]:
24+
if items is None:
25+
return None
26+
return next(iter(items), None)
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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 urllib.parse
17+
18+
import opentelemetry.trace as trace
19+
from opentelemetry import baggage
20+
from opentelemetry.context import Context, get_current
21+
from opentelemetry.sdk.trace.propagation import extract_first_element
22+
from opentelemetry.trace.propagation.textmap import (
23+
Getter,
24+
Setter,
25+
TextMapPropagator,
26+
TextMapPropagatorT,
27+
)
28+
29+
30+
class JaegerPropagator(TextMapPropagator):
31+
"""Propagator for the Jaeger format.
32+
33+
See: https://www.jaegertracing.io/docs/1.19/client-libraries/#propagation-format
34+
"""
35+
36+
TRACE_ID_KEY = "uber-trace-id"
37+
BAGGAGE_PREFIX = "uberctx-"
38+
DEBUG_FLAG = 0x02
39+
40+
def extract(
41+
self,
42+
getter: Getter[TextMapPropagatorT],
43+
carrier: TextMapPropagatorT,
44+
context: typing.Optional[Context] = None,
45+
) -> Context:
46+
47+
if context is None:
48+
context = get_current()
49+
fields = extract_first_element(
50+
getter.get(carrier, self.TRACE_ID_KEY)
51+
).split(":")
52+
53+
context = self._extract_baggage(getter, carrier, context)
54+
if len(fields) != 4:
55+
return trace.set_span_in_context(trace.INVALID_SPAN, context)
56+
57+
trace_id, span_id, _parent_id, flags = fields
58+
if (
59+
trace_id == trace.INVALID_TRACE_ID
60+
or span_id == trace.INVALID_SPAN_ID
61+
):
62+
return trace.set_span_in_context(trace.INVALID_SPAN, context)
63+
64+
span = trace.DefaultSpan(
65+
trace.SpanContext(
66+
trace_id=int(trace_id, 16),
67+
span_id=int(span_id, 16),
68+
is_remote=True,
69+
trace_flags=trace.TraceFlags(
70+
int(flags, 16) & trace.TraceFlags.SAMPLED
71+
),
72+
)
73+
)
74+
return trace.set_span_in_context(span, context)
75+
76+
def inject(
77+
self,
78+
set_in_carrier: Setter[TextMapPropagatorT],
79+
carrier: TextMapPropagatorT,
80+
context: typing.Optional[Context] = None,
81+
) -> None:
82+
span = trace.get_current_span(context=context)
83+
span_context = span.get_span_context()
84+
if span_context == trace.INVALID_SPAN_CONTEXT:
85+
return
86+
87+
span_parent_id = span.parent.span_id if span.parent else 0
88+
trace_flags = span_context.trace_flags
89+
if trace_flags.sampled:
90+
trace_flags |= self.DEBUG_FLAG
91+
92+
# set span identity
93+
set_in_carrier(
94+
carrier,
95+
self.TRACE_ID_KEY,
96+
_format_uber_trace_id(
97+
span_context.trace_id,
98+
span_context.span_id,
99+
span_parent_id,
100+
trace_flags,
101+
),
102+
)
103+
104+
# set span baggage, if any
105+
baggage_entries = baggage.get_all(context=context)
106+
if not baggage_entries:
107+
return
108+
for key, value in baggage_entries.items():
109+
baggage_key = self.BAGGAGE_PREFIX + key
110+
set_in_carrier(
111+
carrier, baggage_key, urllib.parse.quote(str(value))
112+
)
113+
114+
@property
115+
def fields(self) -> typing.Set[str]:
116+
return {self.TRACE_ID_KEY}
117+
118+
def _extract_baggage(self, getter, carrier, context):
119+
baggage_keys = [
120+
key
121+
for key in getter.keys(carrier)
122+
if key.startswith(self.BAGGAGE_PREFIX)
123+
]
124+
for key in baggage_keys:
125+
value = extract_first_element(getter.get(carrier, key))
126+
context = baggage.set_baggage(
127+
key.replace(self.BAGGAGE_PREFIX, ""),
128+
urllib.parse.unquote(value).strip(),
129+
context=context,
130+
)
131+
return context
132+
133+
134+
def _format_uber_trace_id(trace_id, span_id, parent_span_id, flags):
135+
return "{:032x}:{:016x}:{:016x}:{:02x}".format(
136+
trace_id, span_id, parent_span_id, flags
137+
)
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
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+
from unittest.mock import Mock
17+
18+
import opentelemetry.sdk.trace as trace
19+
import opentelemetry.sdk.trace.propagation.jaeger_propagator as jaeger
20+
import opentelemetry.trace as trace_api
21+
from opentelemetry import baggage
22+
from opentelemetry.trace.propagation.textmap import DictGetter
23+
24+
FORMAT = jaeger.JaegerPropagator()
25+
26+
27+
carrier_getter = DictGetter()
28+
29+
30+
def get_context_new_carrier(old_carrier, carrier_baggage=None):
31+
32+
ctx = FORMAT.extract(carrier_getter, old_carrier)
33+
if carrier_baggage:
34+
for key, value in carrier_baggage.items():
35+
ctx = baggage.set_baggage(key, value, ctx)
36+
parent_span_context = trace_api.get_current_span(ctx).get_span_context()
37+
38+
parent = trace._Span("parent", parent_span_context)
39+
child = trace._Span(
40+
"child",
41+
trace_api.SpanContext(
42+
parent_span_context.trace_id,
43+
trace_api.RandomIdsGenerator().generate_span_id(),
44+
is_remote=False,
45+
trace_flags=parent_span_context.trace_flags,
46+
trace_state=parent_span_context.trace_state,
47+
),
48+
parent=parent.get_span_context(),
49+
)
50+
51+
new_carrier = {}
52+
ctx = trace_api.set_span_in_context(child, ctx)
53+
54+
FORMAT.inject(dict.__setitem__, new_carrier, context=ctx)
55+
56+
return ctx, new_carrier
57+
58+
59+
def _format_uber_trace_id(trace_id, span_id, parent_span_id, flags):
60+
return "{:032x}:{:016x}:{:016x}:{:02x}".format(
61+
trace_id, span_id, parent_span_id, flags
62+
)
63+
64+
65+
class TestJaegerPropagator(unittest.TestCase):
66+
@classmethod
67+
def setUpClass(cls):
68+
ids_generator = trace_api.RandomIdsGenerator()
69+
cls.trace_id = ids_generator.generate_trace_id()
70+
cls.span_id = ids_generator.generate_span_id()
71+
cls.parent_span_id = ids_generator.generate_span_id()
72+
cls.serialized_uber_trace_id = _format_uber_trace_id(
73+
cls.trace_id, cls.span_id, cls.parent_span_id, 11
74+
)
75+
76+
def test_extract_valid_span(self):
77+
old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id}
78+
ctx = FORMAT.extract(carrier_getter, old_carrier)
79+
span_context = trace_api.get_current_span(ctx).get_span_context()
80+
self.assertEqual(span_context.trace_id, self.trace_id)
81+
self.assertEqual(span_context.span_id, self.span_id)
82+
83+
def test_trace_id(self):
84+
old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id}
85+
_, new_carrier = get_context_new_carrier(old_carrier)
86+
self.assertEqual(
87+
self.serialized_uber_trace_id.split(":")[0],
88+
new_carrier[FORMAT.TRACE_ID_KEY].split(":")[0],
89+
)
90+
91+
def test_parent_span_id(self):
92+
old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id}
93+
_, new_carrier = get_context_new_carrier(old_carrier)
94+
span_id = self.serialized_uber_trace_id.split(":")[1]
95+
parent_span_id = new_carrier[FORMAT.TRACE_ID_KEY].split(":")[2]
96+
self.assertEqual(span_id, parent_span_id)
97+
98+
def test_sampled_flag_set(self):
99+
old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id}
100+
_, new_carrier = get_context_new_carrier(old_carrier)
101+
sample_flag_value = (
102+
int(new_carrier[FORMAT.TRACE_ID_KEY].split(":")[3]) & 0x01
103+
)
104+
self.assertEqual(1, sample_flag_value)
105+
106+
def test_debug_flag_set(self):
107+
old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id}
108+
_, new_carrier = get_context_new_carrier(old_carrier)
109+
debug_flag_value = (
110+
int(new_carrier[FORMAT.TRACE_ID_KEY].split(":")[3])
111+
& FORMAT.DEBUG_FLAG
112+
)
113+
self.assertEqual(FORMAT.DEBUG_FLAG, debug_flag_value)
114+
115+
def test_sample_debug_flags_unset(self):
116+
uber_trace_id = _format_uber_trace_id(
117+
self.trace_id, self.span_id, self.parent_span_id, 0
118+
)
119+
old_carrier = {FORMAT.TRACE_ID_KEY: uber_trace_id}
120+
_, new_carrier = get_context_new_carrier(old_carrier)
121+
flags = int(new_carrier[FORMAT.TRACE_ID_KEY].split(":")[3])
122+
sample_flag_value = flags & 0x01
123+
debug_flag_value = flags & FORMAT.DEBUG_FLAG
124+
self.assertEqual(0, sample_flag_value)
125+
self.assertEqual(0, debug_flag_value)
126+
127+
def test_baggage(self):
128+
old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id}
129+
input_baggage = {"key1": "value1"}
130+
_, new_carrier = get_context_new_carrier(old_carrier, input_baggage)
131+
ctx = FORMAT.extract(carrier_getter, new_carrier)
132+
self.assertDictEqual(input_baggage, ctx["baggage"])
133+
134+
def test_non_string_baggage(self):
135+
old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id}
136+
input_baggage = {"key1": 1, "key2": True}
137+
formatted_baggage = {"key1": "1", "key2": "True"}
138+
_, new_carrier = get_context_new_carrier(old_carrier, input_baggage)
139+
ctx = FORMAT.extract(carrier_getter, new_carrier)
140+
self.assertDictEqual(formatted_baggage, ctx["baggage"])
141+
142+
def test_extract_invalid_uber_trace_id(self):
143+
old_carrier = {
144+
"uber-trace-id": "000000000000000000000000deadbeef:00000000deadbef0:00",
145+
"uberctx-key1": "value1",
146+
}
147+
formatted_baggage = {"key1": "value1"}
148+
context = FORMAT.extract(carrier_getter, old_carrier)
149+
span_context = trace_api.get_current_span(context).get_span_context()
150+
self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID)
151+
self.assertDictEqual(formatted_baggage, context["baggage"])
152+
153+
def test_extract_invalid_trace_id(self):
154+
old_carrier = {
155+
"uber-trace-id": "00000000000000000000000000000000:00000000deadbef0:00:00",
156+
"uberctx-key1": "value1",
157+
}
158+
formatted_baggage = {"key1": "value1"}
159+
context = FORMAT.extract(carrier_getter, old_carrier)
160+
span_context = trace_api.get_current_span(context).get_span_context()
161+
self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID)
162+
self.assertDictEqual(formatted_baggage, context["baggage"])
163+
164+
def test_extract_invalid_span_id(self):
165+
old_carrier = {
166+
"uber-trace-id": "000000000000000000000000deadbeef:0000000000000000:00:00",
167+
"uberctx-key1": "value1",
168+
}
169+
formatted_baggage = {"key1": "value1"}
170+
context = FORMAT.extract(carrier_getter, old_carrier)
171+
span_context = trace_api.get_current_span(context).get_span_context()
172+
self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID)
173+
self.assertDictEqual(formatted_baggage, context["baggage"])
174+
175+
def test_fields(self):
176+
tracer = trace.TracerProvider().get_tracer("sdk_tracer_provider")
177+
mock_set_in_carrier = Mock()
178+
with tracer.start_as_current_span("parent"):
179+
with tracer.start_as_current_span("child"):
180+
FORMAT.inject(mock_set_in_carrier, {})
181+
inject_fields = set()
182+
for call in mock_set_in_carrier.mock_calls:
183+
inject_fields.add(call[1][1])
184+
self.assertEqual(FORMAT.fields, inject_fields)

0 commit comments

Comments
 (0)