diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/context/propagation/__init__.py new file mode 100644 index 0000000000..c00a800b69 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/context/propagation/__init__.py @@ -0,0 +1,2 @@ +from .httptextformat import HTTPTextFormat +from .binaryformat import BinaryFormat diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/binaryformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/binaryformat.py new file mode 100644 index 0000000000..66dbd277f8 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/context/propagation/binaryformat.py @@ -0,0 +1,29 @@ +# Copyright 2019, 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 abc +import typing +from opentelemetry.trace import SpanContext + + +class BinaryFormat(abc.ABC): + @staticmethod + @abc.abstractmethod + def to_bytes(context: SpanContext) -> bytes: + pass + + @staticmethod + @abc.abstractmethod + def from_bytes(byte_representation: bytes) -> typing.Optional[SpanContext]: + pass diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py new file mode 100644 index 0000000000..395539bbc7 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py @@ -0,0 +1,79 @@ +# Copyright 2019, 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 abc +import typing +from opentelemetry.trace import SpanContext + +Setter = typing.Callable[[object, str, str], None] +Getter = typing.Callable[[object, str], str] + + +class HTTPTextFormat(abc.ABC): + """API for propagation of spans via headers. + + This class provides an interface that enables extracting and injecting + trace state into headers of HTTP requests. Http frameworks and client + can integrate with HTTPTextFormat by providing the object containing the + headers, and a getter and setter function for the extraction and + injection of values, respectively. + + Example:: + + import flask + import requests + from opentelemetry.context.propagation import HTTPTextFormat + + PROPAGATOR = HTTPTextFormat() + + + + def get_header_from_flask_request(request, key): + return request.headers[key] + + def set_header_into_requests_request(request: requests.Request, + key: str, value: str): + request.headers[key] = value + + def example_route(): + span_context = PROPAGATOR.extract( + get_header_from_flask_request, + flask.request + ) + request_to_downstream = requests.Request( + "GET", "http://httpbin.org/get" + ) + PROPAGATOR.inject( + span_context, + set_header_into_requests_request, + request_to_downstream + ) + session = requests.Session() + session.send(request_to_downstream.prepare()) + + + .. _Propagation API Specification: + https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/api-propagators.md + + Enabling this flexi + """ + @abc.abstractmethod + def extract(self, get_from_carrier: Getter, + carrier: object) -> SpanContext: + pass + + @abc.abstractmethod + def inject(self, context: SpanContext, set_in_carrier: Setter, + carrier: object): + pass diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py new file mode 100644 index 0000000000..031f87c75f --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py @@ -0,0 +1,74 @@ +# Copyright 2019, 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. + +from opentelemetry.context.propagation.httptextformat import HTTPTextFormat +import opentelemetry.trace as trace + + +class B3Format(HTTPTextFormat): + """Propagator for the B3 HTTP header format. + + See: https://github.com/openzipkin/b3-propagation + """ + + SINGLE_HEADER_KEY = "b3" + TRACE_ID_KEY = "x-b3-traceid" + SPAN_ID_KEY = "x-b3-spanid" + SAMPLED_KEY = "x-b3-sampled" + + @classmethod + def extract(cls, get_from_carrier, carrier): + trace_id = trace.INVALID_TRACE_ID + span_id = trace.INVALID_SPAN_ID + sampled = 1 + + single_header = get_from_carrier(carrier, cls.SINGLE_HEADER_KEY) + if single_header: + # b3-propagation spec calls for the sampling state to be + # "deferred", which is unspecified. This concept does not + # translate to SpanContext, so we set it as recorded. + sampled = "1" + fields = single_header.split("-", 4) + + if len(fields) == 1: + sampled = fields[0] + elif len(fields) == 2: + trace_id, span_id = fields + elif len(fields) == 3: + trace_id, span_id, sampled = fields + elif len(fields) == 4: + trace_id, span_id, sampled, _parent_span_id = fields + else: + return trace.INVALID_SPAN_CONTEXT + else: + trace_id = get_from_carrier(carrier, cls.TRACE_ID_KEY) + span_id = get_from_carrier(carrier, cls.SPAN_ID_KEY) + sampled = get_from_carrier(carrier, cls.SAMPLED_KEY) + + options = 0 + if sampled == "1": + options |= trace.TraceOptions.RECORDED + return trace.SpanContext( + trace_id=int(trace_id), + span_id=int(span_id), + trace_options=options, + trace_state={}, + ) + + @classmethod + def inject(cls, context, set_in_carrier, carrier): + sampled = (trace.TraceOptions.RECORDED & context.trace_options) != 0 + set_in_carrier(carrier, cls.TRACE_ID_KEY, str(context.trace_id)) + set_in_carrier(carrier, cls.SPAN_ID_KEY, str(context.span_id)) + set_in_carrier(carrier, cls.SAMPLED_KEY, "1" if sampled else "0") diff --git a/opentelemetry-sdk/tests/context/__init__.py b/opentelemetry-sdk/tests/context/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-sdk/tests/context/propagation/__init__.py b/opentelemetry-sdk/tests/context/propagation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opentelemetry-sdk/tests/context/propagation/test_b3_format.py b/opentelemetry-sdk/tests/context/propagation/test_b3_format.py new file mode 100644 index 0000000000..79b1f82726 --- /dev/null +++ b/opentelemetry-sdk/tests/context/propagation/test_b3_format.py @@ -0,0 +1,57 @@ +# Copyright 2019, 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 +import opentelemetry.sdk.context.propagation.b3_format as b3_format +import opentelemetry.sdk.trace as trace + +FORMAT = b3_format.B3Format() + + +def _get_from_dict(carrier: dict, key: str) -> str: + return carrier.get(key) + + +def _set_into_dict(carrier: dict, key: str, value: str): + carrier[key] = value + + +class TestB3Format(unittest.TestCase): + def test_extract_multi_header(self): + """Test the extraction of B3 headers """ + trace_id = str(trace.generate_trace_id()) + span_id = str(trace.generate_span_id()) + carrier = { + FORMAT.TRACE_ID_KEY: trace_id, + FORMAT.SPAN_ID_KEY: span_id, + FORMAT.SAMPLED_KEY: "1", + } + span_context = FORMAT.extract(_get_from_dict, carrier) + new_carrier = {} + FORMAT.inject(span_context, _set_into_dict, new_carrier) + self.assertEqual(new_carrier[FORMAT.TRACE_ID_KEY], trace_id) + self.assertEqual(new_carrier[FORMAT.SPAN_ID_KEY], span_id) + self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") + + def test_extract_single_headder(self): + """Test the extraction from a single b3 header""" + trace_id = str(trace.generate_trace_id()) + span_id = str(trace.generate_span_id()) + carrier = {FORMAT.SINGLE_HEADER_KEY: "{}-{}".format(trace_id, span_id)} + span_context = FORMAT.extract(_get_from_dict, carrier) + new_carrier = {} + FORMAT.inject(span_context, _set_into_dict, new_carrier) + self.assertEqual(new_carrier[FORMAT.TRACE_ID_KEY], trace_id) + self.assertEqual(new_carrier[FORMAT.SPAN_ID_KEY], span_id) + self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1")