Repository: open-telemetry/opentelemetry-python

Issue:
Add support for Jaeger propagator
As per spec, we must provide support for the Jaeger propagation format: https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/context/api-propagators.md#propagators-distribution



Discussion:
Hey @codeboten, could you elaborate on this a bit? Was wondering if I could pick this up!
Hi @Rashmi-K-A, this issue is to add a new propagator to handle the Jaeger format specified [here](https://www.jaegertracing.io/docs/latest/client-libraries/#propagation-format). Here's an example of an existing propagator that's already implemented: https://github.com/open-telemetry/opentelemetry-python/blob/master/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py
Let me know if you'd like to take this on and I can assign it to you, thanks!
@codeboten yep, I'll take this one!
Assigned, thanks!
Hi, @codeboten, the implementation of the extractor function for Jaeger format requires a keys method implemented to extract keys with the prefix 'uberctx-'.  I took a look at the spec discussion [here](https://github.com/open-telemetry/opentelemetry-specification/pull/825#issuecomment-679341006) and I don't see any other way out. Advice on how to proceed please!
Hey @Rashmi-K-A, looks like it's probably worth addressing this issue first then: https://github.com/open-telemetry/opentelemetry-python/issues/1084, did you want to tackle that one?
Someone picked it up, I'll wait for that issue to be closed before raising a PR! 
Wow that was quick, sounds good!

Resolving patch:
diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/__init__.py
--- 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
--- /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
+    )


Test patch:
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
--- /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)