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 d06e78b052..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 @@ -22,6 +22,7 @@ from requests.sessions import Session +from opentelemetry import propagators from opentelemetry.trace import SpanKind @@ -71,6 +72,8 @@ 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.setdefault("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 a3120c2c0a..28caa77e52 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 propagators, 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 = propagators.extract(get_header_from_environ, environ) - with tracer.start_span(path_info, 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) @@ -99,3 +100,18 @@ def __call__(self, environ, start_response): finally: if hasattr(iterable, "close"): iterable.close() + + +def get_header_from_environ( + environ: dict, header_name: 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("-", "_") + value = environ.get(environ_key) + if value: + return [value] + return [] diff --git a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py index 73bba50452..52cffc051a 100644 --- a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py +++ b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py @@ -126,7 +126,7 @@ def validate_response(self, response, error=None): # Verify that start_span has been called self.start_span.assert_called_once_with( - "/", kind=trace_api.SpanKind.SERVER + "/", trace_api.INVALID_SPAN_CONTEXT, kind=trace_api.SpanKind.SERVER ) def test_basic_wsgi_call(self): diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py index f3823a86d1..35bdfbb3fe 100644 --- a/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py +++ b/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py @@ -17,8 +17,10 @@ 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[[typing.Type[_T], str, str], None] +Getter = typing.Callable[[typing.Type[_T], str], typing.List[str]] class HTTPTextFormat(abc.ABC): @@ -70,7 +72,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 +95,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. 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..575644a91f --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py @@ -0,0 +1,39 @@ +# 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 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[_T], _carrier: _T + ) -> trace.SpanContext: + return trace.INVALID_SPAN_CONTEXT + + def inject( + self, + context: trace.SpanContext, + 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 new file mode 100644 index 0000000000..aa59669407 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/propagators/__init__.py @@ -0,0 +1,70 @@ +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[_T], carrier: _T +) -> 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[_T], + carrier: _T, +) -> 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 9568f270c9..229acdfb43 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 propagators, 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 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 # 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..d33e2b9ac0 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,45 @@ 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"}: + self.assertIn(required_header, headers) + self.assertEqual( + 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),