diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 5a71274bfd3..547da6ead5e 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -8,6 +8,8 @@ ([#1440](https://github.com/open-telemetry/opentelemetry-python/pull/1440)) - Add `fields` to propagators ([#1374](https://github.com/open-telemetry/opentelemetry-python/pull/1374)) +- Added support for Jaeger propagator + ([#1219](https://github.com/open-telemetry/opentelemetry-python/pull/1219)) - Add support for OTEL_SPAN_{ATTRIBUTE_COUNT_LIMIT,EVENT_COUNT_LIMIT,LINK_COUNT_LIMIT} ([#1377](https://github.com/open-telemetry/opentelemetry-python/pull/1377)) @@ -27,8 +29,8 @@ Released 2020-11-25 ([#1373](https://github.com/open-telemetry/opentelemetry-python/pull/1373)) - Rename Meter class to Accumulator in Metrics SDK ([#1372](https://github.com/open-telemetry/opentelemetry-python/pull/1372)) -- Fix `ParentBased` sampler for implicit parent spans. Fix also `trace_state` - erasure for dropped spans or spans sampled by the `TraceIdRatioBased` sampler. +- Fix `ParentBased` sampler for implicit parent spans. Fix also `trace_state` + erasure for dropped spans or spans sampled by the `TraceIdRatioBased` sampler. ([#1394](https://github.com/open-telemetry/opentelemetry-python/pull/1394)) ## Version 0.15b0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/__init__.py index e69de29bb2d..a02a07ea2af 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/__init__.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import typing + +from opentelemetry.trace.propagation.textmap import TextMapPropagatorT + + +def extract_first_element( + items: typing.Iterable[TextMapPropagatorT], +) -> typing.Optional[TextMapPropagatorT]: + if items is None: + return None + return next(iter(items), None) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/jaeger_propagator.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/jaeger_propagator.py new file mode 100644 index 00000000000..e4d2b1cb2cc --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/jaeger_propagator.py @@ -0,0 +1,137 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import typing +import urllib.parse + +import opentelemetry.trace as trace +from opentelemetry import baggage +from opentelemetry.context import Context, get_current +from opentelemetry.sdk.trace.propagation import extract_first_element +from opentelemetry.trace.propagation.textmap import ( + Getter, + Setter, + TextMapPropagator, + TextMapPropagatorT, +) + + +class JaegerPropagator(TextMapPropagator): + """Propagator for the Jaeger format. + + See: https://www.jaegertracing.io/docs/1.19/client-libraries/#propagation-format + """ + + TRACE_ID_KEY = "uber-trace-id" + BAGGAGE_PREFIX = "uberctx-" + DEBUG_FLAG = 0x02 + + def extract( + self, + getter: Getter[TextMapPropagatorT], + carrier: TextMapPropagatorT, + context: typing.Optional[Context] = None, + ) -> Context: + + if context is None: + context = get_current() + fields = extract_first_element( + getter.get(carrier, self.TRACE_ID_KEY) + ).split(":") + + context = self._extract_baggage(getter, carrier, context) + if len(fields) != 4: + return trace.set_span_in_context(trace.INVALID_SPAN, context) + + trace_id, span_id, _parent_id, flags = fields + if ( + trace_id == trace.INVALID_TRACE_ID + or span_id == trace.INVALID_SPAN_ID + ): + return trace.set_span_in_context(trace.INVALID_SPAN, context) + + span = trace.DefaultSpan( + trace.SpanContext( + trace_id=int(trace_id, 16), + span_id=int(span_id, 16), + is_remote=True, + trace_flags=trace.TraceFlags( + int(flags, 16) & trace.TraceFlags.SAMPLED + ), + ) + ) + return trace.set_span_in_context(span, context) + + def inject( + self, + set_in_carrier: Setter[TextMapPropagatorT], + carrier: TextMapPropagatorT, + context: typing.Optional[Context] = None, + ) -> None: + span = trace.get_current_span(context=context) + span_context = span.get_span_context() + if span_context == trace.INVALID_SPAN_CONTEXT: + return + + span_parent_id = span.parent.span_id if span.parent else 0 + trace_flags = span_context.trace_flags + if trace_flags.sampled: + trace_flags |= self.DEBUG_FLAG + + # set span identity + set_in_carrier( + carrier, + self.TRACE_ID_KEY, + _format_uber_trace_id( + span_context.trace_id, + span_context.span_id, + span_parent_id, + trace_flags, + ), + ) + + # set span baggage, if any + baggage_entries = baggage.get_all(context=context) + if not baggage_entries: + return + for key, value in baggage_entries.items(): + baggage_key = self.BAGGAGE_PREFIX + key + set_in_carrier( + carrier, baggage_key, urllib.parse.quote(str(value)) + ) + + @property + def fields(self) -> typing.Set[str]: + return {self.TRACE_ID_KEY} + + def _extract_baggage(self, getter, carrier, context): + baggage_keys = [ + key + for key in getter.keys(carrier) + if key.startswith(self.BAGGAGE_PREFIX) + ] + for key in baggage_keys: + value = extract_first_element(getter.get(carrier, key)) + context = baggage.set_baggage( + key.replace(self.BAGGAGE_PREFIX, ""), + urllib.parse.unquote(value).strip(), + context=context, + ) + return context + + +def _format_uber_trace_id(trace_id, span_id, parent_span_id, flags): + return "{:032x}:{:016x}:{:016x}:{:02x}".format( + trace_id, span_id, parent_span_id, flags + ) diff --git a/opentelemetry-sdk/tests/trace/propagation/test_jaeger_propagator.py b/opentelemetry-sdk/tests/trace/propagation/test_jaeger_propagator.py new file mode 100644 index 00000000000..bd80770368b --- /dev/null +++ b/opentelemetry-sdk/tests/trace/propagation/test_jaeger_propagator.py @@ -0,0 +1,184 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest.mock import Mock + +import opentelemetry.sdk.trace as trace +import opentelemetry.sdk.trace.propagation.jaeger_propagator as jaeger +import opentelemetry.trace as trace_api +from opentelemetry import baggage +from opentelemetry.trace.propagation.textmap import DictGetter + +FORMAT = jaeger.JaegerPropagator() + + +carrier_getter = DictGetter() + + +def get_context_new_carrier(old_carrier, carrier_baggage=None): + + ctx = FORMAT.extract(carrier_getter, old_carrier) + if carrier_baggage: + for key, value in carrier_baggage.items(): + ctx = baggage.set_baggage(key, value, ctx) + parent_span_context = trace_api.get_current_span(ctx).get_span_context() + + parent = trace._Span("parent", parent_span_context) + child = trace._Span( + "child", + trace_api.SpanContext( + parent_span_context.trace_id, + trace_api.RandomIdsGenerator().generate_span_id(), + is_remote=False, + trace_flags=parent_span_context.trace_flags, + trace_state=parent_span_context.trace_state, + ), + parent=parent.get_span_context(), + ) + + new_carrier = {} + ctx = trace_api.set_span_in_context(child, ctx) + + FORMAT.inject(dict.__setitem__, new_carrier, context=ctx) + + return ctx, new_carrier + + +def _format_uber_trace_id(trace_id, span_id, parent_span_id, flags): + return "{:032x}:{:016x}:{:016x}:{:02x}".format( + trace_id, span_id, parent_span_id, flags + ) + + +class TestJaegerPropagator(unittest.TestCase): + @classmethod + def setUpClass(cls): + ids_generator = trace_api.RandomIdsGenerator() + cls.trace_id = ids_generator.generate_trace_id() + cls.span_id = ids_generator.generate_span_id() + cls.parent_span_id = ids_generator.generate_span_id() + cls.serialized_uber_trace_id = _format_uber_trace_id( + cls.trace_id, cls.span_id, cls.parent_span_id, 11 + ) + + def test_extract_valid_span(self): + old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id} + ctx = FORMAT.extract(carrier_getter, old_carrier) + span_context = trace_api.get_current_span(ctx).get_span_context() + self.assertEqual(span_context.trace_id, self.trace_id) + self.assertEqual(span_context.span_id, self.span_id) + + def test_trace_id(self): + old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id} + _, new_carrier = get_context_new_carrier(old_carrier) + self.assertEqual( + self.serialized_uber_trace_id.split(":")[0], + new_carrier[FORMAT.TRACE_ID_KEY].split(":")[0], + ) + + def test_parent_span_id(self): + old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id} + _, new_carrier = get_context_new_carrier(old_carrier) + span_id = self.serialized_uber_trace_id.split(":")[1] + parent_span_id = new_carrier[FORMAT.TRACE_ID_KEY].split(":")[2] + self.assertEqual(span_id, parent_span_id) + + def test_sampled_flag_set(self): + old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id} + _, new_carrier = get_context_new_carrier(old_carrier) + sample_flag_value = ( + int(new_carrier[FORMAT.TRACE_ID_KEY].split(":")[3]) & 0x01 + ) + self.assertEqual(1, sample_flag_value) + + def test_debug_flag_set(self): + old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id} + _, new_carrier = get_context_new_carrier(old_carrier) + debug_flag_value = ( + int(new_carrier[FORMAT.TRACE_ID_KEY].split(":")[3]) + & FORMAT.DEBUG_FLAG + ) + self.assertEqual(FORMAT.DEBUG_FLAG, debug_flag_value) + + def test_sample_debug_flags_unset(self): + uber_trace_id = _format_uber_trace_id( + self.trace_id, self.span_id, self.parent_span_id, 0 + ) + old_carrier = {FORMAT.TRACE_ID_KEY: uber_trace_id} + _, new_carrier = get_context_new_carrier(old_carrier) + flags = int(new_carrier[FORMAT.TRACE_ID_KEY].split(":")[3]) + sample_flag_value = flags & 0x01 + debug_flag_value = flags & FORMAT.DEBUG_FLAG + self.assertEqual(0, sample_flag_value) + self.assertEqual(0, debug_flag_value) + + def test_baggage(self): + old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id} + input_baggage = {"key1": "value1"} + _, new_carrier = get_context_new_carrier(old_carrier, input_baggage) + ctx = FORMAT.extract(carrier_getter, new_carrier) + self.assertDictEqual(input_baggage, ctx["baggage"]) + + def test_non_string_baggage(self): + old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id} + input_baggage = {"key1": 1, "key2": True} + formatted_baggage = {"key1": "1", "key2": "True"} + _, new_carrier = get_context_new_carrier(old_carrier, input_baggage) + ctx = FORMAT.extract(carrier_getter, new_carrier) + self.assertDictEqual(formatted_baggage, ctx["baggage"]) + + def test_extract_invalid_uber_trace_id(self): + old_carrier = { + "uber-trace-id": "000000000000000000000000deadbeef:00000000deadbef0:00", + "uberctx-key1": "value1", + } + formatted_baggage = {"key1": "value1"} + context = FORMAT.extract(carrier_getter, old_carrier) + span_context = trace_api.get_current_span(context).get_span_context() + self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) + self.assertDictEqual(formatted_baggage, context["baggage"]) + + def test_extract_invalid_trace_id(self): + old_carrier = { + "uber-trace-id": "00000000000000000000000000000000:00000000deadbef0:00:00", + "uberctx-key1": "value1", + } + formatted_baggage = {"key1": "value1"} + context = FORMAT.extract(carrier_getter, old_carrier) + span_context = trace_api.get_current_span(context).get_span_context() + self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) + self.assertDictEqual(formatted_baggage, context["baggage"]) + + def test_extract_invalid_span_id(self): + old_carrier = { + "uber-trace-id": "000000000000000000000000deadbeef:0000000000000000:00:00", + "uberctx-key1": "value1", + } + formatted_baggage = {"key1": "value1"} + context = FORMAT.extract(carrier_getter, old_carrier) + span_context = trace_api.get_current_span(context).get_span_context() + self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) + self.assertDictEqual(formatted_baggage, context["baggage"]) + + def test_fields(self): + tracer = trace.TracerProvider().get_tracer("sdk_tracer_provider") + mock_set_in_carrier = Mock() + with tracer.start_as_current_span("parent"): + with tracer.start_as_current_span("child"): + FORMAT.inject(mock_set_in_carrier, {}) + inject_fields = set() + for call in mock_set_in_carrier.mock_calls: + inject_fields.add(call[1][1]) + self.assertEqual(FORMAT.fields, inject_fields)