From 5f84c2babbc2656e5a58d4d82f73838166e7c4ff Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Thu, 7 Nov 2019 15:21:46 -0800 Subject: [PATCH] stopgap PR to start the discussion on context propagation. --- .../context_propagation_example.py | 81 +++++++++++++++++++ .../src/opentelemetry/baggage/__init__.py | 2 + .../context/propagation/httptextformat.py | 8 +- .../distributedcontext/__init__.py | 56 ++++++------- .../src/opentelemetry/propagators/__init__.py | 26 +++--- 5 files changed, 126 insertions(+), 47 deletions(-) create mode 100644 examples/opentelemetry-example-app/src/opentelemetry_example_app/context_propagation_example.py create mode 100644 opentelemetry-api/src/opentelemetry/baggage/__init__.py diff --git a/examples/opentelemetry-example-app/src/opentelemetry_example_app/context_propagation_example.py b/examples/opentelemetry-example-app/src/opentelemetry_example_app/context_propagation_example.py new file mode 100644 index 00000000000..fe87626f7d6 --- /dev/null +++ b/examples/opentelemetry-example-app/src/opentelemetry_example_app/context_propagation_example.py @@ -0,0 +1,81 @@ +# 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. +# +""" +This module serves as an example for baggage, which exists +to pass application-defined key-value pairs from service to service. +""" +import flask +import requests + +import opentelemetry.ext.http_requests +from opentelemetry import propagators, trace +from opentelemetry import baggage +from opnetelemetry.context import Context +from opentelemetry.ext.wsgi import OpenTelemetryMiddleware +from opentelemetry.sdk.context.propagation.b3_format import B3Format +from opentelemetry.sdk.trace import Tracer + + +def configure_opentelemetry(flask_app: flask.Flask): + """Configure a flask application to use OpenTelemetry. + + This activates the specific components: + + * sets tracer to the SDK's Tracer + * enables requests integration on the Tracer + * uses a WSGI middleware to enable configuration + + TODO: + + * processors? + * exporters? + """ + # Start by configuring all objects required to ensure + # a complete end to end workflow. + # the preferred implementation of these objects must be set, + # as the opentelemetry-api defines the interface with a no-op + # implementation. + trace.set_preferred_tracer_implementation(lambda _: Tracer()) + # extractors and injectors are now separate, as it could be possible + # to want different behavior for those (e.g. don't propagate because of external services) + # + # the current configuration will only propagate w3c/correlationcontext + # and baggage. One would have to add other propagators to handle + # things such as w3c/tracecontext + propagator_list = [CorrelationContextFormat(), BaggageFormat()] + + propagators.set_http_extractors(propagator_list) + propagators.set_http_injectors(propagator_list) + + # 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. + opentelemetry.ext.http_requests.enable(trace.tracer()) + flask_app.wsgi_app = OpenTelemetryMiddleware(flask_app.wsgi_app) + + +app = flask.Flask(__name__) + + +@app.route("/") +def hello(): + # extract a baggage header + original_service = baggage.get(Context, "original-service") + # add a new one + baggage.set(Context, "environment", "foo") + return "hello" + + +configure_opentelemetry(app) diff --git a/opentelemetry-api/src/opentelemetry/baggage/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/__init__.py new file mode 100644 index 00000000000..96623a8acab --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/baggage/__init__.py @@ -0,0 +1,2 @@ +class Baggage: + """""" diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py index 9b6098a9a42..b90b4c9ebaa 100644 --- a/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py +++ b/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py @@ -16,6 +16,7 @@ import typing from opentelemetry.trace import SpanContext +from opentelemetry.context import BaseRuntimeContext _T = typing.TypeVar("_T") @@ -72,7 +73,7 @@ def example_route(): @abc.abstractmethod def extract( - self, get_from_carrier: Getter[_T], carrier: _T + context: BaseRuntimeContext, get_from_carrier: Getter[_T], carrier: _T ) -> SpanContext: """Create a SpanContext from values in the carrier. @@ -95,7 +96,10 @@ def extract( @abc.abstractmethod def inject( - self, context: SpanContext, set_in_carrier: Setter[_T], carrier: _T + self, + context: BaseRuntimeContext, + set_in_carrier: Setter[_T], + carrier: _T, ) -> None: """Inject values from a SpanContext into a carrier. diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py index 38ef3739b90..80001d2288a 100644 --- a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py +++ b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py @@ -15,7 +15,7 @@ import itertools import string import typing -from contextlib import contextmanager +from opentelemetry.context import BaseRuntimeContext PRINTABLE = frozenset( itertools.chain( @@ -81,45 +81,35 @@ def __init__( class DistributedContext: """A container for distributed context entries""" + KEY = "DistributedContext" + def __init__(self, entries: typing.Iterable[Entry]) -> None: self._container = {entry.key: entry for entry in entries} - def get_entries(self) -> typing.Iterable[Entry]: + @classmethod + def set_value( + cls, context: BaseRuntimeContext, entry_list: typing.Iterable[Entry] + ): + distributed_context = getattr(context, cls.KEY, {}) + for entry in entry_list: + distributed_context[entry.key] = entry + + @classmethod + def get_entries( + cls, context: BaseRuntimeContext + ) -> typing.Iterable[Entry]: """Returns an immutable iterator to entries.""" - return self._container.values() + return getattr(context, cls.KEY, {}).values() - def get_entry_value(self, key: EntryKey) -> typing.Optional[EntryValue]: + @classmethod + def get_entry_value( + cls, context: BaseRuntimeContext, key: EntryKey + ) -> typing.Optional[EntryValue]: """Returns the entry associated with a key or None Args: key: the key with which to perform a lookup """ - if key in self._container: - return self._container[key].value - return None - - -class DistributedContextManager: - def get_current_context(self) -> typing.Optional[DistributedContext]: - """Gets the current DistributedContext. - - Returns: - A DistributedContext instance representing the current context. - """ - - @contextmanager # type: ignore - def use_context( - self, context: DistributedContext - ) -> typing.Iterator[DistributedContext]: - """Context manager for controlling a DistributedContext lifetime. - - Set the context as the active DistributedContext. - - On exiting, the context manager will restore the parent - DistributedContext. - - Args: - context: A DistributedContext instance to make current. - """ - # pylint: disable=no-self-use - yield context + container = getattr(context, cls.KEY, {}) + if key in container: + return container[key].value diff --git a/opentelemetry-api/src/opentelemetry/propagators/__init__.py b/opentelemetry-api/src/opentelemetry/propagators/__init__.py index bb75d84c3a4..190c77dc98e 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagators/__init__.py @@ -19,13 +19,14 @@ from opentelemetry.context.propagation.tracecontexthttptextformat import ( TraceContextHTTPTextFormat, ) +from opentelemetry.context import BaseRuntimeContext _T = typing.TypeVar("_T") def extract( get_from_carrier: httptextformat.Getter[_T], carrier: _T -) -> trace.SpanContext: +) -> BaseRuntimeContext: """Load the parent SpanContext from values in the carrier. Using the specified HTTPTextFormatter, the propagator will @@ -45,7 +46,7 @@ def extract( def inject( - tracer: trace.Tracer, + context: BaseRuntimeContext set_in_carrier: httptextformat.Setter[_T], carrier: _T, ) -> None: @@ -67,18 +68,19 @@ def inject( tracer.get_current_span().get_context(), set_in_carrier, carrier ) - -_HTTP_TEXT_FORMAT = ( +_HTTP_TEXT_INJECTORS = [ TraceContextHTTPTextFormat() -) # type: httptextformat.HTTPTextFormat +] # typing.List[httptextformat.HTTPTextFormat] +_HTTP_TEXT_EXTRACTORS = [ + TraceContextHTTPTextFormat() +] # typing.List[httptextformat.HTTPTextFormat] -def get_global_httptextformat() -> httptextformat.HTTPTextFormat: - return _HTTP_TEXT_FORMAT +def set_http_extractors(extractor_list: typing.List[httptextformat.HTTPTextFormat]) -> None: + global _HTTP_TEXT_EXTRACTORS # pylint:disable=global-statement + _HTTP_TEXT_EXTRACTORS = extractor_list -def set_global_httptextformat( - http_text_format: httptextformat.HTTPTextFormat, -) -> None: - global _HTTP_TEXT_FORMAT # pylint:disable=global-statement - _HTTP_TEXT_FORMAT = http_text_format +def set_http_injectors(extractor_list: typing.List[httptextformat.HTTPTextFormat]) -> None: + global _HTTP_TEXT_INJECTORS # pylint:disable=global-statement + _HTTP_TEXT_INJECTORS = injector_list \ No newline at end of file