From 384496cb1d69273b7ce3df3b7b74e51c92ade0bb Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Sat, 14 Sep 2019 11:58:22 -0700 Subject: [PATCH 1/6] Adding a working propagator, adding to integrations and example Adding a full, end-to-end example of propagation at work in the example application, including a test. Adding the use of propagators into the integrations. --- .../ext/http_requests/__init__.py | 7 ++ .../src/opentelemetry/ext/wsgi/__init__.py | 25 ++++-- .../tests/test_wsgi_middleware.py | 4 +- .../propagation/tracestatehttptextformat.py | 20 +++++ .../src/opentelemetry/propagator/__init__.py | 77 +++++++++++++++++++ .../flask_example.py | 18 +++-- .../tests/test_flask_example.py | 49 +++++++++++- .../sdk/context/propagation/b3_format.py | 1 - 8 files changed, 184 insertions(+), 17 deletions(-) create mode 100644 opentelemetry-api/src/opentelemetry/context/propagation/tracestatehttptextformat.py create mode 100644 opentelemetry-api/src/opentelemetry/propagator/__init__.py diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py index 2689b8ea2d..6be8bc6519 100644 --- a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py @@ -22,6 +22,8 @@ from requests.sessions import Session +import opentelemetry.propagator as propagator + # NOTE: Currently we force passing a tracer. But in turn, this forces the user # to configure a SDK before enabling this integration. In turn, this means that @@ -72,6 +74,11 @@ def instrumented_request(self, method, url, *args, **kwargs): # TODO: Propagate the trace context via headers once we have a way # to access propagators. + headers = kwargs.get("headers", {}) + propagator.get_global_propagator().inject( + tracer, type(headers).__setitem__, headers + ) + kwargs["headers"] = headers result = wrapped(self, method, url, *args, **kwargs) # *** PROCEED span.set_attribute("http.status_code", result.status_code) diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py index 974a6998a8..1a8a6e0799 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py @@ -19,9 +19,10 @@ """ import functools +import typing import wsgiref.util as wsgiref_util -from opentelemetry import trace +from opentelemetry import propagator, trace from opentelemetry.ext.wsgi.version import __version__ # noqa @@ -35,12 +36,9 @@ class OpenTelemetryMiddleware: wsgi: The WSGI application callable. """ - def __init__(self, wsgi, propagators=None): + def __init__(self, wsgi): self.wsgi = wsgi - # TODO: implement context propagation - self.propagators = propagators - @staticmethod def _add_request_attributes(span, environ): span.set_attribute("component", "http") @@ -87,8 +85,11 @@ def __call__(self, environ, start_response): tracer = trace.tracer() path_info = environ["PATH_INFO"] or "/" + parent_span = propagator.get_global_propagator().extract( + get_header_from_environ, environ + ) - with tracer.start_span(path_info) as span: + with tracer.start_span(path_info, parent_span) as span: self._add_request_attributes(span, environ) start_response = self._create_start_response(span, start_response) @@ -99,3 +100,15 @@ def __call__(self, environ, start_response): finally: if hasattr(iterable, "close"): iterable.close() + + +def get_header_from_environ( + environ: dict, header_name: str +) -> typing.Optional[str]: + """Retrieve the header value from the wsgi environ dictionary. + + Returns: + A string with the header value if it exists, else None. + """ + environ_key = "HTTP_" + header_name.upper().replace("-", "_") + return [environ.get(environ_key)] diff --git a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py index b423224ade..c69c1bdc22 100644 --- a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py +++ b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py @@ -125,7 +125,9 @@ def validate_response(self, response, error=None): self.assertIsNone(self.exc_info) # Verify that start_span has been called - self.start_span.assert_called_once_with("/") + self.start_span.assert_called_once_with( + "/", trace_api.INVALID_SPAN_CONTEXT + ) def test_basic_wsgi_call(self): app = OpenTelemetryMiddleware(simple_wsgi) diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/tracestatehttptextformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/tracestatehttptextformat.py new file mode 100644 index 0000000000..dbc3451408 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/context/propagation/tracestatehttptextformat.py @@ -0,0 +1,20 @@ +import opentelemetry.trace as trace +from opentelemetry.context.propagation import httptextformat + + +class TraceStateHTTPTextFormat(httptextformat.HTTPTextFormat): + """TODO: a propagator that extracts and injects tracestate. + """ + + def extract( + self, _get_from_carrier: httptextformat.Getter, _carrier: object + ) -> trace.SpanContext: + return trace.INVALID_SPAN_CONTEXT + + def inject( + self, + context: trace.SpanContext, + set_in_carrier: httptextformat.Setter, + carrier: object, + ) -> None: + pass diff --git a/opentelemetry-api/src/opentelemetry/propagator/__init__.py b/opentelemetry-api/src/opentelemetry/propagator/__init__.py new file mode 100644 index 0000000000..0c7cf5b299 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/propagator/__init__.py @@ -0,0 +1,77 @@ +import typing + +import opentelemetry.context.propagation.httptextformat as httptextformat +import opentelemetry.trace as trace +from opentelemetry.context.propagation.tracestatehttptextformat import ( + TraceStateHTTPTextFormat, +) + + +class Propagator: + """Class which encapsulates propagation of values to and from context. + + In contrast to using the formatters directly, a propagator object can + help own configuration around which formatters to use, as well as + help simplify the work require for integrations to use the intended + formatters. + """ + + def __init__(self, httptextformat_instance: httptextformat.HTTPTextFormat): + self._httptextformat = httptextformat_instance + + def extract( + self, get_from_carrier: httptextformat.Getter, carrier: object + ) -> typing.Union[trace.SpanContext, trace.Span, None]: + """Load the parent SpanContext from values in the carrier. + + Using the specified HTTPTextFormatter, the propagator will + extract a SpanContext from the carrier. If one is found, + it will be set as the parent context of the current span. + + Args: + get_from_carrier: a function that can retrieve zero + or more values from the carrier. In the case that + the value does not exist, return an empty list. + carrier: and object which contains values that are + used to construct a SpanContext. This object + must be paired with an appropriate get_from_carrier + which understands how to extract a value from it. + """ + span_context = self._httptextformat.extract(get_from_carrier, carrier) + return span_context if span_context else trace.Tracer.CURRENT_SPAN + + def inject( + self, + tracer: trace.Tracer, + set_in_carrier: httptextformat.Setter, + carrier: object, + ) -> None: + """Inject values from the current context into the carrier. + + inject enables the propagation of values into HTTP clients or + other objects which perform an HTTP request. Implementations + should use the set_in_carrier method to set values on the + carrier. + + Args: + set_in_carrier: A setter function that can set values + on the carrier. + carrier: An object that a place to define HTTP headers. + Should be paired with set_in_carrier, which should + know how to set header values on the carrier. + """ + self._httptextformat.inject( + tracer.get_current_span().get_context(), set_in_carrier, carrier + ) + + +_PROPAGATOR = Propagator(TraceStateHTTPTextFormat()) + + +def get_global_propagator() -> Propagator: + return _PROPAGATOR + + +def set_global_propagator(propagator: Propagator) -> None: + global _PROPAGATOR # pylint:disable=global-statement + _PROPAGATOR = propagator diff --git a/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py b/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py index 9568f270c9..71158a573d 100644 --- a/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py +++ b/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py @@ -16,13 +16,13 @@ This module serves as an example to integrate with flask, using the requests library to perform downstream requests """ -import time - import flask +import requests import opentelemetry.ext.http_requests -from opentelemetry import trace +from opentelemetry import propagator, trace from opentelemetry.ext.wsgi import OpenTelemetryMiddleware +from opentelemetry.sdk.context.propagation.b3_format import B3Format from opentelemetry.sdk.trace import Tracer @@ -39,7 +39,6 @@ def configure_opentelemetry(flask_app: flask.Flask): * processors? * exporters? - * propagators? """ # Start by configuring all objects required to ensure # a complete end to end workflow. @@ -47,6 +46,13 @@ def configure_opentelemetry(flask_app: flask.Flask): # as the opentelemetry-api defines the interface with a no-op # implementation. trace.set_preferred_tracer_implementation(lambda _: Tracer()) + # Next, we need to configure how the values that are used by + # traces and metrics are propagated (such as what specific headers + # carry this value). + + # TBD: can remove once default TraceState propagators are installed. + propagator.set_global_propagator(propagator.Propagator(B3Format())) + # Integrations are the glue that binds the OpenTelemetry API # and the frameworks and libraries that are used together, automatically # creating Spans and propagating context as appropriate. @@ -61,8 +67,8 @@ def configure_opentelemetry(flask_app: flask.Flask): def hello(): # emit a trace that measures how long the # sleep takes - with trace.tracer().start_span("sleep"): - time.sleep(0.001) + with trace.tracer().start_span("example-request"): + requests.get("http://www.example.com") return "hello" diff --git a/opentelemetry-example-app/tests/test_flask_example.py b/opentelemetry-example-app/tests/test_flask_example.py index ca2a237a60..82bfa725ff 100644 --- a/opentelemetry-example-app/tests/test_flask_example.py +++ b/opentelemetry-example-app/tests/test_flask_example.py @@ -1,6 +1,13 @@ import unittest +from unittest import mock + +import requests +from werkzeug.test import Client +from werkzeug.wrappers import BaseResponse import opentelemetry_example_app.flask_example as flask_example +from opentelemetry.sdk import trace +from opentelemetry.sdk.context.propagation import b3_format class TestFlaskExample(unittest.TestCase): @@ -8,7 +15,43 @@ class TestFlaskExample(unittest.TestCase): def setUpClass(cls): cls.app = flask_example.app + def setUp(self): + mocked_response = requests.models.Response() + mocked_response.status_code = 200 + mocked_response.reason = "Roger that!" + self.send_patcher = mock.patch.object( + requests.Session, + "send", + autospec=True, + spec_set=True, + return_value=mocked_response, + ) + self.send = self.send_patcher.start() + + def tearDown(self): + self.send_patcher.stop() + def test_full_path(self): - with self.app.test_client() as client: - response = client.get("/") - assert response.data.decode() == "hello" + trace_id = trace.generate_trace_id() + # We need to use the Werkzeug test app because + # The headers are injected at the wsgi layer. + # The flask test app will not include these, and + # result in the values not propagated. + client = Client(self.app.wsgi_app, BaseResponse) + # emulate b3 headers + client.get( + "/", + headers={ + "x-b3-traceid": b3_format.format_trace_id(trace_id), + "x-b3-spanid": b3_format.format_span_id( + trace.generate_span_id() + ), + "x-b3-sampled": "1", + }, + ) + # assert the http request header was propagated through. + prepared_request = self.send.call_args[0][1] + headers = prepared_request.headers + for required_header in {"x-b3-traceid", "x-b3-spanid", "x-b3-sampled"}: + assert required_header in headers + assert headers["x-b3-traceid"] == b3_format.format_trace_id(trace_id) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py index 72d02d6070..9e8f7d3f19 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py @@ -91,7 +91,6 @@ def extract(cls, get_from_carrier, carrier): # header is set to allow. if sampled in cls._SAMPLE_PROPAGATE_VALUES or flags == "1": options |= trace.TraceOptions.RECORDED - return trace.SpanContext( # trace an span ids are encoded in hex, so must be converted trace_id=int(trace_id, 16), From 4e9f07551401e62a1e61f6611dd5daa86c9e6aa1 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Mon, 16 Sep 2019 16:26:37 -0700 Subject: [PATCH 2/6] Addressing review feedback Fixing incorrect type annotation for WSGI getter for HTTPTextFormatter. Using setdefautl to enable more atomic and simple setting of headers. Using UnitTest style assertions vs PyTest. --- .../src/opentelemetry/ext/http_requests/__init__.py | 3 +-- .../src/opentelemetry/ext/wsgi/__init__.py | 7 +++++-- opentelemetry-example-app/tests/test_flask_example.py | 6 ++++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py index 6be8bc6519..576d13d922 100644 --- a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py @@ -74,11 +74,10 @@ def instrumented_request(self, method, url, *args, **kwargs): # TODO: Propagate the trace context via headers once we have a way # to access propagators. - headers = kwargs.get("headers", {}) + headers = kwargs.setdefault("headers", {}) propagator.get_global_propagator().inject( tracer, type(headers).__setitem__, headers ) - kwargs["headers"] = headers result = wrapped(self, method, url, *args, **kwargs) # *** PROCEED span.set_attribute("http.status_code", result.status_code) diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py index 1a8a6e0799..1577ae546c 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py @@ -104,11 +104,14 @@ def __call__(self, environ, start_response): def get_header_from_environ( environ: dict, header_name: str -) -> typing.Optional[str]: +) -> typing.List[str]: """Retrieve the header value from the wsgi environ dictionary. Returns: A string with the header value if it exists, else None. """ environ_key = "HTTP_" + header_name.upper().replace("-", "_") - return [environ.get(environ_key)] + value = environ.get(environ_key) + if value: + return [value] + return [] diff --git a/opentelemetry-example-app/tests/test_flask_example.py b/opentelemetry-example-app/tests/test_flask_example.py index 82bfa725ff..d33e2b9ac0 100644 --- a/opentelemetry-example-app/tests/test_flask_example.py +++ b/opentelemetry-example-app/tests/test_flask_example.py @@ -53,5 +53,7 @@ def test_full_path(self): prepared_request = self.send.call_args[0][1] headers = prepared_request.headers for required_header in {"x-b3-traceid", "x-b3-spanid", "x-b3-sampled"}: - assert required_header in headers - assert headers["x-b3-traceid"] == b3_format.format_trace_id(trace_id) + self.assertIn(required_header, headers) + self.assertEqual( + headers["x-b3-traceid"], b3_format.format_trace_id(trace_id) + ) From d856f9d1810d8077be7ef654f6273f120c3485e8 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Mon, 16 Sep 2019 16:43:56 -0700 Subject: [PATCH 3/6] Make getter/setter types specific to context --- .../context/propagation/httptextformat.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py index f3823a86d1..ffde2f1c25 100644 --- a/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py +++ b/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py @@ -14,11 +14,14 @@ import abc import typing +from typing import Type, TypeVar from opentelemetry.trace import SpanContext -Setter = typing.Callable[[object, str, str], None] -Getter = typing.Callable[[object, str], typing.List[str]] +_T = typing.TypeVar("_T") + +Setter = typing.Callable[[Type[_T], str, str], None] +Getter = typing.Callable[[Type[_T], str], typing.List[str]] class HTTPTextFormat(abc.ABC): @@ -70,7 +73,7 @@ def example_route(): @abc.abstractmethod def extract( - self, get_from_carrier: Getter, carrier: object + self, get_from_carrier: Getter[_T], carrier: _T ) -> SpanContext: """Create a SpanContext from values in the carrier. @@ -93,7 +96,7 @@ def extract( @abc.abstractmethod def inject( - self, context: SpanContext, set_in_carrier: Setter, carrier: object + self, context: SpanContext, set_in_carrier: Setter[_T], carrier: _T ) -> None: """Inject values from a SpanContext into a carrier. From 9789e581da3d7c0646161d576e84e0cd45ba2d99 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Tue, 17 Sep 2019 21:31:12 -0700 Subject: [PATCH 4/6] Addressing feedback Renaming propagator to plural, to match other top level module names. Using static functions for setting / getting global httptextformat, and making propagator methods static. Renaming "TraceStateHTTP" to "TraceContextHTTP", since the latter is the correct name for the w3c spec. --- .../ext/http_requests/__init__.py | 6 +- .../src/opentelemetry/ext/wsgi/__init__.py | 6 +- .../propagation/tracecontexthttptextformat.py | 35 +++++++++ .../propagation/tracestatehttptextformat.py | 20 ----- .../src/opentelemetry/propagator/__init__.py | 77 ------------------- .../src/opentelemetry/propagators/__init__.py | 66 ++++++++++++++++ .../flask_example.py | 6 +- 7 files changed, 108 insertions(+), 108 deletions(-) create mode 100644 opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py delete mode 100644 opentelemetry-api/src/opentelemetry/context/propagation/tracestatehttptextformat.py delete mode 100644 opentelemetry-api/src/opentelemetry/propagator/__init__.py create mode 100644 opentelemetry-api/src/opentelemetry/propagators/__init__.py diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py index 576d13d922..d904a85f90 100644 --- a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py @@ -22,7 +22,7 @@ from requests.sessions import Session -import opentelemetry.propagator as propagator +from opentelemetry import propagators # NOTE: Currently we force passing a tracer. But in turn, this forces the user @@ -75,9 +75,7 @@ def instrumented_request(self, method, url, *args, **kwargs): # to access propagators. headers = kwargs.setdefault("headers", {}) - propagator.get_global_propagator().inject( - tracer, type(headers).__setitem__, headers - ) + propagators.inject(tracer, type(headers).__setitem__, headers) result = wrapped(self, method, url, *args, **kwargs) # *** PROCEED span.set_attribute("http.status_code", result.status_code) diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py index 1577ae546c..1f428fb9ab 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py @@ -22,7 +22,7 @@ import typing import wsgiref.util as wsgiref_util -from opentelemetry import propagator, trace +from opentelemetry import propagators, trace from opentelemetry.ext.wsgi.version import __version__ # noqa @@ -85,9 +85,7 @@ def __call__(self, environ, start_response): tracer = trace.tracer() path_info = environ["PATH_INFO"] or "/" - parent_span = propagator.get_global_propagator().extract( - get_header_from_environ, environ - ) + parent_span = propagators.extract(get_header_from_environ, environ) with tracer.start_span(path_info, parent_span) as span: self._add_request_attributes(span, environ) diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py new file mode 100644 index 0000000000..35591635f9 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py @@ -0,0 +1,35 @@ +# 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 opentelemetry.trace as trace +from opentelemetry.context.propagation import httptextformat + + +class TraceContextHTTPTextFormat(httptextformat.HTTPTextFormat): + """TODO: extracts and injects using w3c TraceContext's headers. + """ + + def extract( + self, _get_from_carrier: httptextformat.Getter, _carrier: object + ) -> trace.SpanContext: + return trace.INVALID_SPAN_CONTEXT + + def inject( + self, + context: trace.SpanContext, + set_in_carrier: httptextformat.Setter, + carrier: object, + ) -> None: + pass diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/tracestatehttptextformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/tracestatehttptextformat.py deleted file mode 100644 index dbc3451408..0000000000 --- a/opentelemetry-api/src/opentelemetry/context/propagation/tracestatehttptextformat.py +++ /dev/null @@ -1,20 +0,0 @@ -import opentelemetry.trace as trace -from opentelemetry.context.propagation import httptextformat - - -class TraceStateHTTPTextFormat(httptextformat.HTTPTextFormat): - """TODO: a propagator that extracts and injects tracestate. - """ - - def extract( - self, _get_from_carrier: httptextformat.Getter, _carrier: object - ) -> trace.SpanContext: - return trace.INVALID_SPAN_CONTEXT - - def inject( - self, - context: trace.SpanContext, - set_in_carrier: httptextformat.Setter, - carrier: object, - ) -> None: - pass diff --git a/opentelemetry-api/src/opentelemetry/propagator/__init__.py b/opentelemetry-api/src/opentelemetry/propagator/__init__.py deleted file mode 100644 index 0c7cf5b299..0000000000 --- a/opentelemetry-api/src/opentelemetry/propagator/__init__.py +++ /dev/null @@ -1,77 +0,0 @@ -import typing - -import opentelemetry.context.propagation.httptextformat as httptextformat -import opentelemetry.trace as trace -from opentelemetry.context.propagation.tracestatehttptextformat import ( - TraceStateHTTPTextFormat, -) - - -class Propagator: - """Class which encapsulates propagation of values to and from context. - - In contrast to using the formatters directly, a propagator object can - help own configuration around which formatters to use, as well as - help simplify the work require for integrations to use the intended - formatters. - """ - - def __init__(self, httptextformat_instance: httptextformat.HTTPTextFormat): - self._httptextformat = httptextformat_instance - - def extract( - self, get_from_carrier: httptextformat.Getter, carrier: object - ) -> typing.Union[trace.SpanContext, trace.Span, None]: - """Load the parent SpanContext from values in the carrier. - - Using the specified HTTPTextFormatter, the propagator will - extract a SpanContext from the carrier. If one is found, - it will be set as the parent context of the current span. - - Args: - get_from_carrier: a function that can retrieve zero - or more values from the carrier. In the case that - the value does not exist, return an empty list. - carrier: and object which contains values that are - used to construct a SpanContext. This object - must be paired with an appropriate get_from_carrier - which understands how to extract a value from it. - """ - span_context = self._httptextformat.extract(get_from_carrier, carrier) - return span_context if span_context else trace.Tracer.CURRENT_SPAN - - def inject( - self, - tracer: trace.Tracer, - set_in_carrier: httptextformat.Setter, - carrier: object, - ) -> None: - """Inject values from the current context into the carrier. - - inject enables the propagation of values into HTTP clients or - other objects which perform an HTTP request. Implementations - should use the set_in_carrier method to set values on the - carrier. - - Args: - set_in_carrier: A setter function that can set values - on the carrier. - carrier: An object that a place to define HTTP headers. - Should be paired with set_in_carrier, which should - know how to set header values on the carrier. - """ - self._httptextformat.inject( - tracer.get_current_span().get_context(), set_in_carrier, carrier - ) - - -_PROPAGATOR = Propagator(TraceStateHTTPTextFormat()) - - -def get_global_propagator() -> Propagator: - return _PROPAGATOR - - -def set_global_propagator(propagator: Propagator) -> None: - global _PROPAGATOR # pylint:disable=global-statement - _PROPAGATOR = propagator diff --git a/opentelemetry-api/src/opentelemetry/propagators/__init__.py b/opentelemetry-api/src/opentelemetry/propagators/__init__.py new file mode 100644 index 0000000000..1fb8551ab5 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/propagators/__init__.py @@ -0,0 +1,66 @@ +import opentelemetry.context.propagation.httptextformat as httptextformat +import opentelemetry.trace as trace +from opentelemetry.context.propagation.tracecontexthttptextformat import ( + TraceContextHTTPTextFormat, +) + + +def extract( + get_from_carrier: httptextformat.Getter, carrier: object +) -> trace.SpanContext: + """Load the parent SpanContext from values in the carrier. + + Using the specified HTTPTextFormatter, the propagator will + extract a SpanContext from the carrier. If one is found, + it will be set as the parent context of the current span. + + Args: + get_from_carrier: a function that can retrieve zero + or more values from the carrier. In the case that + the value does not exist, return an empty list. + carrier: and object which contains values that are + used to construct a SpanContext. This object + must be paired with an appropriate get_from_carrier + which understands how to extract a value from it. + """ + return get_global_httptextformat().extract(get_from_carrier, carrier) + + +def inject( + tracer: trace.Tracer, + set_in_carrier: httptextformat.Setter, + carrier: object, +) -> None: + """Inject values from the current context into the carrier. + + inject enables the propagation of values into HTTP clients or + other objects which perform an HTTP request. Implementations + should use the set_in_carrier method to set values on the + carrier. + + Args: + set_in_carrier: A setter function that can set values + on the carrier. + carrier: An object that contains a representation of HTTP + headers. Should be paired with set_in_carrier, which + should know how to set header values on the carrier. + """ + get_global_httptextformat().inject( + tracer.get_current_span().get_context(), set_in_carrier, carrier + ) + + +_HTTP_TEXT_FORMAT = ( + TraceContextHTTPTextFormat() +) # type: httptextformat.HTTPTextFormat + + +def get_global_httptextformat() -> httptextformat.HTTPTextFormat: + return _HTTP_TEXT_FORMAT + + +def set_global_httptextformat( + http_text_format: httptextformat.HTTPTextFormat +) -> None: + global _HTTP_TEXT_FORMAT # pylint:disable=global-statement + _HTTP_TEXT_FORMAT = http_text_format diff --git a/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py b/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py index 71158a573d..229acdfb43 100644 --- a/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py +++ b/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py @@ -20,7 +20,7 @@ import requests import opentelemetry.ext.http_requests -from opentelemetry import propagator, trace +from opentelemetry import propagators, trace from opentelemetry.ext.wsgi import OpenTelemetryMiddleware from opentelemetry.sdk.context.propagation.b3_format import B3Format from opentelemetry.sdk.trace import Tracer @@ -50,8 +50,8 @@ def configure_opentelemetry(flask_app: flask.Flask): # traces and metrics are propagated (such as what specific headers # carry this value). - # TBD: can remove once default TraceState propagators are installed. - propagator.set_global_propagator(propagator.Propagator(B3Format())) + # TBD: can remove once default TraceContext propagators are installed. + propagators.set_global_httptextformat(B3Format()) # Integrations are the glue that binds the OpenTelemetry API # and the frameworks and libraries that are used together, automatically From 105654517f57e1318dd3d8c7dcf59473c87ac6de Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Tue, 17 Sep 2019 22:13:53 -0700 Subject: [PATCH 5/6] Fixing type signatures --- .../context/propagation/httptextformat.py | 5 ++--- .../context/propagation/tracecontexthttptextformat.py | 10 +++++++--- .../src/opentelemetry/propagators/__init__.py | 10 +++++++--- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py index ffde2f1c25..35bdfbb3fe 100644 --- a/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py +++ b/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py @@ -14,14 +14,13 @@ import abc import typing -from typing import Type, TypeVar from opentelemetry.trace import SpanContext _T = typing.TypeVar("_T") -Setter = typing.Callable[[Type[_T], str, str], None] -Getter = typing.Callable[[Type[_T], str], typing.List[str]] +Setter = typing.Callable[[typing.Type[_T], str, str], None] +Getter = typing.Callable[[typing.Type[_T], str], typing.List[str]] class HTTPTextFormat(abc.ABC): diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py index 35591635f9..575644a91f 100644 --- a/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py +++ b/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py @@ -13,23 +13,27 @@ # limitations under the License. # +import typing + import opentelemetry.trace as trace from opentelemetry.context.propagation import httptextformat +_T = typing.TypeVar("_T") + class TraceContextHTTPTextFormat(httptextformat.HTTPTextFormat): """TODO: extracts and injects using w3c TraceContext's headers. """ def extract( - self, _get_from_carrier: httptextformat.Getter, _carrier: object + self, _get_from_carrier: httptextformat.Getter[_T], _carrier: _T ) -> trace.SpanContext: return trace.INVALID_SPAN_CONTEXT def inject( self, context: trace.SpanContext, - set_in_carrier: httptextformat.Setter, - carrier: object, + set_in_carrier: httptextformat.Setter[_T], + carrier: _T, ) -> None: pass diff --git a/opentelemetry-api/src/opentelemetry/propagators/__init__.py b/opentelemetry-api/src/opentelemetry/propagators/__init__.py index 1fb8551ab5..aa59669407 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagators/__init__.py @@ -1,12 +1,16 @@ +import typing + import opentelemetry.context.propagation.httptextformat as httptextformat import opentelemetry.trace as trace from opentelemetry.context.propagation.tracecontexthttptextformat import ( TraceContextHTTPTextFormat, ) +_T = typing.TypeVar("_T") + def extract( - get_from_carrier: httptextformat.Getter, carrier: object + get_from_carrier: httptextformat.Getter[_T], carrier: _T ) -> trace.SpanContext: """Load the parent SpanContext from values in the carrier. @@ -28,8 +32,8 @@ def extract( def inject( tracer: trace.Tracer, - set_in_carrier: httptextformat.Setter, - carrier: object, + set_in_carrier: httptextformat.Setter[_T], + carrier: _T, ) -> None: """Inject values from the current context into the carrier. From 607a7943fc7dce105b16b8304c97785a21286578 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Wed, 18 Sep 2019 09:53:27 -0700 Subject: [PATCH 6/6] fixing formatting. --- .../src/opentelemetry/ext/http_requests/__init__.py | 1 + .../src/opentelemetry/ext/wsgi/__init__.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py index 7971b0e128..994968e196 100644 --- a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py @@ -25,6 +25,7 @@ from opentelemetry import propagators from opentelemetry.trace import SpanKind + # NOTE: Currently we force passing a tracer. But in turn, this forces the user # to configure a SDK before enabling this integration. In turn, this means that # if the SDK/tracer is already using `requests` they may, in theory, bypass our diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py index 6974a77df3..28caa77e52 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py @@ -87,7 +87,9 @@ def __call__(self, environ, start_response): path_info = environ["PATH_INFO"] or "/" parent_span = propagators.extract(get_header_from_environ, environ) - with tracer.start_span(path_info, parent_span, kind=trace.SpanKind.SERVER) as span: + with tracer.start_span( + path_info, parent_span, kind=trace.SpanKind.SERVER + ) as span: self._add_request_attributes(span, environ) start_response = self._create_start_response(span, start_response)