From 1500bec563a0a092a31cf5423290ba4271663292 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 3 Feb 2020 15:37:42 -0800 Subject: [PATCH 01/56] Context API This change implements the Context API portion of OTEP #66. The CorrelationContext API and Propagation API changes will come in future PRs. We're leveraging entrypoints to support other implementations of the Context API if/when necessary. For backwards compatibility, this change uses aiocontextvars for Python versions older than 3.7. Signed-off-by: Alex Boten --- .../ext/http_requests/__init__.py | 4 +- opentelemetry-api/setup.py | 6 + .../src/opentelemetry/context/__init__.py | 201 ++++++------------ .../opentelemetry/context/async_context.py | 45 ---- .../src/opentelemetry/context/base_context.py | 130 ----------- .../src/opentelemetry/context/context.py | 50 +++++ .../context/contextvars_context.py | 133 ++++++++++++ .../opentelemetry/context/default_context.py | 37 ++++ .../context/thread_local_context.py | 45 ---- .../distributedcontext/__init__.py | 25 ++- .../trace/propagation/__init__.py | 56 +++++ opentelemetry-sdk/setup.py | 9 +- .../sdk/distributedcontext/__init__.py | 27 +-- .../src/opentelemetry/sdk/trace/__init__.py | 18 +- .../sdk/trace/export/__init__.py | 7 +- opentelemetry-sdk/tests/conftest.py | 23 ++ tox.ini | 2 +- 17 files changed, 425 insertions(+), 393 deletions(-) delete mode 100644 opentelemetry-api/src/opentelemetry/context/async_context.py delete mode 100644 opentelemetry-api/src/opentelemetry/context/base_context.py create mode 100644 opentelemetry-api/src/opentelemetry/context/context.py create mode 100644 opentelemetry-api/src/opentelemetry/context/contextvars_context.py create mode 100644 opentelemetry-api/src/opentelemetry/context/default_context.py delete mode 100644 opentelemetry-api/src/opentelemetry/context/thread_local_context.py create mode 100644 opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py create mode 100644 opentelemetry-sdk/tests/conftest.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 4f5a18cf9e..e06a3e5fd3 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 @@ -23,7 +23,7 @@ from requests.sessions import Session from opentelemetry import propagators -from opentelemetry.context import Context +from opentelemetry.context import get_value from opentelemetry.ext.http_requests.version import __version__ from opentelemetry.trace import SpanKind @@ -54,7 +54,7 @@ def enable(tracer_source): @functools.wraps(wrapped) def instrumented_request(self, method, url, *args, **kwargs): - if Context.suppress_instrumentation: + if get_value("suppress_instrumentation"): return wrapped(self, method, url, *args, **kwargs) # See diff --git a/opentelemetry-api/setup.py b/opentelemetry-api/setup.py index ee8adf26ae..cabf539ef7 100644 --- a/opentelemetry-api/setup.py +++ b/opentelemetry-api/setup.py @@ -56,4 +56,10 @@ "/tree/master/opentelemetry-api" ), zip_safe=False, + entry_points={ + "opentelemetry_context": [ + "default_context = " + "opentelemetry.context.default_context:DefaultContext", + ] + }, ) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 43a7722f88..d274b1ab27 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -12,141 +12,68 @@ # See the License for the specific language governing permissions and # limitations under the License. +import typing +from os import environ -""" -The OpenTelemetry context module provides abstraction layer on top of -thread-local storage and contextvars. The long term direction is to switch to -contextvars provided by the Python runtime library. - -A global object ``Context`` is provided to access all the context related -functionalities:: - - >>> from opentelemetry.context import Context - >>> Context.foo = 1 - >>> Context.foo = 2 - >>> Context.foo - 2 - -When explicit thread is used, a helper function -``Context.with_current_context`` can be used to carry the context across -threads:: - - from threading import Thread - from opentelemetry.context import Context - - def work(name): - print('Entering worker:', Context) - Context.operation_id = name - print('Exiting worker:', Context) - - if __name__ == '__main__': - print('Main thread:', Context) - Context.operation_id = 'main' - - print('Main thread:', Context) - - # by default context is not propagated to worker thread - thread = Thread(target=work, args=('foo',)) - thread.start() - thread.join() - - print('Main thread:', Context) - - # user can propagate context explicitly - thread = Thread( - target=Context.with_current_context(work), - args=('bar',), - ) - thread.start() - thread.join() - - print('Main thread:', Context) - -Here goes another example using thread pool:: - - import time - import threading - - from multiprocessing.dummy import Pool as ThreadPool - from opentelemetry.context import Context - - _console_lock = threading.Lock() - - def println(msg): - with _console_lock: - print(msg) - - def work(name): - println('Entering worker[{}]: {}'.format(name, Context)) - Context.operation_id = name - time.sleep(0.01) - println('Exiting worker[{}]: {}'.format(name, Context)) - - if __name__ == "__main__": - println('Main thread: {}'.format(Context)) - Context.operation_id = 'main' - pool = ThreadPool(2) # create a thread pool with 2 threads - pool.map(Context.with_current_context(work), [ - 'bear', - 'cat', - 'dog', - 'horse', - 'rabbit', - ]) - pool.close() - pool.join() - println('Main thread: {}'.format(Context)) - -Here goes a simple demo of how async could work in Python 3.7+:: - - import asyncio - - from opentelemetry.context import Context - - class Span(object): - def __init__(self, name): - self.name = name - self.parent = Context.current_span - - def __repr__(self): - return ('{}(name={}, parent={})' - .format( - type(self).__name__, - self.name, - self.parent, - )) - - async def __aenter__(self): - Context.current_span = self - - async def __aexit__(self, exc_type, exc, tb): - Context.current_span = self.parent - - async def main(): - print(Context) - async with Span('foo'): - print(Context) - await asyncio.sleep(0.1) - async with Span('bar'): - print(Context) - await asyncio.sleep(0.1) - print(Context) - await asyncio.sleep(0.1) - print(Context) - - if __name__ == '__main__': - asyncio.run(main()) -""" - -from .base_context import BaseRuntimeContext - -__all__ = ["Context"] - -try: - from .async_context import AsyncRuntimeContext - - Context = AsyncRuntimeContext() # type: BaseRuntimeContext -except ImportError: - from .thread_local_context import ThreadLocalRuntimeContext - - Context = ThreadLocalRuntimeContext() +from pkg_resources import iter_entry_points + +from opentelemetry.context.context import Context + +# FIXME use a better implementation of a configuration manager to avoid having +# to get configuration values straight from environment variables +_CONTEXT = { + entry_point.name: entry_point.load() + for entry_point in (iter_entry_points("opentelemetry_context")) +}[ + environ.get("OPENTELEMETRY_CONTEXT", "default_context") +]() # type: Context + + +def _copy_context(context: typing.Optional[Context]) -> Context: + if context: + return context.copy() + return get_current().copy() + + +def create_key(key: str) -> "object": + # FIXME Implement this + raise NotImplementedError + + +def get_value(key: str, context: typing.Optional[Context] = None) -> "object": + if context: + return context.get_value(key) + return get_current().get_value(key) + + +def set_value( + key: str, value: "object", context: typing.Optional[Context] = None +) -> Context: + new_context = _copy_context(context) + new_context.set_value(key, value) + return new_context + + +def remove_value(context: Context, key: str) -> Context: + new_context = _copy_context(context) + new_context.remove_value(key) + return new_context + + +def get_current() -> Context: + return _CONTEXT + + +def set_current(context: Context) -> None: + global _CONTEXT + _CONTEXT = context + + +__all__ = [ + "get_value", + "set_value", + "remove_value", + "get_current", + "set_current", + "Context", +] diff --git a/opentelemetry-api/src/opentelemetry/context/async_context.py b/opentelemetry-api/src/opentelemetry/context/async_context.py deleted file mode 100644 index 267059fb31..0000000000 --- a/opentelemetry-api/src/opentelemetry/context/async_context.py +++ /dev/null @@ -1,45 +0,0 @@ -# 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. - -try: - from contextvars import ContextVar -except ImportError: - pass -else: - import typing # pylint: disable=unused-import - from . import base_context - - class AsyncRuntimeContext(base_context.BaseRuntimeContext): - class Slot(base_context.BaseRuntimeContext.Slot): - def __init__(self, name: str, default: object): - # pylint: disable=super-init-not-called - self.name = name - self.contextvar = ContextVar(name) # type: ContextVar[object] - self.default = base_context.wrap_callable( - default - ) # type: typing.Callable[..., object] - - def clear(self) -> None: - self.contextvar.set(self.default()) - - def get(self) -> object: - try: - return self.contextvar.get() - except LookupError: - value = self.default() - self.set(value) - return value - - def set(self, value: object) -> None: - self.contextvar.set(value) diff --git a/opentelemetry-api/src/opentelemetry/context/base_context.py b/opentelemetry-api/src/opentelemetry/context/base_context.py deleted file mode 100644 index 99d6869dd5..0000000000 --- a/opentelemetry-api/src/opentelemetry/context/base_context.py +++ /dev/null @@ -1,130 +0,0 @@ -# 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 threading -import typing -from contextlib import contextmanager - - -def wrap_callable(target: "object") -> typing.Callable[[], object]: - if callable(target): - return target - return lambda: target - - -class BaseRuntimeContext: - class Slot: - def __init__(self, name: str, default: "object"): - raise NotImplementedError - - def clear(self) -> None: - raise NotImplementedError - - def get(self) -> "object": - raise NotImplementedError - - def set(self, value: "object") -> None: - raise NotImplementedError - - _lock = threading.Lock() - _slots = {} # type: typing.Dict[str, 'BaseRuntimeContext.Slot'] - - @classmethod - def clear(cls) -> None: - """Clear all slots to their default value.""" - keys = cls._slots.keys() - for name in keys: - slot = cls._slots[name] - slot.clear() - - @classmethod - def register_slot( - cls, name: str, default: "object" = None - ) -> "BaseRuntimeContext.Slot": - """Register a context slot with an optional default value. - - :type name: str - :param name: The name of the context slot. - - :type default: object - :param name: The default value of the slot, can be a value or lambda. - - :returns: The registered slot. - """ - with cls._lock: - if name not in cls._slots: - cls._slots[name] = cls.Slot(name, default) - return cls._slots[name] - - def apply(self, snapshot: typing.Dict[str, "object"]) -> None: - """Set the current context from a given snapshot dictionary""" - - for name in snapshot: - setattr(self, name, snapshot[name]) - - def snapshot(self) -> typing.Dict[str, "object"]: - """Return a dictionary of current slots by reference.""" - - keys = self._slots.keys() - return dict((n, self._slots[n].get()) for n in keys) - - def __repr__(self) -> str: - return "{}({})".format(type(self).__name__, self.snapshot()) - - def __getattr__(self, name: str) -> "object": - if name not in self._slots: - self.register_slot(name, None) - slot = self._slots[name] - return slot.get() - - def __setattr__(self, name: str, value: "object") -> None: - if name not in self._slots: - self.register_slot(name, None) - slot = self._slots[name] - slot.set(value) - - def __getitem__(self, name: str) -> "object": - return self.__getattr__(name) - - def __setitem__(self, name: str, value: "object") -> None: - self.__setattr__(name, value) - - @contextmanager # type: ignore - def use(self, **kwargs: typing.Dict[str, object]) -> typing.Iterator[None]: - snapshot = {key: self[key] for key in kwargs} - for key in kwargs: - self[key] = kwargs[key] - yield - for key in kwargs: - self[key] = snapshot[key] - - def with_current_context( - self, func: typing.Callable[..., "object"] - ) -> typing.Callable[..., "object"]: - """Capture the current context and apply it to the provided func. - """ - - caller_context = self.snapshot() - - def call_with_current_context( - *args: "object", **kwargs: "object" - ) -> "object": - try: - backup_context = self.snapshot() - self.apply(caller_context) - return func(*args, **kwargs) - finally: - self.apply(backup_context) - - return call_with_current_context diff --git a/opentelemetry-api/src/opentelemetry/context/context.py b/opentelemetry-api/src/opentelemetry/context/context.py new file mode 100644 index 0000000000..38380b813f --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/context/context.py @@ -0,0 +1,50 @@ +# 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 abc import ABC, abstractmethod +from contextlib import contextmanager +from copy import deepcopy +from typing import Dict, Iterator + + +class Context(ABC): + @abstractmethod + def set_value(self, key: str, value: "object") -> None: + """Set a value in this context""" + + @abstractmethod + def get_value(self, key: str) -> "object": + """Get a value from this context""" + + @abstractmethod + def remove_value(self, key: str) -> None: + """Remove a value from this context""" + + @contextmanager + def use(self, **kwargs: Dict[str, object]) -> Iterator[None]: + snapshot = {key: self.get_value(key) for key in kwargs} + for key in kwargs: + self.set_value(key, kwargs[key]) + yield + for key in kwargs: + self.set_value(key, snapshot[key]) + + def copy(self) -> "Context": + """Return a copy of this context""" + + return deepcopy(self) + + +__all__ = ["Context"] diff --git a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py new file mode 100644 index 0000000000..7be2f137d8 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py @@ -0,0 +1,133 @@ +# 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 +from contextvars import ContextVar +from copy import copy +from sys import version_info + +from opentelemetry.context.context import Context + +if (3, 5, 3) <= version_info < (3, 7): + import aiocontextvars + +elif (3, 4) < version_info <= (3, 5, 2): + + # This is copied and pasted from: + # https://github.com/fantix/aiocontextvars/issues/88#issuecomment-522276290 + + import asyncio + import asyncio.coroutines + import asyncio.futures + import concurrent.futures + + if not hasattr(asyncio, "_get_running_loop"): + # noinspection PyCompatibility + import asyncio.events + from threading import local as threading_local + + if not hasattr(asyncio.events, "_get_running_loop"): + + class _RunningLoop(threading_local): + _loop = None + + _running_loop = _RunningLoop() + + def _get_running_loop(): + return _running_loop._loop + + def set_running_loop(loop): # noqa: F811 + _running_loop._loop = loop + + def _get_event_loop(): + current_loop = _get_running_loop() + if current_loop is not None: + return current_loop + return asyncio.events.get_event_loop_policy().get_event_loop() + + asyncio.events.get_event_loop = _get_event_loop + asyncio.events._get_running_loop = _get_running_loop + asyncio.events._set_running_loop = set_running_loop + + asyncio._get_running_loop = asyncio.events._get_running_loop + asyncio._set_running_loop = asyncio.events._set_running_loop + + # It needs only to be imported to activate the patching of the contextvars + # backport (see the comment in setup.py) + # noinspection PyUnresolvedReferences + import aiocontextvars + + def _run_coroutine_threadsafe(coro, loop): + """ + Patch to create task in the same thread instead of in the callback. + This ensures that contextvars get copied. Python 3.7 copies contextvars + without this. + """ + if not asyncio.coroutines.iscoroutine(coro): + raise TypeError("A coroutine object is required") + future = concurrent.futures.Future() + task = asyncio.ensure_future(coro, loop=loop) + + def callback() -> None: + try: + # noinspection PyProtectedMember,PyUnresolvedReferences + asyncio.futures._chain_future(task, future) + except Exception as exc: + if future.set_running_or_notify_cancel(): + future.set_exception(exc) + raise + + loop.call_soon_threadsafe(callback) + return future + + asyncio.run_coroutine_threadsafe = _run_coroutine_threadsafe + + +class ContextVarsContext(Context): + def __init__(self) -> None: + self._contextvars = {} # type: typing.Dict[str, ContextVar[object]] + + def set_value(self, key: str, value: "object") -> None: + """Set a value in this context""" + if key not in self._contextvars.keys(): + self._contextvars[key] = ContextVar(key) + + self._contextvars[key].set(value) + + def get_value(self, key: str) -> "object": + """Get a value from this context""" + if key in self._contextvars: + try: + return self._contextvars[key].get() + except LookupError: + pass + return None + + def remove_value(self, key: str) -> None: + """Remove a value from this context""" + if key in self._contextvars.keys(): + self._contextvars.pop(key) + + def copy(self) -> Context: + """Return a copy of this context""" + + context_copy = ContextVarsContext() + + for key, value in self._contextvars.items(): + context_copy._contextvars[key] = ContextVar(key) + context_copy._contextvars[key].set(copy(value)) + + return context_copy + + +__all__ = ["ContextVarsContext"] diff --git a/opentelemetry-api/src/opentelemetry/context/default_context.py b/opentelemetry-api/src/opentelemetry/context/default_context.py new file mode 100644 index 0000000000..aa169ff9c8 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/context/default_context.py @@ -0,0 +1,37 @@ +# 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.context import Context + + +class DefaultContext(Context): + def __init__(self): + self.values = {} + + def set_value(self, key: str, value: "object") -> None: + """Set a value in this context""" + self.values[key] = value + + def get_value(self, key: str) -> "object": + """Get a value from this context""" + return self.values.get(key) + + def remove_value(self, key: str) -> None: + """Remove a value from this context""" + if key in self.values: + self.values.pop(key) + + +__all__ = ["DefaultContext"] diff --git a/opentelemetry-api/src/opentelemetry/context/thread_local_context.py b/opentelemetry-api/src/opentelemetry/context/thread_local_context.py deleted file mode 100644 index b60914f846..0000000000 --- a/opentelemetry-api/src/opentelemetry/context/thread_local_context.py +++ /dev/null @@ -1,45 +0,0 @@ -# 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 threading -import typing # pylint: disable=unused-import - -from . import base_context - - -class ThreadLocalRuntimeContext(base_context.BaseRuntimeContext): - class Slot(base_context.BaseRuntimeContext.Slot): - _thread_local = threading.local() - - def __init__(self, name: str, default: "object"): - # pylint: disable=super-init-not-called - self.name = name - self.default = base_context.wrap_callable( - default - ) # type: typing.Callable[..., object] - - def clear(self) -> None: - setattr(self._thread_local, self.name, self.default()) - - def get(self) -> "object": - try: - got = getattr(self._thread_local, self.name) # type: object - return got - except AttributeError: - value = self.default() - self.set(value) - return value - - def set(self, value: "object") -> None: - setattr(self._thread_local, self.name, value) diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py index 38ef3739b9..7cd68a90a9 100644 --- a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py +++ b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py @@ -17,6 +17,8 @@ import typing from contextlib import contextmanager +from opentelemetry.context import Context, get_current + PRINTABLE = frozenset( itertools.chain( string.ascii_letters, string.digits, string.punctuation, " " @@ -100,7 +102,9 @@ def get_entry_value(self, key: EntryKey) -> typing.Optional[EntryValue]: class DistributedContextManager: - def get_current_context(self) -> typing.Optional[DistributedContext]: + def get_current_context( + self, context: typing.Optional[Context] = None + ) -> typing.Optional[DistributedContext]: """Gets the current DistributedContext. Returns: @@ -123,3 +127,22 @@ def use_context( """ # pylint: disable=no-self-use yield context + + +_DISTRIBUTED_CONTEXT_KEY = "DistributedContext" + + +def distributed_context_from_context( + context: typing.Optional[Context] = None, +) -> DistributedContext: + if context: + return context.get_value(_DISTRIBUTED_CONTEXT_KEY) + return get_current().get_value(_DISTRIBUTED_CONTEXT_KEY) + + +def with_distributed_context( + dctx: DistributedContext, context: typing.Optional[Context] = None +) -> Context: + if context: + return context.set_value(_DISTRIBUTED_CONTEXT_KEY, dctx) + return get_current().set_value(_DISTRIBUTED_CONTEXT_KEY, dctx) diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py new file mode 100644 index 0000000000..f4b31523e7 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py @@ -0,0 +1,56 @@ +# 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 typing import Optional + +from opentelemetry.context import Context, get_current +from opentelemetry.trace import INVALID_SPAN_CONTEXT, Span, SpanContext + +_SPAN_CONTEXT_KEY = "extracted-span-context" +_SPAN_KEY = "current-span" + + +def span_context_from_context( + context: Optional[Context] = None, +) -> SpanContext: + span = span_from_context(context=context) + if span: + return span.get_context() + if context: + sc = context.get_value(_SPAN_CONTEXT_KEY) + else: + sc = get_current().get_value(_SPAN_CONTEXT_KEY) + if sc: + return sc + + return INVALID_SPAN_CONTEXT + + +def with_span_context( + span_context: SpanContext, context: Optional[Context] = None +) -> Context: + if context: + context.set_value(_SPAN_CONTEXT_KEY, span_context) + return get_current().set_value(_SPAN_CONTEXT_KEY, span_context) + + +def span_from_context(context: Optional[Context] = None) -> Span: + if context: + context.get_value(_SPAN_KEY) + return get_current().get_value(_SPAN_KEY) + + +def with_span(span: Span, context: Optional[Context] = None) -> Context: + if context: + return context.set_value(_SPAN_KEY, span) + return get_current().set_value(_SPAN_KEY, span) diff --git a/opentelemetry-sdk/setup.py b/opentelemetry-sdk/setup.py index cbfb0f075d..9136c88a09 100644 --- a/opentelemetry-sdk/setup.py +++ b/opentelemetry-sdk/setup.py @@ -44,7 +44,7 @@ include_package_data=True, long_description=open("README.rst").read(), long_description_content_type="text/x-rst", - install_requires=["opentelemetry-api==0.4.dev0"], + install_requires=["opentelemetry-api==0.4.dev0", "aiocontextvars"], extras_require={}, license="Apache-2.0", package_dir={"": "src"}, @@ -56,4 +56,11 @@ "/tree/master/opentelemetry-sdk" ), zip_safe=False, + entry_points={ + "opentelemetry_context": [ + "contextvars_context = " + "opentelemetry.context.contextvars_context:" + "ContextVarsContext" + ] + }, ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/__init__.py index a20cbf8963..7a0a66a8a9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/__init__.py @@ -16,33 +16,27 @@ from contextlib import contextmanager from opentelemetry import distributedcontext as dctx_api -from opentelemetry.context import Context +from opentelemetry.context import Context, get_value, set_value +from opentelemetry.distributedcontext import ( + distributed_context_from_context, + with_distributed_context, +) class DistributedContextManager(dctx_api.DistributedContextManager): """See `opentelemetry.distributedcontext.DistributedContextManager` - Args: - name: The name of the context manager """ - def __init__(self, name: str = "") -> None: - if name: - slot_name = "DistributedContext.{}".format(name) - else: - slot_name = "DistributedContext" - - self._current_context = Context.register_slot(slot_name) - def get_current_context( - self, + self, context: typing.Optional[Context] = None ) -> typing.Optional[dctx_api.DistributedContext]: """Gets the current DistributedContext. Returns: A DistributedContext instance representing the current context. """ - return self._current_context.get() + return distributed_context_from_context(context=context) @contextmanager def use_context( @@ -58,9 +52,10 @@ def use_context( Args: context: A DistributedContext instance to make current. """ - snapshot = self._current_context.get() - self._current_context.set(context) + snapshot = distributed_context_from_context() + with_distributed_context(context) + try: yield context finally: - self._current_context.set(snapshot) + with_distributed_context(snapshot) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 9829c8b33b..6c239757aa 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -23,10 +23,11 @@ from typing import Iterator, Optional, Sequence, Tuple, Type from opentelemetry import trace as trace_api -from opentelemetry.context import Context +from opentelemetry.context import Context, get_value from opentelemetry.sdk import util from opentelemetry.sdk.util import BoundedDict, BoundedList from opentelemetry.trace import SpanContext, sampling +from opentelemetry.trace.propagation import span_from_context, with_span from opentelemetry.trace.status import Status, StatusCanonicalCode from opentelemetry.util import time_ns, types @@ -528,16 +529,12 @@ def use_span( ) -> Iterator[trace_api.Span]: """See `opentelemetry.trace.Tracer.use_span`.""" try: - span_snapshot = self.source.get_current_span() - self.source._current_span_slot.set( # pylint:disable=protected-access - span - ) + span_snapshot = span_from_context() + with_span(span) try: yield span finally: - self.source._current_span_slot.set( # pylint:disable=protected-access - span_snapshot - ) + with_span(span_snapshot) finally: if end_on_exit: span.end() @@ -551,7 +548,6 @@ def __init__( ): # TODO: How should multiple TracerSources behave? Should they get their own contexts? # This could be done by adding `str(id(self))` to the slot name. - self._current_span_slot = Context.register_slot("current_span") self._active_span_processor = MultiSpanProcessor() self.sampler = sampler self._atexit_handler = None @@ -573,8 +569,8 @@ def get_tracer( ), ) - def get_current_span(self) -> Span: - return self._current_span_slot.get() + def get_current_span(self, context: Optional[Context] = None) -> Span: + return span_from_context(context=context) def add_span_processor(self, span_processor: SpanProcessor) -> None: """Registers a new :class:`SpanProcessor` for this `TracerSource`. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index b70fb01019..4ffbe01697 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -18,7 +18,7 @@ import typing from enum import Enum -from opentelemetry.context import Context +from opentelemetry.context import get_current from opentelemetry.util import time_ns from .. import Span, SpanProcessor @@ -73,7 +73,7 @@ def on_start(self, span: Span) -> None: pass def on_end(self, span: Span) -> None: - with Context.use(suppress_instrumentation=True): + with get_current().use(suppress_instrumentation=True): try: self.span_exporter.export((span,)) # pylint: disable=broad-except @@ -182,7 +182,7 @@ def export(self) -> None: while idx < self.max_export_batch_size and self.queue: self.spans_list[idx] = self.queue.pop() idx += 1 - with Context.use(suppress_instrumentation=True): + with get_current().use(suppress_instrumentation=True): try: # Ignore type b/c the Optional[None]+slicing is too "clever" # for mypy @@ -192,7 +192,6 @@ def export(self) -> None: # pylint: disable=broad-except except Exception: logger.exception("Exception while exporting Span batch.") - # clean up list for index in range(idx): self.spans_list[index] = None diff --git a/opentelemetry-sdk/tests/conftest.py b/opentelemetry-sdk/tests/conftest.py new file mode 100644 index 0000000000..9ab3bd4338 --- /dev/null +++ b/opentelemetry-sdk/tests/conftest.py @@ -0,0 +1,23 @@ +# 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 os import environ + + +def pytest_sessionstart(session): + environ["OPENTELEMETRY_CONTEXT"] = "contextvars_context" + + +def pytest_sessionfinish(session): + environ.pop("OPENTELEMETRY_CONTEXT") diff --git a/tox.ini b/tox.ini index d077d07893..ec575fcbab 100644 --- a/tox.ini +++ b/tox.ini @@ -81,7 +81,7 @@ commands_pre = jaeger: pip install {toxinidir}/opentelemetry-sdk jaeger: pip install {toxinidir}/ext/opentelemetry-ext-jaeger opentracing-shim: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/ext/opentelemetry-ext-opentracing-shim - zipkin: pip install {toxinidir}/ext/opentelemetry-ext-zipkin + zipkin: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-sdk {toxinidir}/ext/opentelemetry-ext-zipkin ; In order to get a healthy coverage report, ; we have to install packages in editable mode. From 3bdf1c8f3bf02b5f33acc64e28260b94a637f0a0 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Mon, 3 Feb 2020 16:03:25 -0800 Subject: [PATCH 02/56] Lint fixes Signed-off-by: Alex Boten --- opentelemetry-api/src/opentelemetry/context/__init__.py | 2 +- .../src/opentelemetry/context/contextvars_context.py | 7 ++++--- opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 1 + opentelemetry-sdk/tests/conftest.py | 2 ++ 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index d274b1ab27..37846f7dde 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -65,7 +65,7 @@ def get_current() -> Context: def set_current(context: Context) -> None: - global _CONTEXT + global _CONTEXT # pylint: disable=global-statement _CONTEXT = context diff --git a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py index 7be2f137d8..b9e1b1425f 100644 --- a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py +++ b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py @@ -19,7 +19,7 @@ from opentelemetry.context.context import Context if (3, 5, 3) <= version_info < (3, 7): - import aiocontextvars + import aiocontextvars # pylint:disable=unused-import elif (3, 4) < version_info <= (3, 5, 2): @@ -33,6 +33,7 @@ if not hasattr(asyncio, "_get_running_loop"): # noinspection PyCompatibility + # pylint:disable=protected-access import asyncio.events from threading import local as threading_local @@ -81,6 +82,7 @@ def _run_coroutine_threadsafe(coro, loop): def callback() -> None: try: # noinspection PyProtectedMember,PyUnresolvedReferences + # pylint:disable=protected-access asyncio.futures._chain_future(task, future) except Exception as exc: if future.set_running_or_notify_cancel(): @@ -124,8 +126,7 @@ def copy(self) -> Context: context_copy = ContextVarsContext() for key, value in self._contextvars.items(): - context_copy._contextvars[key] = ContextVar(key) - context_copy._contextvars[key].set(copy(value)) + context_copy.set_value(key, copy(value)) return context_copy diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 6c239757aa..5052a839c6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -570,6 +570,7 @@ def get_tracer( ) def get_current_span(self, context: Optional[Context] = None) -> Span: + # pylint: disable=no-self-use return span_from_context(context=context) def add_span_processor(self, span_processor: SpanProcessor) -> None: diff --git a/opentelemetry-sdk/tests/conftest.py b/opentelemetry-sdk/tests/conftest.py index 9ab3bd4338..f8aea0e2cc 100644 --- a/opentelemetry-sdk/tests/conftest.py +++ b/opentelemetry-sdk/tests/conftest.py @@ -16,8 +16,10 @@ def pytest_sessionstart(session): + # pylint: disable=unused-argument environ["OPENTELEMETRY_CONTEXT"] = "contextvars_context" def pytest_sessionfinish(session): + # pylint: disable=unused-argument environ.pop("OPENTELEMETRY_CONTEXT") From 569878ec0d18e040e6ddbc8271358ee44a41b331 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 4 Feb 2020 08:02:40 -0800 Subject: [PATCH 03/56] adding support for python 3.4 via thread-local Signed-off-by: Alex Boten --- .../src/opentelemetry/context/__init__.py | 18 +++++++-- .../src/opentelemetry/context/context.py | 2 +- .../opentelemetry/context/default_context.py | 6 +-- .../context/threadlocal_context.py | 37 +++++++++++++++++++ .../distributedcontext/__init__.py | 2 +- .../trace/propagation/__init__.py | 4 +- opentelemetry-sdk/setup.py | 5 ++- opentelemetry-sdk/tests/conftest.py | 7 +++- 8 files changed, 68 insertions(+), 13 deletions(-) create mode 100644 opentelemetry-api/src/opentelemetry/context/threadlocal_context.py diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 37846f7dde..88e28ef84d 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging import typing from os import environ @@ -19,12 +20,21 @@ from opentelemetry.context.context import Context +logger = logging.getLogger(__name__) + # FIXME use a better implementation of a configuration manager to avoid having # to get configuration values straight from environment variables -_CONTEXT = { - entry_point.name: entry_point.load() - for entry_point in (iter_entry_points("opentelemetry_context")) -}[ +available_contexts = {} + +for entry_point in iter_entry_points("opentelemetry_context"): + try: + available_contexts[entry_point.name] = entry_point.load() + except Exception as err: + logger.warning( + "Could not load entry_point %s:%s", entry_point.name, err + ) + +_CONTEXT = available_contexts[ environ.get("OPENTELEMETRY_CONTEXT", "default_context") ]() # type: Context diff --git a/opentelemetry-api/src/opentelemetry/context/context.py b/opentelemetry-api/src/opentelemetry/context/context.py index 38380b813f..6c07d4bd5c 100644 --- a/opentelemetry-api/src/opentelemetry/context/context.py +++ b/opentelemetry-api/src/opentelemetry/context/context.py @@ -32,7 +32,7 @@ def get_value(self, key: str) -> "object": def remove_value(self, key: str) -> None: """Remove a value from this context""" - @contextmanager + @contextmanager # type: ignore def use(self, **kwargs: Dict[str, object]) -> Iterator[None]: snapshot = {key: self.get_value(key) for key in kwargs} for key in kwargs: diff --git a/opentelemetry-api/src/opentelemetry/context/default_context.py b/opentelemetry-api/src/opentelemetry/context/default_context.py index aa169ff9c8..b02871b4bd 100644 --- a/opentelemetry-api/src/opentelemetry/context/default_context.py +++ b/opentelemetry-api/src/opentelemetry/context/default_context.py @@ -11,14 +11,14 @@ # 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.context.context import Context class DefaultContext(Context): - def __init__(self): - self.values = {} + def __init__(self) -> None: + self.values = {} # type: typing.Dict[str, object] def set_value(self, key: str, value: "object") -> None: """Set a value in this context""" diff --git a/opentelemetry-api/src/opentelemetry/context/threadlocal_context.py b/opentelemetry-api/src/opentelemetry/context/threadlocal_context.py new file mode 100644 index 0000000000..6652073e32 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/context/threadlocal_context.py @@ -0,0 +1,37 @@ +# 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 threading +from opentelemetry.context import Context + + +class ThreadLocalContext(Context): + def __init__(self) -> None: + self._thread_local = threading.local() + + def set_value(self, key: str, value: "object") -> None: + """Set a value in this context""" + setattr(self._thread_local, key, value) + + def get_value(self, key: str) -> "object": + """Get a value from this context""" + try: + got = getattr(self._thread_local, key) # type: object + return got + except AttributeError: + return None + + def remove_value(self, key: str) -> None: + """Remove a value from this context""" + delattr(self._thread_local, key) diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py index 7cd68a90a9..570bbb3b65 100644 --- a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py +++ b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py @@ -142,7 +142,7 @@ def distributed_context_from_context( def with_distributed_context( dctx: DistributedContext, context: typing.Optional[Context] = None -) -> Context: +) -> None: if context: return context.set_value(_DISTRIBUTED_CONTEXT_KEY, dctx) return get_current().set_value(_DISTRIBUTED_CONTEXT_KEY, dctx) diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py index f4b31523e7..3bb5d5006e 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py @@ -38,7 +38,7 @@ def span_context_from_context( def with_span_context( span_context: SpanContext, context: Optional[Context] = None -) -> Context: +) -> None: if context: context.set_value(_SPAN_CONTEXT_KEY, span_context) return get_current().set_value(_SPAN_CONTEXT_KEY, span_context) @@ -50,7 +50,7 @@ def span_from_context(context: Optional[Context] = None) -> Span: return get_current().get_value(_SPAN_KEY) -def with_span(span: Span, context: Optional[Context] = None) -> Context: +def with_span(span: Span, context: Optional[Context] = None) -> None: if context: return context.set_value(_SPAN_KEY, span) return get_current().set_value(_SPAN_KEY, span) diff --git a/opentelemetry-sdk/setup.py b/opentelemetry-sdk/setup.py index 9136c88a09..e341a86777 100644 --- a/opentelemetry-sdk/setup.py +++ b/opentelemetry-sdk/setup.py @@ -60,7 +60,10 @@ "opentelemetry_context": [ "contextvars_context = " "opentelemetry.context.contextvars_context:" - "ContextVarsContext" + "ContextVarsContext", + "threadlocal_context = " + "opentelemetry.context.threadlocal_context:" + "ThreadLocalContext", ] }, ) diff --git a/opentelemetry-sdk/tests/conftest.py b/opentelemetry-sdk/tests/conftest.py index f8aea0e2cc..fa2ac2bdaa 100644 --- a/opentelemetry-sdk/tests/conftest.py +++ b/opentelemetry-sdk/tests/conftest.py @@ -13,11 +13,16 @@ # limitations under the License. from os import environ +from sys import version_info def pytest_sessionstart(session): # pylint: disable=unused-argument - environ["OPENTELEMETRY_CONTEXT"] = "contextvars_context" + if (3, 4) <= version_info: + # contextvars are not supported in 3.4, use thread-local storage + environ["OPENTELEMETRY_CONTEXT"] = "threadlocal_context" + else: + environ["OPENTELEMETRY_CONTEXT"] = "contextvars_context" def pytest_sessionfinish(session): From 55c817af43bc76acbc2da78ec12c571686cbb0d7 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 4 Feb 2020 08:34:24 -0800 Subject: [PATCH 04/56] Fix lint Signed-off-by: Alex Boten --- opentelemetry-api/src/opentelemetry/context/__init__.py | 2 +- .../src/opentelemetry/context/threadlocal_context.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 88e28ef84d..261c686f0e 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -29,7 +29,7 @@ for entry_point in iter_entry_points("opentelemetry_context"): try: available_contexts[entry_point.name] = entry_point.load() - except Exception as err: + except Exception as err: # pylint: disable=broad-except logger.warning( "Could not load entry_point %s:%s", entry_point.name, err ) diff --git a/opentelemetry-api/src/opentelemetry/context/threadlocal_context.py b/opentelemetry-api/src/opentelemetry/context/threadlocal_context.py index 6652073e32..c409c3d27e 100644 --- a/opentelemetry-api/src/opentelemetry/context/threadlocal_context.py +++ b/opentelemetry-api/src/opentelemetry/context/threadlocal_context.py @@ -13,6 +13,7 @@ # limitations under the License. import threading + from opentelemetry.context import Context From 746e59141eb0d9119c9ffe80586c71071aa19678 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 4 Feb 2020 09:39:15 -0800 Subject: [PATCH 05/56] review feedback Signed-off-by: Alex Boten --- opentelemetry-api/setup.py | 9 ++++++++- .../src/opentelemetry/context/__init__.py | 16 +++++++++------- .../src/opentelemetry/context/default_context.py | 10 +++++----- .../opentelemetry/distributedcontext/__init__.py | 4 ++-- .../opentelemetry/trace/propagation/__init__.py | 12 ++++++------ opentelemetry-sdk/setup.py | 10 ---------- 6 files changed, 30 insertions(+), 31 deletions(-) diff --git a/opentelemetry-api/setup.py b/opentelemetry-api/setup.py index cabf539ef7..34fe3e4613 100644 --- a/opentelemetry-api/setup.py +++ b/opentelemetry-api/setup.py @@ -58,8 +58,15 @@ zip_safe=False, entry_points={ "opentelemetry_context": [ + "contextvars_context = " + "opentelemetry.context.contextvars_context:" + "ContextVarsContext", + "threadlocal_context = " + "opentelemetry.context.threadlocal_context:" + "ThreadLocalContext", "default_context = " - "opentelemetry.context.default_context:DefaultContext", + "opentelemetry.context.default_context:" + "DefaultContext", ] }, ) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 261c686f0e..634619481d 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -24,23 +24,23 @@ # FIXME use a better implementation of a configuration manager to avoid having # to get configuration values straight from environment variables -available_contexts = {} +_available_contexts = {} for entry_point in iter_entry_points("opentelemetry_context"): try: - available_contexts[entry_point.name] = entry_point.load() + _available_contexts[entry_point.name] = entry_point.load() except Exception as err: # pylint: disable=broad-except logger.warning( "Could not load entry_point %s:%s", entry_point.name, err ) -_CONTEXT = available_contexts[ +_CONTEXT = _available_contexts[ environ.get("OPENTELEMETRY_CONTEXT", "default_context") ]() # type: Context -def _copy_context(context: typing.Optional[Context]) -> Context: - if context: +def _copy_context(context: Context) -> Context: + if context is not None: return context.copy() return get_current().copy() @@ -51,7 +51,7 @@ def create_key(key: str) -> "object": def get_value(key: str, context: typing.Optional[Context] = None) -> "object": - if context: + if context is not None: return context.get_value(key) return get_current().get_value(key) @@ -64,7 +64,9 @@ def set_value( return new_context -def remove_value(context: Context, key: str) -> Context: +def remove_value( + key: str, context: typing.Optional[Context] = None +) -> Context: new_context = _copy_context(context) new_context.remove_value(key) return new_context diff --git a/opentelemetry-api/src/opentelemetry/context/default_context.py b/opentelemetry-api/src/opentelemetry/context/default_context.py index b02871b4bd..22b21fac4e 100644 --- a/opentelemetry-api/src/opentelemetry/context/default_context.py +++ b/opentelemetry-api/src/opentelemetry/context/default_context.py @@ -18,20 +18,20 @@ class DefaultContext(Context): def __init__(self) -> None: - self.values = {} # type: typing.Dict[str, object] + self._values = {} # type: typing.Dict[str, object] def set_value(self, key: str, value: "object") -> None: """Set a value in this context""" - self.values[key] = value + self._values[key] = value def get_value(self, key: str) -> "object": """Get a value from this context""" - return self.values.get(key) + return self._values.get(key) def remove_value(self, key: str) -> None: """Remove a value from this context""" - if key in self.values: - self.values.pop(key) + if key in self._values: + self._values.pop(key) __all__ = ["DefaultContext"] diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py index 570bbb3b65..8474561eb1 100644 --- a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py +++ b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py @@ -135,7 +135,7 @@ def use_context( def distributed_context_from_context( context: typing.Optional[Context] = None, ) -> DistributedContext: - if context: + if context is not None: return context.get_value(_DISTRIBUTED_CONTEXT_KEY) return get_current().get_value(_DISTRIBUTED_CONTEXT_KEY) @@ -143,6 +143,6 @@ def distributed_context_from_context( def with_distributed_context( dctx: DistributedContext, context: typing.Optional[Context] = None ) -> None: - if context: + if context is not None: return context.set_value(_DISTRIBUTED_CONTEXT_KEY, dctx) return get_current().set_value(_DISTRIBUTED_CONTEXT_KEY, dctx) diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py index 3bb5d5006e..57c09b0ef8 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py @@ -24,13 +24,13 @@ def span_context_from_context( context: Optional[Context] = None, ) -> SpanContext: span = span_from_context(context=context) - if span: + if span is not None: return span.get_context() - if context: + if context is not None: sc = context.get_value(_SPAN_CONTEXT_KEY) else: sc = get_current().get_value(_SPAN_CONTEXT_KEY) - if sc: + if sc is not None: return sc return INVALID_SPAN_CONTEXT @@ -39,18 +39,18 @@ def span_context_from_context( def with_span_context( span_context: SpanContext, context: Optional[Context] = None ) -> None: - if context: + if context is not None: context.set_value(_SPAN_CONTEXT_KEY, span_context) return get_current().set_value(_SPAN_CONTEXT_KEY, span_context) def span_from_context(context: Optional[Context] = None) -> Span: - if context: + if context is not None: context.get_value(_SPAN_KEY) return get_current().get_value(_SPAN_KEY) def with_span(span: Span, context: Optional[Context] = None) -> None: - if context: + if context is not None: return context.set_value(_SPAN_KEY, span) return get_current().set_value(_SPAN_KEY, span) diff --git a/opentelemetry-sdk/setup.py b/opentelemetry-sdk/setup.py index e341a86777..d40e9d1b3c 100644 --- a/opentelemetry-sdk/setup.py +++ b/opentelemetry-sdk/setup.py @@ -56,14 +56,4 @@ "/tree/master/opentelemetry-sdk" ), zip_safe=False, - entry_points={ - "opentelemetry_context": [ - "contextvars_context = " - "opentelemetry.context.contextvars_context:" - "ContextVarsContext", - "threadlocal_context = " - "opentelemetry.context.threadlocal_context:" - "ThreadLocalContext", - ] - }, ) From b53bdeae307d2c2839d08ad190ff06b440407800 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 4 Feb 2020 09:50:33 -0800 Subject: [PATCH 06/56] fix docs build Signed-off-by: Alex Boten --- ...text.base_context.rst => opentelemetry.context.context.rst} | 2 +- docs/opentelemetry.context.rst | 2 +- tox.ini | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) rename docs/{opentelemetry.context.base_context.rst => opentelemetry.context.context.rst} (73%) diff --git a/docs/opentelemetry.context.base_context.rst b/docs/opentelemetry.context.context.rst similarity index 73% rename from docs/opentelemetry.context.base_context.rst rename to docs/opentelemetry.context.context.rst index ac28d40008..331557d2dd 100644 --- a/docs/opentelemetry.context.base_context.rst +++ b/docs/opentelemetry.context.context.rst @@ -1,7 +1,7 @@ opentelemetry.context.base\_context module ========================================== -.. automodule:: opentelemetry.context.base_context +.. automodule:: opentelemetry.context.context :members: :undoc-members: :show-inheritance: diff --git a/docs/opentelemetry.context.rst b/docs/opentelemetry.context.rst index 7bc738a050..2b25793458 100644 --- a/docs/opentelemetry.context.rst +++ b/docs/opentelemetry.context.rst @@ -6,7 +6,7 @@ Submodules .. toctree:: - opentelemetry.context.base_context + opentelemetry.context.context Module contents --------------- diff --git a/tox.ini b/tox.ini index ec575fcbab..f5a20f515b 100644 --- a/tox.ini +++ b/tox.ini @@ -134,6 +134,9 @@ deps = changedir = docs +commands_pre = + pip install -e {toxinidir}/opentelemetry-api + commands = sphinx-build -E -a -W --keep-going -b html -T . _build/html From 76a8c030ac8fb1adb6af699d7b6584448d838144 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 4 Feb 2020 10:17:30 -0800 Subject: [PATCH 07/56] adding named tracer support back Signed-off-by: Alex Boten --- .../src/opentelemetry/context/__init__.py | 2 +- .../trace/propagation/__init__.py | 26 ++++++++++++++----- .../src/opentelemetry/sdk/trace/__init__.py | 15 +++++++---- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 634619481d..b6f371ba6b 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -39,7 +39,7 @@ ]() # type: Context -def _copy_context(context: Context) -> Context: +def _copy_context(context: typing.Optional[Context]) -> Context: if context is not None: return context.copy() return get_current().copy() diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py index 57c09b0ef8..d82aa123ab 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py @@ -44,13 +44,27 @@ def with_span_context( return get_current().set_value(_SPAN_CONTEXT_KEY, span_context) -def span_from_context(context: Optional[Context] = None) -> Span: +def _get_span_key(name: Optional[str] = None) -> str: + key = _SPAN_KEY + if name is not None: + key = "{}-{}".format(key, name) + print(key) + return key + + +def span_from_context( + context: Optional[Context] = None, name: Optional[str] = None +) -> Span: + key = _get_span_key(name) if context is not None: - context.get_value(_SPAN_KEY) - return get_current().get_value(_SPAN_KEY) + context.get_value(key) + return get_current().get_value(key) -def with_span(span: Span, context: Optional[Context] = None) -> None: +def with_span( + span: Span, context: Optional[Context] = None, name: Optional[str] = None +) -> None: + key = _get_span_key(name) if context is not None: - return context.set_value(_SPAN_KEY, span) - return get_current().set_value(_SPAN_KEY, span) + return context.set_value(key, span) + return get_current().set_value(key, span) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 451383ae8e..89f9ce7c20 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -529,12 +529,12 @@ def use_span( ) -> Iterator[trace_api.Span]: """See `opentelemetry.trace.Tracer.use_span`.""" try: - span_snapshot = span_from_context() - with_span(span) + span_snapshot = self.source.get_current_span() + self.source.set_current_span(span) try: yield span finally: - with_span(span_snapshot) + self.source.set_current_span(span_snapshot) except Exception as error: # pylint: disable=broad-except if ( @@ -565,6 +565,7 @@ def __init__( ): # TODO: How should multiple TracerSources behave? Should they get their own contexts? # This could be done by adding `str(id(self))` to the slot name. + self._name = str(id(self)) self._active_span_processor = MultiSpanProcessor() self.sampler = sampler self._atexit_handler = None @@ -587,8 +588,12 @@ def get_tracer( ) def get_current_span(self, context: Optional[Context] = None) -> Span: - # pylint: disable=no-self-use - return span_from_context(context=context) + return span_from_context(context=context, name=self._name) + + def set_current_span( + self, span, context: Optional[Context] = None + ) -> Span: + return with_span(span, context=context, name=self._name) def add_span_processor(self, span_processor: SpanProcessor) -> None: """Registers a new :class:`SpanProcessor` for this `TracerSource`. From 3fe5a23091a133c5c4cac96cb8b945e82d6a6ac5 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 4 Feb 2020 12:56:56 -0800 Subject: [PATCH 08/56] mypy fixes Signed-off-by: Alex Boten --- opentelemetry-api/setup.py | 2 +- .../src/opentelemetry/context/__init__.py | 2 +- .../context/aiocontextvarsfix.py | 88 +++++++++++++++++++ .../context/contextvars_context.py | 72 +-------------- .../distributedcontext/__init__.py | 4 +- opentelemetry-sdk/setup.py | 2 +- 6 files changed, 94 insertions(+), 76 deletions(-) create mode 100644 opentelemetry-api/src/opentelemetry/context/aiocontextvarsfix.py diff --git a/opentelemetry-api/setup.py b/opentelemetry-api/setup.py index 34fe3e4613..a08cc8097f 100644 --- a/opentelemetry-api/setup.py +++ b/opentelemetry-api/setup.py @@ -44,7 +44,7 @@ include_package_data=True, long_description=open("README.rst").read(), long_description_content_type="text/x-rst", - install_requires=["typing; python_version<'3.5'"], + install_requires=["typing; python_version<'3.5'", "aiocontextvars"], extras_require={}, license="Apache-2.0", package_dir={"": "src"}, diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index b6f371ba6b..012fbd3eb3 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -28,7 +28,7 @@ for entry_point in iter_entry_points("opentelemetry_context"): try: - _available_contexts[entry_point.name] = entry_point.load() + _available_contexts[entry_point.name] = entry_point.load() # type: ignore except Exception as err: # pylint: disable=broad-except logger.warning( "Could not load entry_point %s:%s", entry_point.name, err diff --git a/opentelemetry-api/src/opentelemetry/context/aiocontextvarsfix.py b/opentelemetry-api/src/opentelemetry/context/aiocontextvarsfix.py new file mode 100644 index 0000000000..f9cb51eeb6 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/context/aiocontextvarsfix.py @@ -0,0 +1,88 @@ +# type: ignore +# 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 is a patch to allow aiocontextvars to work for older versions +# of Python 3.5. It is copied and pasted from: +# https://github.com/fantix/aiocontextvars/issues/88#issuecomment-522276290 + +import asyncio +import asyncio.coroutines +import asyncio.futures +import concurrent.futures + +if not hasattr(asyncio, "_get_running_loop"): + # noinspection PyCompatibility + # pylint:disable=protected-access + import asyncio.events + from threading import local as threading_local + + if not hasattr(asyncio.events, "_get_running_loop"): + + class _RunningLoop(threading_local): + _loop = None + + _running_loop = _RunningLoop() + + def _get_running_loop(): + return _running_loop._loop + + def set_running_loop(loop): # noqa: F811 + _running_loop._loop = loop + + def _get_event_loop(): + current_loop = _get_running_loop() + if current_loop is not None: + return current_loop + return asyncio.events.get_event_loop_policy().get_event_loop() + + asyncio.events.get_event_loop = _get_event_loop + asyncio.events._get_running_loop = _get_running_loop + asyncio.events._set_running_loop = set_running_loop + + asyncio._get_running_loop = asyncio.events._get_running_loop + asyncio._set_running_loop = asyncio.events._set_running_loop + +# It needs only to be imported to activate the patching of the contextvars +# backport (see the comment in setup.py) +# noinspection PyUnresolvedReferences +import aiocontextvars + + +def _run_coroutine_threadsafe(coro, loop): + """ + Patch to create task in the same thread instead of in the callback. + This ensures that contextvars get copied. Python 3.7 copies contextvars + without this. + """ + if not asyncio.coroutines.iscoroutine(coro): + raise TypeError("A coroutine object is required") + future = concurrent.futures.Future() + task = asyncio.ensure_future(coro, loop=loop) + + def callback() -> None: + try: + # noinspection PyProtectedMember,PyUnresolvedReferences + # pylint:disable=protected-access + asyncio.futures._chain_future(task, future) + except Exception as exc: + if future.set_running_or_notify_cancel(): + future.set_exception(exc) + raise + + loop.call_soon_threadsafe(callback) + return future + + +asyncio.run_coroutine_threadsafe = _run_coroutine_threadsafe diff --git a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py index b9e1b1425f..30bd747a3c 100644 --- a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py +++ b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py @@ -22,77 +22,7 @@ import aiocontextvars # pylint:disable=unused-import elif (3, 4) < version_info <= (3, 5, 2): - - # This is copied and pasted from: - # https://github.com/fantix/aiocontextvars/issues/88#issuecomment-522276290 - - import asyncio - import asyncio.coroutines - import asyncio.futures - import concurrent.futures - - if not hasattr(asyncio, "_get_running_loop"): - # noinspection PyCompatibility - # pylint:disable=protected-access - import asyncio.events - from threading import local as threading_local - - if not hasattr(asyncio.events, "_get_running_loop"): - - class _RunningLoop(threading_local): - _loop = None - - _running_loop = _RunningLoop() - - def _get_running_loop(): - return _running_loop._loop - - def set_running_loop(loop): # noqa: F811 - _running_loop._loop = loop - - def _get_event_loop(): - current_loop = _get_running_loop() - if current_loop is not None: - return current_loop - return asyncio.events.get_event_loop_policy().get_event_loop() - - asyncio.events.get_event_loop = _get_event_loop - asyncio.events._get_running_loop = _get_running_loop - asyncio.events._set_running_loop = set_running_loop - - asyncio._get_running_loop = asyncio.events._get_running_loop - asyncio._set_running_loop = asyncio.events._set_running_loop - - # It needs only to be imported to activate the patching of the contextvars - # backport (see the comment in setup.py) - # noinspection PyUnresolvedReferences - import aiocontextvars - - def _run_coroutine_threadsafe(coro, loop): - """ - Patch to create task in the same thread instead of in the callback. - This ensures that contextvars get copied. Python 3.7 copies contextvars - without this. - """ - if not asyncio.coroutines.iscoroutine(coro): - raise TypeError("A coroutine object is required") - future = concurrent.futures.Future() - task = asyncio.ensure_future(coro, loop=loop) - - def callback() -> None: - try: - # noinspection PyProtectedMember,PyUnresolvedReferences - # pylint:disable=protected-access - asyncio.futures._chain_future(task, future) - except Exception as exc: - if future.set_running_or_notify_cancel(): - future.set_exception(exc) - raise - - loop.call_soon_threadsafe(callback) - return future - - asyncio.run_coroutine_threadsafe = _run_coroutine_threadsafe + import opentelemetry.context.aiocontextvarsfix # pylint:disable=unused-import class ContextVarsContext(Context): diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py index 8474561eb1..4ca27e8dc2 100644 --- a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py +++ b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py @@ -136,8 +136,8 @@ def distributed_context_from_context( context: typing.Optional[Context] = None, ) -> DistributedContext: if context is not None: - return context.get_value(_DISTRIBUTED_CONTEXT_KEY) - return get_current().get_value(_DISTRIBUTED_CONTEXT_KEY) + return context.get_value(_DISTRIBUTED_CONTEXT_KEY) # type: ignore + return get_current().get_value(_DISTRIBUTED_CONTEXT_KEY) # type: ignore def with_distributed_context( diff --git a/opentelemetry-sdk/setup.py b/opentelemetry-sdk/setup.py index d40e9d1b3c..cbfb0f075d 100644 --- a/opentelemetry-sdk/setup.py +++ b/opentelemetry-sdk/setup.py @@ -44,7 +44,7 @@ include_package_data=True, long_description=open("README.rst").read(), long_description_content_type="text/x-rst", - install_requires=["opentelemetry-api==0.4.dev0", "aiocontextvars"], + install_requires=["opentelemetry-api==0.4.dev0"], extras_require={}, license="Apache-2.0", package_dir={"": "src"}, From 1f8d67fdc401af85e7089311c2fea10fda0a8f2a Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 4 Feb 2020 13:49:18 -0800 Subject: [PATCH 09/56] more mypy fixes Signed-off-by: Alex Boten --- .../src/opentelemetry/context/__init__.py | 37 ++++++++++--------- .../context/aiocontextvarsfix.py | 3 +- .../context/contextvars_context.py | 2 +- .../trace/propagation/__init__.py | 2 +- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 012fbd3eb3..d4321c4111 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -21,22 +21,7 @@ from opentelemetry.context.context import Context logger = logging.getLogger(__name__) - -# FIXME use a better implementation of a configuration manager to avoid having -# to get configuration values straight from environment variables -_available_contexts = {} - -for entry_point in iter_entry_points("opentelemetry_context"): - try: - _available_contexts[entry_point.name] = entry_point.load() # type: ignore - except Exception as err: # pylint: disable=broad-except - logger.warning( - "Could not load entry_point %s:%s", entry_point.name, err - ) - -_CONTEXT = _available_contexts[ - environ.get("OPENTELEMETRY_CONTEXT", "default_context") -]() # type: Context +_CONTEXT = None # type: typing.Optional[Context] def _copy_context(context: typing.Optional[Context]) -> Context: @@ -73,7 +58,25 @@ def remove_value( def get_current() -> Context: - return _CONTEXT + global _CONTEXT # pylint: disable=global-statement + if _CONTEXT is None: + # FIXME use a better implementation of a configuration manager to avoid having + # to get configuration values straight from environment variables + _available_contexts = {} # typing.Dict[str, Context] + + for entry_point in iter_entry_points("opentelemetry_context"): + try: + _available_contexts[entry_point.name] = entry_point.load() # type: ignore + except Exception as err: # pylint: disable=broad-except + logger.warning( + "Could not load entry_point %s:%s", entry_point.name, err + ) + + configured_context = environ.get( + "OPENTELEMETRY_CONTEXT", "default_context" + ) # type: str + _CONTEXT = _available_contexts[configured_context]() # type: ignore + return _CONTEXT # type: ignore def set_current(context: Context) -> None: diff --git a/opentelemetry-api/src/opentelemetry/context/aiocontextvarsfix.py b/opentelemetry-api/src/opentelemetry/context/aiocontextvarsfix.py index f9cb51eeb6..7546b27a75 100644 --- a/opentelemetry-api/src/opentelemetry/context/aiocontextvarsfix.py +++ b/opentelemetry-api/src/opentelemetry/context/aiocontextvarsfix.py @@ -22,6 +22,7 @@ import asyncio.futures import concurrent.futures + if not hasattr(asyncio, "_get_running_loop"): # noinspection PyCompatibility # pylint:disable=protected-access @@ -57,7 +58,7 @@ def _get_event_loop(): # It needs only to be imported to activate the patching of the contextvars # backport (see the comment in setup.py) # noinspection PyUnresolvedReferences -import aiocontextvars +import aiocontextvars # pylint: disable=unused-import def _run_coroutine_threadsafe(coro, loop): diff --git a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py index 30bd747a3c..304438d4dd 100644 --- a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py +++ b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py @@ -19,7 +19,7 @@ from opentelemetry.context.context import Context if (3, 5, 3) <= version_info < (3, 7): - import aiocontextvars # pylint:disable=unused-import + import aiocontextvars # type: ignore # pylint:disable=unused-import elif (3, 4) < version_info <= (3, 5, 2): import opentelemetry.context.aiocontextvarsfix # pylint:disable=unused-import diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py index d82aa123ab..fd62fdd0d6 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py @@ -58,7 +58,7 @@ def span_from_context( key = _get_span_key(name) if context is not None: context.get_value(key) - return get_current().get_value(key) + return get_current().get_value(key) # type: ignore def with_span( From 4bdd27cf728e5f531a257a5afa468a5ad7c12abc Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 4 Feb 2020 14:23:11 -0800 Subject: [PATCH 10/56] lint fixes Signed-off-by: Alex Boten --- .../src/opentelemetry/context/aiocontextvarsfix.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/aiocontextvarsfix.py b/opentelemetry-api/src/opentelemetry/context/aiocontextvarsfix.py index 7546b27a75..21287eb699 100644 --- a/opentelemetry-api/src/opentelemetry/context/aiocontextvarsfix.py +++ b/opentelemetry-api/src/opentelemetry/context/aiocontextvarsfix.py @@ -22,7 +22,6 @@ import asyncio.futures import concurrent.futures - if not hasattr(asyncio, "_get_running_loop"): # noinspection PyCompatibility # pylint:disable=protected-access @@ -55,10 +54,8 @@ def _get_event_loop(): asyncio._get_running_loop = asyncio.events._get_running_loop asyncio._set_running_loop = asyncio.events._set_running_loop -# It needs only to be imported to activate the patching of the contextvars -# backport (see the comment in setup.py) # noinspection PyUnresolvedReferences -import aiocontextvars # pylint: disable=unused-import +import aiocontextvars # pylint: disable=unused-import,wrong-import-position # noqa # isort:skip def _run_coroutine_threadsafe(coro, loop): From b28fb3f729bb85ce0ad0ab01fbe0dc1c4b3aa935 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 4 Feb 2020 14:45:13 -0800 Subject: [PATCH 11/56] moving contextvars and threadlocal context to sdk Signed-off-by: Alex Boten --- opentelemetry-api/setup.py | 8 +------- opentelemetry-sdk/setup.py | 12 +++++++++++- .../opentelemetry/sdk}/context/aiocontextvarsfix.py | 0 .../sdk}/context/contextvars_context.py | 2 +- .../sdk}/context/threadlocal_context.py | 0 5 files changed, 13 insertions(+), 9 deletions(-) rename {opentelemetry-api/src/opentelemetry => opentelemetry-sdk/src/opentelemetry/sdk}/context/aiocontextvarsfix.py (100%) rename {opentelemetry-api/src/opentelemetry => opentelemetry-sdk/src/opentelemetry/sdk}/context/contextvars_context.py (95%) rename {opentelemetry-api/src/opentelemetry => opentelemetry-sdk/src/opentelemetry/sdk}/context/threadlocal_context.py (100%) diff --git a/opentelemetry-api/setup.py b/opentelemetry-api/setup.py index a08cc8097f..3d65aeb9e2 100644 --- a/opentelemetry-api/setup.py +++ b/opentelemetry-api/setup.py @@ -44,7 +44,7 @@ include_package_data=True, long_description=open("README.rst").read(), long_description_content_type="text/x-rst", - install_requires=["typing; python_version<'3.5'", "aiocontextvars"], + install_requires=["typing; python_version<'3.5'"], extras_require={}, license="Apache-2.0", package_dir={"": "src"}, @@ -58,12 +58,6 @@ zip_safe=False, entry_points={ "opentelemetry_context": [ - "contextvars_context = " - "opentelemetry.context.contextvars_context:" - "ContextVarsContext", - "threadlocal_context = " - "opentelemetry.context.threadlocal_context:" - "ThreadLocalContext", "default_context = " "opentelemetry.context.default_context:" "DefaultContext", diff --git a/opentelemetry-sdk/setup.py b/opentelemetry-sdk/setup.py index cbfb0f075d..4bd81111e0 100644 --- a/opentelemetry-sdk/setup.py +++ b/opentelemetry-sdk/setup.py @@ -44,7 +44,7 @@ include_package_data=True, long_description=open("README.rst").read(), long_description_content_type="text/x-rst", - install_requires=["opentelemetry-api==0.4.dev0"], + install_requires=["opentelemetry-api==0.4.dev0", "aiocontextvars"], extras_require={}, license="Apache-2.0", package_dir={"": "src"}, @@ -56,4 +56,14 @@ "/tree/master/opentelemetry-sdk" ), zip_safe=False, + entry_points={ + "opentelemetry_context": [ + "contextvars_context = " + "opentelemetry.sdk.context.contextvars_context:" + "ContextVarsContext", + "threadlocal_context = " + "opentelemetry.sdk.context.threadlocal_context:" + "ThreadLocalContext", + ] + }, ) diff --git a/opentelemetry-api/src/opentelemetry/context/aiocontextvarsfix.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/aiocontextvarsfix.py similarity index 100% rename from opentelemetry-api/src/opentelemetry/context/aiocontextvarsfix.py rename to opentelemetry-sdk/src/opentelemetry/sdk/context/aiocontextvarsfix.py diff --git a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py similarity index 95% rename from opentelemetry-api/src/opentelemetry/context/contextvars_context.py rename to opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py index 304438d4dd..8493fba014 100644 --- a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py @@ -22,7 +22,7 @@ import aiocontextvars # type: ignore # pylint:disable=unused-import elif (3, 4) < version_info <= (3, 5, 2): - import opentelemetry.context.aiocontextvarsfix # pylint:disable=unused-import + import opentelemetry.sdk.context.aiocontextvarsfix # pylint:disable=unused-import class ContextVarsContext(Context): diff --git a/opentelemetry-api/src/opentelemetry/context/threadlocal_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py similarity index 100% rename from opentelemetry-api/src/opentelemetry/context/threadlocal_context.py rename to opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py From 51af769c87e92bc3f4ec4eef4ac21d4ee69c65f0 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 4 Feb 2020 16:11:43 -0800 Subject: [PATCH 12/56] adding documentation Signed-off-by: Alex Boten --- .../src/opentelemetry/context/__init__.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index d4321c4111..1f4d9253d2 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -36,6 +36,15 @@ def create_key(key: str) -> "object": def get_value(key: str, context: typing.Optional[Context] = None) -> "object": + """ + To access the local state of an concern, the Context API + provides a function which takes a context and a key as input, + and returns a value. + + Args: + key: The key of the value to retrieve. + context: The context from which to retrieve the value, if None, the current context is used. + """ if context is not None: return context.get_value(key) return get_current().get_value(key) @@ -44,6 +53,17 @@ def get_value(key: str, context: typing.Optional[Context] = None) -> "object": def set_value( key: str, value: "object", context: typing.Optional[Context] = None ) -> Context: + """ + To record the local state of a cross-cutting concern, the + Context API provides a function which takes a context, a + key, and a value as input, and returns an updated context + which contains the new value. + + Args: + key: The key of the entry to set + value: The value of the entry to set + context: The context to copy, if None, the current context is used + """ new_context = _copy_context(context) new_context.set_value(key, value) return new_context @@ -52,12 +72,25 @@ def set_value( def remove_value( key: str, context: typing.Optional[Context] = None ) -> Context: + """ + To remove a value, this method returns a new context with the key cleared. + Note that the removed value still remains present in the old context. + + Args: + key: The key of the entry to remove + context: The context to copy, if None, the current context is used + """ new_context = _copy_context(context) new_context.remove_value(key) return new_context def get_current() -> Context: + """ + To access the context associated with program execution, + the Context API provides a function which takes no arguments + and returns a Context. + """ global _CONTEXT # pylint: disable=global-statement if _CONTEXT is None: # FIXME use a better implementation of a configuration manager to avoid having @@ -80,6 +113,13 @@ def get_current() -> Context: def set_current(context: Context) -> None: + """ + To associate a context with program execution, the Context + API provides a function which takes a Context. + + Args: + context: The context to use as current. + """ global _CONTEXT # pylint: disable=global-statement _CONTEXT = context From 530c21a2dd27c32ea45241fe6c744793faabf394 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 4 Feb 2020 16:59:36 -0800 Subject: [PATCH 13/56] documentation Signed-off-by: Alex Boten --- .../src/opentelemetry/context/context.py | 29 +++++++++++++++++-- .../opentelemetry/context/default_context.py | 5 ++++ .../sdk/context/contextvars_context.py | 6 ++++ .../sdk/context/threadlocal_context.py | 6 ++++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/context.py b/opentelemetry-api/src/opentelemetry/context/context.py index 6c07d4bd5c..251944656b 100644 --- a/opentelemetry-api/src/opentelemetry/context/context.py +++ b/opentelemetry-api/src/opentelemetry/context/context.py @@ -20,17 +20,40 @@ class Context(ABC): + """ + The Context interface provides a wrapper for the different + mechanisms that are used to propagate context in Python. + Implementations can be made available via entry_points and + selected through environment variables. + """ + @abstractmethod def set_value(self, key: str, value: "object") -> None: - """Set a value in this context""" + """ + Set a value in this context + + Args: + key: The key for the value to set. + value: The value to set. + """ @abstractmethod def get_value(self, key: str) -> "object": - """Get a value from this context""" + """ + Get a value from this context + + Args: + key: The key for the value to retrieve. + """ @abstractmethod def remove_value(self, key: str) -> None: - """Remove a value from this context""" + """ + Remove a value from this context + + Args: + key: The key for the value to remove. + """ @contextmanager # type: ignore def use(self, **kwargs: Dict[str, object]) -> Iterator[None]: diff --git a/opentelemetry-api/src/opentelemetry/context/default_context.py b/opentelemetry-api/src/opentelemetry/context/default_context.py index 22b21fac4e..5daecfa0b4 100644 --- a/opentelemetry-api/src/opentelemetry/context/default_context.py +++ b/opentelemetry-api/src/opentelemetry/context/default_context.py @@ -17,6 +17,11 @@ class DefaultContext(Context): + """ + A default implementation of the Context interface using + a dictionary to store values. + """ + def __init__(self) -> None: self._values = {} # type: typing.Dict[str, object] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py index 8493fba014..ca69175870 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py @@ -26,6 +26,12 @@ class ContextVarsContext(Context): + """ + An implementation of the Context interface + which wraps ContextVar under the hood. This is the prefered + implementation for usage with Python 3.5+ + """ + def __init__(self) -> None: self._contextvars = {} # type: typing.Dict[str, ContextVar[object]] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py index c409c3d27e..e2c18e2f7d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py @@ -18,6 +18,12 @@ class ThreadLocalContext(Context): + """ + An implementation of the Context interface + which uses thread-local storage under the hood. This + implementation is available for usage with Python 3.4. + """ + def __init__(self) -> None: self._thread_local = threading.local() From 16eb703241ef69abfafa251a72f28576f2c89f36 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 5 Feb 2020 09:20:00 -0800 Subject: [PATCH 14/56] replace get_value w/ context.get_value Signed-off-by: Alex Boten --- .../src/opentelemetry/ext/http_requests/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 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 e06a3e5fd3..a557e6fc45 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,8 +22,7 @@ from requests.sessions import Session -from opentelemetry import propagators -from opentelemetry.context import get_value +from opentelemetry import context, propagators from opentelemetry.ext.http_requests.version import __version__ from opentelemetry.trace import SpanKind @@ -54,7 +53,7 @@ def enable(tracer_source): @functools.wraps(wrapped) def instrumented_request(self, method, url, *args, **kwargs): - if get_value("suppress_instrumentation"): + if context.get_value("suppress_instrumentation"): return wrapped(self, method, url, *args, **kwargs) # See From ddd81a57523b9ba5bd15dfc384fc80b63ba094fc Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 5 Feb 2020 09:26:40 -0800 Subject: [PATCH 15/56] log as error. more cleanup Signed-off-by: Alex Boten --- opentelemetry-api/src/opentelemetry/context/__init__.py | 6 ++---- .../src/opentelemetry/trace/propagation/__init__.py | 1 - tox.ini | 3 --- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 1f4d9253d2..57c6376e10 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -100,10 +100,8 @@ def get_current() -> Context: for entry_point in iter_entry_points("opentelemetry_context"): try: _available_contexts[entry_point.name] = entry_point.load() # type: ignore - except Exception as err: # pylint: disable=broad-except - logger.warning( - "Could not load entry_point %s:%s", entry_point.name, err - ) + except Exception: # pylint: disable=broad-except + logger.error("Could not load entry_point %s", entry_point.name) configured_context = environ.get( "OPENTELEMETRY_CONTEXT", "default_context" diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py index fd62fdd0d6..352776c90a 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py @@ -48,7 +48,6 @@ def _get_span_key(name: Optional[str] = None) -> str: key = _SPAN_KEY if name is not None: key = "{}-{}".format(key, name) - print(key) return key diff --git a/tox.ini b/tox.ini index f5a20f515b..ec575fcbab 100644 --- a/tox.ini +++ b/tox.ini @@ -134,9 +134,6 @@ deps = changedir = docs -commands_pre = - pip install -e {toxinidir}/opentelemetry-api - commands = sphinx-build -E -a -W --keep-going -b html -T . _build/html From b935bf52ed4dc84e1ad540275f561c41fcfc8cb0 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 5 Feb 2020 09:30:26 -0800 Subject: [PATCH 16/56] zipkin exporter needs the sdk Signed-off-by: Alex Boten --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index ec575fcbab..3cf3626310 100644 --- a/tox.ini +++ b/tox.ini @@ -81,7 +81,8 @@ commands_pre = jaeger: pip install {toxinidir}/opentelemetry-sdk jaeger: pip install {toxinidir}/ext/opentelemetry-ext-jaeger opentracing-shim: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/ext/opentelemetry-ext-opentracing-shim - zipkin: pip install {toxinidir}/opentelemetry-api {toxinidir}/opentelemetry-sdk {toxinidir}/ext/opentelemetry-ext-zipkin + zipkin: pip install {toxinidir}/opentelemetry-sdk + zipkin: pip install {toxinidir}/ext/opentelemetry-ext-zipkin ; In order to get a healthy coverage report, ; we have to install packages in editable mode. From 15805a4b4103199c31902cd271e9a5869e222a27 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 5 Feb 2020 09:52:15 -0800 Subject: [PATCH 17/56] small cleanups Signed-off-by: Alex Boten --- .../src/opentelemetry/context/__init__.py | 14 +++----------- .../src/opentelemetry/context/context.py | 6 +++--- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 57c6376e10..a7fbe731d8 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -24,12 +24,6 @@ _CONTEXT = None # type: typing.Optional[Context] -def _copy_context(context: typing.Optional[Context]) -> Context: - if context is not None: - return context.copy() - return get_current().copy() - - def create_key(key: str) -> "object": # FIXME Implement this raise NotImplementedError @@ -45,9 +39,7 @@ def get_value(key: str, context: typing.Optional[Context] = None) -> "object": key: The key of the value to retrieve. context: The context from which to retrieve the value, if None, the current context is used. """ - if context is not None: - return context.get_value(key) - return get_current().get_value(key) + return context.get_value(key) if context else get_current().get_value(key) def set_value( @@ -64,7 +56,7 @@ def set_value( value: The value of the entry to set context: The context to copy, if None, the current context is used """ - new_context = _copy_context(context) + new_context = context.copy() if context else get_current().copy() new_context.set_value(key, value) return new_context @@ -80,7 +72,7 @@ def remove_value( key: The key of the entry to remove context: The context to copy, if None, the current context is used """ - new_context = _copy_context(context) + new_context = context.copy() if context else get_current().copy() new_context.remove_value(key) return new_context diff --git a/opentelemetry-api/src/opentelemetry/context/context.py b/opentelemetry-api/src/opentelemetry/context/context.py index 251944656b..d87f8ce101 100644 --- a/opentelemetry-api/src/opentelemetry/context/context.py +++ b/opentelemetry-api/src/opentelemetry/context/context.py @@ -31,7 +31,7 @@ class Context(ABC): def set_value(self, key: str, value: "object") -> None: """ Set a value in this context - + Args: key: The key for the value to set. value: The value to set. @@ -41,7 +41,7 @@ def set_value(self, key: str, value: "object") -> None: def get_value(self, key: str) -> "object": """ Get a value from this context - + Args: key: The key for the value to retrieve. """ @@ -50,7 +50,7 @@ def get_value(self, key: str) -> "object": def remove_value(self, key: str) -> None: """ Remove a value from this context - + Args: key: The key for the value to remove. """ From 68b98e566975ffce155344bb0bf0090a7622c70e Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 5 Feb 2020 09:57:03 -0800 Subject: [PATCH 18/56] don't bother constructing a map for entry_points Signed-off-by: Alex Boten --- .../src/opentelemetry/context/__init__.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index a7fbe731d8..14a08a968b 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -87,18 +87,13 @@ def get_current() -> Context: if _CONTEXT is None: # FIXME use a better implementation of a configuration manager to avoid having # to get configuration values straight from environment variables - _available_contexts = {} # typing.Dict[str, Context] - - for entry_point in iter_entry_points("opentelemetry_context"): - try: - _available_contexts[entry_point.name] = entry_point.load() # type: ignore - except Exception: # pylint: disable=broad-except - logger.error("Could not load entry_point %s", entry_point.name) configured_context = environ.get( "OPENTELEMETRY_CONTEXT", "default_context" ) # type: str - _CONTEXT = _available_contexts[configured_context]() # type: ignore + _CONTEXT = next( + iter_entry_points("opentelemetry_context", configured_context) + ).load()() return _CONTEXT # type: ignore From 3a2b0ef725179bf806b72d2d3e4ebffbbcd25e0b Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 5 Feb 2020 10:51:29 -0800 Subject: [PATCH 19/56] cleaning up more code, adding some context tests Signed-off-by: Alex Boten --- .../distributedcontext/__init__.py | 6 +-- .../trace/propagation/__init__.py | 11 ++--- .../tests/context/test_context.py | 46 +++++++++++++++++++ 3 files changed, 51 insertions(+), 12 deletions(-) create mode 100644 opentelemetry-api/tests/context/test_context.py diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py index 4ca27e8dc2..878fdc2352 100644 --- a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py +++ b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py @@ -17,7 +17,7 @@ import typing from contextlib import contextmanager -from opentelemetry.context import Context, get_current +from opentelemetry.context import Context, get_current, get_value PRINTABLE = frozenset( itertools.chain( @@ -135,9 +135,7 @@ def use_context( def distributed_context_from_context( context: typing.Optional[Context] = None, ) -> DistributedContext: - if context is not None: - return context.get_value(_DISTRIBUTED_CONTEXT_KEY) # type: ignore - return get_current().get_value(_DISTRIBUTED_CONTEXT_KEY) # type: ignore + return get_value(_DISTRIBUTED_CONTEXT_KEY, context) def with_distributed_context( diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py index 352776c90a..12c3b58c78 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py @@ -13,7 +13,7 @@ # limitations under the License. from typing import Optional -from opentelemetry.context import Context, get_current +from opentelemetry.context import Context, get_current, get_value from opentelemetry.trace import INVALID_SPAN_CONTEXT, Span, SpanContext _SPAN_CONTEXT_KEY = "extracted-span-context" @@ -26,10 +26,7 @@ def span_context_from_context( span = span_from_context(context=context) if span is not None: return span.get_context() - if context is not None: - sc = context.get_value(_SPAN_CONTEXT_KEY) - else: - sc = get_current().get_value(_SPAN_CONTEXT_KEY) + sc = get_value(_SPAN_CONTEXT_KEY, context=context) if sc is not None: return sc @@ -55,9 +52,7 @@ def span_from_context( context: Optional[Context] = None, name: Optional[str] = None ) -> Span: key = _get_span_key(name) - if context is not None: - context.get_value(key) - return get_current().get_value(key) # type: ignore + return get_value(key, context) def with_span( diff --git a/opentelemetry-api/tests/context/test_context.py b/opentelemetry-api/tests/context/test_context.py new file mode 100644 index 0000000000..e3158f4302 --- /dev/null +++ b/opentelemetry-api/tests/context/test_context.py @@ -0,0 +1,46 @@ +# 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 + +from opentelemetry import context + + +def do_work(): + context.set_current(context.set_value("say-something", "bar")) + + +class TestContext(unittest.TestCase): + def test_context(self): + self.assertIsNone(context.get_value("say-something")) + empty_context = context.get_current() + second_context = context.set_value("say-something", "foo") + self.assertEqual(second_context.get_value("say-something"), "foo") + + do_work() + self.assertEqual(context.get_value("say-something"), "bar") + third_context = context.get_current() + + self.assertIsNone(empty_context.get_value("say-something")) + self.assertEqual(second_context.get_value("say-something"), "foo") + self.assertEqual(third_context.get_value("say-something"), "bar") + + def test_set_value(self): + first = context.set_value("a", "yyy") + second = context.set_value("a", "zzz") + third = context.set_value("a", "---", first) + self.assertEqual("yyy", context.get_value("a", context=first)) + self.assertEqual("zzz", context.get_value("a", context=second)) + self.assertEqual("---", context.get_value("a", context=third)) + self.assertEqual(None, context.get_value("a")) From 62c76ecacfbd6a4837ec5146b02aa1d88966faa7 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 5 Feb 2020 12:54:49 -0800 Subject: [PATCH 20/56] mypy fixes Signed-off-by: Alex Boten --- .../src/opentelemetry/distributedcontext/__init__.py | 2 +- .../src/opentelemetry/trace/propagation/__init__.py | 2 +- opentelemetry-api/tests/context/test_context.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py index 878fdc2352..36232225f8 100644 --- a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py +++ b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py @@ -135,7 +135,7 @@ def use_context( def distributed_context_from_context( context: typing.Optional[Context] = None, ) -> DistributedContext: - return get_value(_DISTRIBUTED_CONTEXT_KEY, context) + return get_value(_DISTRIBUTED_CONTEXT_KEY, context) # type: ignore def with_distributed_context( diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py index 12c3b58c78..d700d82912 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py @@ -52,7 +52,7 @@ def span_from_context( context: Optional[Context] = None, name: Optional[str] = None ) -> Span: key = _get_span_key(name) - return get_value(key, context) + return get_value(key, context) # type: ignore def with_span( diff --git a/opentelemetry-api/tests/context/test_context.py b/opentelemetry-api/tests/context/test_context.py index e3158f4302..e1b8238eaf 100644 --- a/opentelemetry-api/tests/context/test_context.py +++ b/opentelemetry-api/tests/context/test_context.py @@ -17,7 +17,7 @@ from opentelemetry import context -def do_work(): +def do_work() -> None: context.set_current(context.set_value("say-something", "bar")) From 5dca7cc47e5b56b3442d5332ad9e544e9622cf75 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 5 Feb 2020 14:51:05 -0800 Subject: [PATCH 21/56] Fix ThreadlocalContext, use ContextVarsContext for tests with python > 3.4 Signed-off-by: Alex Boten --- .../opentelemetry/sdk/context/threadlocal_context.py | 11 +++++++++++ opentelemetry-sdk/tests/conftest.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py index e2c18e2f7d..63075d87f6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from copy import copy import threading from opentelemetry.context import Context @@ -42,3 +43,13 @@ def get_value(self, key: str) -> "object": def remove_value(self, key: str) -> None: """Remove a value from this context""" delattr(self._thread_local, key) + + def copy(self) -> Context: + """Return a copy of this context""" + + context_copy = ThreadLocalContext() + + for key, value in self._thread_local.__dict__.items(): + context_copy.set_value(key, copy(value)) + + return context_copy diff --git a/opentelemetry-sdk/tests/conftest.py b/opentelemetry-sdk/tests/conftest.py index fa2ac2bdaa..40428bc43f 100644 --- a/opentelemetry-sdk/tests/conftest.py +++ b/opentelemetry-sdk/tests/conftest.py @@ -18,7 +18,7 @@ def pytest_sessionstart(session): # pylint: disable=unused-argument - if (3, 4) <= version_info: + if (3, 4) >= version_info: # contextvars are not supported in 3.4, use thread-local storage environ["OPENTELEMETRY_CONTEXT"] = "threadlocal_context" else: From 7203e72abc8211b95b65d93e04c0763cae312f6d Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 5 Feb 2020 15:11:57 -0800 Subject: [PATCH 22/56] fix tests Signed-off-by: Alex Boten --- .../src/opentelemetry/sdk/context/contextvars_context.py | 2 +- opentelemetry-sdk/tests/trace/export/test_export.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py index ca69175870..9c4d736fff 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py @@ -62,7 +62,7 @@ def copy(self) -> Context: context_copy = ContextVarsContext() for key, value in self._contextvars.items(): - context_copy.set_value(key, copy(value)) + context_copy.set_value(key, copy(value.get())) return context_copy diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index 54fdee2629..9733cf9a41 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -17,7 +17,9 @@ from unittest import mock from opentelemetry import trace as trace_api +from opentelemetry.context import set_current from opentelemetry.sdk import trace +from opentelemetry.sdk.context.contextvars_context import ContextVarsContext from opentelemetry.sdk.trace import export @@ -97,6 +99,11 @@ def _create_start_and_end_span(name, span_processor): class TestBatchExportSpanProcessor(unittest.TestCase): + @classmethod + def setUpClass(cls): + # reset the current context + set_current(ContextVarsContext()) + def test_batch_span_processor(self): spans_names_list = [] From e43f16819865ccc409450b2c45ee66ff244858e1 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 5 Feb 2020 15:24:58 -0800 Subject: [PATCH 23/56] as per review feedback, removing use Signed-off-by: Alex Boten --- .../src/opentelemetry/context/context.py | 9 ----- .../sdk/trace/export/__init__.py | 34 +++++++++---------- 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/context.py b/opentelemetry-api/src/opentelemetry/context/context.py index d87f8ce101..feebad0902 100644 --- a/opentelemetry-api/src/opentelemetry/context/context.py +++ b/opentelemetry-api/src/opentelemetry/context/context.py @@ -55,15 +55,6 @@ def remove_value(self, key: str) -> None: key: The key for the value to remove. """ - @contextmanager # type: ignore - def use(self, **kwargs: Dict[str, object]) -> Iterator[None]: - snapshot = {key: self.get_value(key) for key in kwargs} - for key in kwargs: - self.set_value(key, kwargs[key]) - yield - for key in kwargs: - self.set_value(key, snapshot[key]) - def copy(self) -> "Context": """Return a copy of this context""" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 4ffbe01697..677bb61e15 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -18,7 +18,7 @@ import typing from enum import Enum -from opentelemetry.context import get_current +from opentelemetry.context import remove_value, set_current, set_value from opentelemetry.util import time_ns from .. import Span, SpanProcessor @@ -73,12 +73,13 @@ def on_start(self, span: Span) -> None: pass def on_end(self, span: Span) -> None: - with get_current().use(suppress_instrumentation=True): - try: - self.span_exporter.export((span,)) - # pylint: disable=broad-except - except Exception: - logger.exception("Exception while exporting Span.") + set_current(set_value("suppress_instrumentation", True)) + try: + self.span_exporter.export((span,)) + # pylint: disable=broad-except + except Exception: + logger.exception("Exception while exporting Span.") + set_current(remove_value("suppress_instrumentation")) def shutdown(self) -> None: self.span_exporter.shutdown() @@ -182,16 +183,15 @@ def export(self) -> None: while idx < self.max_export_batch_size and self.queue: self.spans_list[idx] = self.queue.pop() idx += 1 - with get_current().use(suppress_instrumentation=True): - try: - # Ignore type b/c the Optional[None]+slicing is too "clever" - # for mypy - self.span_exporter.export( - self.spans_list[:idx] - ) # type: ignore - # pylint: disable=broad-except - except Exception: - logger.exception("Exception while exporting Span batch.") + set_current(set_value("suppress_instrumentation", True)) + try: + # Ignore type b/c the Optional[None]+slicing is too "clever" + # for mypy + self.span_exporter.export(self.spans_list[:idx]) # type: ignore + # pylint: disable=broad-except + except Exception: + logger.exception("Exception while exporting Span batch.") + set_current(remove_value("suppress_instrumentation")) # clean up list for index in range(idx): self.spans_list[index] = None From f81a673a0a4d6c27a5111aca232adc04d431cada Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 5 Feb 2020 15:46:35 -0800 Subject: [PATCH 24/56] adding tests for sdk threadlocal and contextvar contexts Signed-off-by: Alex Boten --- .../src/opentelemetry/context/context.py | 2 - .../sdk/context/threadlocal_context.py | 2 +- .../tests/context/test_context.py | 87 +++++++++++++++++++ 3 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 opentelemetry-sdk/tests/context/test_context.py diff --git a/opentelemetry-api/src/opentelemetry/context/context.py b/opentelemetry-api/src/opentelemetry/context/context.py index feebad0902..90a32c71a8 100644 --- a/opentelemetry-api/src/opentelemetry/context/context.py +++ b/opentelemetry-api/src/opentelemetry/context/context.py @@ -14,9 +14,7 @@ from abc import ABC, abstractmethod -from contextlib import contextmanager from copy import deepcopy -from typing import Dict, Iterator class Context(ABC): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py index 63075d87f6..69235bf60b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from copy import copy import threading +from copy import copy from opentelemetry.context import Context diff --git a/opentelemetry-sdk/tests/context/test_context.py b/opentelemetry-sdk/tests/context/test_context.py new file mode 100644 index 0000000000..d2af5d5fc6 --- /dev/null +++ b/opentelemetry-sdk/tests/context/test_context.py @@ -0,0 +1,87 @@ +# 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 + +from opentelemetry import context +from opentelemetry.sdk.context.contextvars_context import ContextVarsContext +from opentelemetry.sdk.context.threadlocal_context import ThreadLocalContext + + +def do_work() -> None: + context.set_current(context.set_value("say-something", "bar")) + + +class TestThreadLocalContext(unittest.TestCase): + def setUp(self): + self.previous_context = context.get_current() + context.set_current(ThreadLocalContext()) + + def tearDown(self): + context.set_current(self.previous_context) + + def test_context(self): + self.assertIsNone(context.get_value("say-something")) + empty_context = context.get_current() + second_context = context.set_value("say-something", "foo") + self.assertEqual(second_context.get_value("say-something"), "foo") + + do_work() + self.assertEqual(context.get_value("say-something"), "bar") + third_context = context.get_current() + + self.assertIsNone(empty_context.get_value("say-something")) + self.assertEqual(second_context.get_value("say-something"), "foo") + self.assertEqual(third_context.get_value("say-something"), "bar") + + def test_set_value(self): + first = context.set_value("a", "yyy") + second = context.set_value("a", "zzz") + third = context.set_value("a", "---", first) + self.assertEqual("yyy", context.get_value("a", context=first)) + self.assertEqual("zzz", context.get_value("a", context=second)) + self.assertEqual("---", context.get_value("a", context=third)) + self.assertEqual(None, context.get_value("a")) + + +class TestContextVarsContext(unittest.TestCase): + def setUp(self): + self.previous_context = context.get_current() + context.set_current(ContextVarsContext()) + + def tearDown(self): + context.set_current(self.previous_context) + + def test_context(self): + self.assertIsNone(context.get_value("say-something")) + empty_context = context.get_current() + second_context = context.set_value("say-something", "foo") + self.assertEqual(second_context.get_value("say-something"), "foo") + + do_work() + self.assertEqual(context.get_value("say-something"), "bar") + third_context = context.get_current() + + self.assertIsNone(empty_context.get_value("say-something")) + self.assertEqual(second_context.get_value("say-something"), "foo") + self.assertEqual(third_context.get_value("say-something"), "bar") + + def test_set_value(self): + first = context.set_value("a", "yyy") + second = context.set_value("a", "zzz") + third = context.set_value("a", "---", first) + self.assertEqual("yyy", context.get_value("a", context=first)) + self.assertEqual("zzz", context.get_value("a", context=second)) + self.assertEqual("---", context.get_value("a", context=third)) + self.assertEqual(None, context.get_value("a")) From ba6584570b6e88021e6c533bb42699a23f702302 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 5 Feb 2020 16:00:29 -0800 Subject: [PATCH 25/56] removing create_key Signed-off-by: Alex Boten --- opentelemetry-api/src/opentelemetry/context/__init__.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 14a08a968b..47873412bd 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -24,11 +24,6 @@ _CONTEXT = None # type: typing.Optional[Context] -def create_key(key: str) -> "object": - # FIXME Implement this - raise NotImplementedError - - def get_value(key: str, context: typing.Optional[Context] = None) -> "object": """ To access the local state of an concern, the Context API From ed187bf04b38d3b0e3879bcbba3274bd32d4c7f8 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Thu, 6 Feb 2020 08:49:39 -0800 Subject: [PATCH 26/56] add try/except Signed-off-by: Alex Boten --- opentelemetry-api/src/opentelemetry/context/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 47873412bd..f165ed584c 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -86,9 +86,12 @@ def get_current() -> Context: configured_context = environ.get( "OPENTELEMETRY_CONTEXT", "default_context" ) # type: str - _CONTEXT = next( - iter_entry_points("opentelemetry_context", configured_context) - ).load()() + try: + _CONTEXT = next( + iter_entry_points("opentelemetry_context", configured_context) + ).load()() + except Exception: # pylint: disable=broad-except + logger.error("Failed to load context: %s", configured_context) return _CONTEXT # type: ignore From 2b8c69f1e9de8d27cfaeaf17f4f31add6865c7fb Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Thu, 6 Feb 2020 10:24:13 -0800 Subject: [PATCH 27/56] adding tests for threads and asyncio Signed-off-by: Alex Boten --- .../src/opentelemetry/context/__init__.py | 22 +++++ .../tests/context/test_asyncio.py | 51 ++++++++++++ .../tests/context/test_threads.py | 81 +++++++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 opentelemetry-sdk/tests/context/test_asyncio.py create mode 100644 opentelemetry-sdk/tests/context/test_threads.py diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index f165ed584c..e9bfc6ca9e 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -107,6 +107,28 @@ def set_current(context: Context) -> None: _CONTEXT = context +def with_current_context( + func: typing.Callable[..., "object"] +) -> typing.Callable[..., "object"]: + """ + Capture the current context and apply it to the provided func. + """ + + caller_context = get_current().copy() + + def call_with_current_context( + *args: "object", **kwargs: "object" + ) -> "object": + try: + backup_context = get_current().copy() + set_current(caller_context) + return func(*args, **kwargs) + finally: + set_current(backup_context) + + return call_with_current_context + + __all__ = [ "get_value", "set_value", diff --git a/opentelemetry-sdk/tests/context/test_asyncio.py b/opentelemetry-sdk/tests/context/test_asyncio.py new file mode 100644 index 0000000000..993d67f1fb --- /dev/null +++ b/opentelemetry-sdk/tests/context/test_asyncio.py @@ -0,0 +1,51 @@ +# 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 + +from opentelemetry import context +from opentelemetry.sdk import trace +from opentelemetry.sdk.context.contextvars_context import ContextVarsContext +from opentelemetry.sdk.trace import export +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) + + +class TestAsyncio(unittest.TestCase): + span_names = [ + "test_span1", + "test_span2", + "test_span3", + "test_span4", + "test_span5", + ] + + def do_work(self, name="default"): + with self.tracer.start_as_current_span(name): + context.set_value("say-something", "bar") + + def setUp(self): + self.previous_context = context.get_current() + context.set_current(ContextVarsContext()) + self.tracer_source = trace.TracerSource() + self.tracer = self.tracer_source.get_tracer(__name__) + self.memory_exporter = InMemorySpanExporter() + span_processor = export.SimpleExportSpanProcessor(self.memory_exporter) + self.tracer_source.add_span_processor(span_processor) + + def tearDown(self): + context.set_current(self.previous_context) + + # FIXME: add an actual test diff --git a/opentelemetry-sdk/tests/context/test_threads.py b/opentelemetry-sdk/tests/context/test_threads.py new file mode 100644 index 0000000000..dfbe78d4ef --- /dev/null +++ b/opentelemetry-sdk/tests/context/test_threads.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. + +import unittest +from multiprocessing.dummy import Pool as ThreadPool + +from opentelemetry import context +from opentelemetry.sdk import trace +from opentelemetry.sdk.context.threadlocal_context import ThreadLocalContext +from opentelemetry.sdk.trace import export +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) + + +class TestThreads(unittest.TestCase): + span_names = [ + "test_span1", + "test_span2", + "test_span3", + "test_span4", + "test_span5", + ] + + def do_work(self, name="default"): + with self.tracer.start_as_current_span(name): + context.set_value("say-something", "bar") + + def setUp(self): + self.previous_context = context.get_current() + context.set_current(ThreadLocalContext()) + self.tracer_source = trace.TracerSource() + self.tracer = self.tracer_source.get_tracer(__name__) + self.memory_exporter = InMemorySpanExporter() + span_processor = export.SimpleExportSpanProcessor(self.memory_exporter) + self.tracer_source.add_span_processor(span_processor) + + def tearDown(self): + context.set_current(self.previous_context) + + def test_with_threads(self): + with self.tracer.start_as_current_span("threads_test"): + pool = ThreadPool(5) # create a thread pool + pool.map( + context.with_current_context(self.do_work), self.span_names + ) + pool.close() + pool.join() + span_list = self.memory_exporter.get_finished_spans() + span_names_list = [span.name for span in span_list] + expected = [ + "test_span1", + "test_span2", + "test_span3", + "test_span4", + "test_span5", + "threads_test", + ] + self.assertCountEqual(span_names_list, expected) + span_names_list.sort() + expected.sort() + self.assertListEqual(span_names_list, expected) + expected_parent = next( + span for span in span_list if span.name == "threads_test" + ) + # FIXME + for span in span_list: + if span is expected_parent: + continue + self.assertEqual(span.parent, expected_parent) From 1a0217f8fbac5c1cc1724e8156929c886c19ab44 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Thu, 6 Feb 2020 10:37:12 -0800 Subject: [PATCH 28/56] filling in asyncio test Signed-off-by: Alex Boten --- .../tests/context/test_asyncio.py | 70 ++++++++++++++++--- 1 file changed, 60 insertions(+), 10 deletions(-) diff --git a/opentelemetry-sdk/tests/context/test_asyncio.py b/opentelemetry-sdk/tests/context/test_asyncio.py index 993d67f1fb..9c38762107 100644 --- a/opentelemetry-sdk/tests/context/test_asyncio.py +++ b/opentelemetry-sdk/tests/context/test_asyncio.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import asyncio import unittest from opentelemetry import context @@ -22,20 +23,36 @@ InMemorySpanExporter, ) +_SPAN_NAMES = [ + "test_span1", + "test_span2", + "test_span3", + "test_span4", + "test_span5", +] + + +def stop_loop_when(loop, cond_func, timeout=5.0): + """ + Registers a periodic callback that stops the loop when cond_func() == True. + Compatible with both Tornado and asyncio. + """ + if cond_func() or timeout <= 0.0: + loop.stop() + return + + timeout -= 0.1 + loop.call_later(0.1, stop_loop_when, loop, cond_func, timeout) + class TestAsyncio(unittest.TestCase): - span_names = [ - "test_span1", - "test_span2", - "test_span3", - "test_span4", - "test_span5", - ] - - def do_work(self, name="default"): + async def task(self, name): with self.tracer.start_as_current_span(name): context.set_value("say-something", "bar") + def submit_another_task(self, name): + self.loop.create_task(self.task(name)) + def setUp(self): self.previous_context = context.get_current() context.set_current(ContextVarsContext()) @@ -44,8 +61,41 @@ def setUp(self): self.memory_exporter = InMemorySpanExporter() span_processor = export.SimpleExportSpanProcessor(self.memory_exporter) self.tracer_source.add_span_processor(span_processor) + self.loop = asyncio.get_event_loop() def tearDown(self): context.set_current(self.previous_context) - # FIXME: add an actual test + def test_with_asyncio(self): + with self.tracer.start_as_current_span("asyncio_test"): + for name in _SPAN_NAMES: + self.submit_another_task(name) + + stop_loop_when( + self.loop, + lambda: len(self.memory_exporter.get_finished_spans()) >= 6, + timeout=5.0, + ) + self.loop.run_forever() + span_list = self.memory_exporter.get_finished_spans() + span_names_list = [span.name for span in span_list] + expected = [ + "test_span1", + "test_span2", + "test_span3", + "test_span4", + "test_span5", + "asyncio_test", + ] + self.assertCountEqual(span_names_list, expected) + span_names_list.sort() + expected.sort() + self.assertListEqual(span_names_list, expected) + expected_parent = next( + span for span in span_list if span.name == "asyncio_test" + ) + # FIXME + for span in span_list: + if span is expected_parent: + continue + self.assertEqual(span.parent, expected_parent) From 212b14bb5e234a9aa5e283a09a00f7cbcceaa221 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 6 Feb 2020 17:15:45 -0600 Subject: [PATCH 29/56] Test --- opentelemetry-sdk/test | 1 + 1 file changed, 1 insertion(+) create mode 100644 opentelemetry-sdk/test diff --git a/opentelemetry-sdk/test b/opentelemetry-sdk/test new file mode 100644 index 0000000000..9daeafb986 --- /dev/null +++ b/opentelemetry-sdk/test @@ -0,0 +1 @@ +test From dbfeb987d773ba503803522de6dd5875571bdc02 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 6 Feb 2020 17:16:40 -0600 Subject: [PATCH 30/56] Revert "Test" This reverts commit 212b14bb5e234a9aa5e283a09a00f7cbcceaa221. --- opentelemetry-sdk/test | 1 - 1 file changed, 1 deletion(-) delete mode 100644 opentelemetry-sdk/test diff --git a/opentelemetry-sdk/test b/opentelemetry-sdk/test deleted file mode 100644 index 9daeafb986..0000000000 --- a/opentelemetry-sdk/test +++ /dev/null @@ -1 +0,0 @@ -test From 2235bc37f35011670b9d06fd894340a27be08d40 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Thu, 6 Feb 2020 15:36:24 -0800 Subject: [PATCH 31/56] fix threadlocal behaviour, more feedback changes Signed-off-by: Alex Boten --- .../src/opentelemetry/context/__init__.py | 11 +++++++---- .../src/opentelemetry/context/default_context.py | 3 +-- .../sdk/context/contextvars_context.py | 14 +++++++++++--- .../sdk/context/threadlocal_context.py | 13 +++++++++++++ opentelemetry-sdk/tests/context/test_threads.py | 4 ++-- 5 files changed, 34 insertions(+), 11 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index e9bfc6ca9e..f08931fed5 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -114,17 +114,20 @@ def with_current_context( Capture the current context and apply it to the provided func. """ - caller_context = get_current().copy() + caller_context = get_current().snapshot() def call_with_current_context( *args: "object", **kwargs: "object" ) -> "object": try: - backup_context = get_current().copy() - set_current(caller_context) + backup = get_current().snapshot() + new_context = get_current().copy() + new_context.apply(caller_context) + set_current(new_context) return func(*args, **kwargs) finally: - set_current(backup_context) + new_context.apply(backup) + set_current(new_context) return call_with_current_context diff --git a/opentelemetry-api/src/opentelemetry/context/default_context.py b/opentelemetry-api/src/opentelemetry/context/default_context.py index 5daecfa0b4..02d8d76114 100644 --- a/opentelemetry-api/src/opentelemetry/context/default_context.py +++ b/opentelemetry-api/src/opentelemetry/context/default_context.py @@ -35,8 +35,7 @@ def get_value(self, key: str) -> "object": def remove_value(self, key: str) -> None: """Remove a value from this context""" - if key in self._values: - self._values.pop(key) + self._values.pop(key, None) __all__ = ["DefaultContext"] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py index 9c4d736fff..601b9cbba6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py @@ -47,14 +47,13 @@ def get_value(self, key: str) -> "object": if key in self._contextvars: try: return self._contextvars[key].get() - except LookupError: + except (KeyError, LookupError): pass return None def remove_value(self, key: str) -> None: """Remove a value from this context""" - if key in self._contextvars.keys(): - self._contextvars.pop(key) + self._contextvars.pop(key, None) def copy(self) -> Context: """Return a copy of this context""" @@ -66,5 +65,14 @@ def copy(self) -> Context: return context_copy + def snapshot(self) -> typing.Dict: + return dict( + (key, value.get()) for key, value in self._contextvars.items() + ) + + def apply(self, snapshot: typing.Dict) -> None: + for name in snapshot: + self.set_value(name, snapshot[name]) + __all__ = ["ContextVarsContext"] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py index 69235bf60b..145891c653 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py @@ -13,6 +13,7 @@ # limitations under the License. import threading +import typing from copy import copy from opentelemetry.context import Context @@ -53,3 +54,15 @@ def copy(self) -> Context: context_copy.set_value(key, copy(value)) return context_copy + + def snapshot(self) -> typing.Dict: + return dict( + (key, value) for key, value in self._thread_local.__dict__.items() + ) + + def apply(self, snapshot: typing.Dict) -> None: + for name in snapshot: + self.set_value(name, snapshot[name]) + + +__all__ = ["ThreadLocalContext"] diff --git a/opentelemetry-sdk/tests/context/test_threads.py b/opentelemetry-sdk/tests/context/test_threads.py index dfbe78d4ef..f747a1c71d 100644 --- a/opentelemetry-sdk/tests/context/test_threads.py +++ b/opentelemetry-sdk/tests/context/test_threads.py @@ -13,7 +13,7 @@ # limitations under the License. import unittest -from multiprocessing.dummy import Pool as ThreadPool +from multiprocessing.dummy import Pool from opentelemetry import context from opentelemetry.sdk import trace @@ -51,7 +51,7 @@ def tearDown(self): def test_with_threads(self): with self.tracer.start_as_current_span("threads_test"): - pool = ThreadPool(5) # create a thread pool + pool = Pool(5) # create a thread pool pool.map( context.with_current_context(self.do_work), self.span_names ) From 5dc9fd5a41d4675193862d0d241187872372915d Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Thu, 6 Feb 2020 21:36:45 -0800 Subject: [PATCH 32/56] fix context restore bug Signed-off-by: Alex Boten --- .../src/opentelemetry/context/context.py | 4 +--- .../src/opentelemetry/context/default_context.py | 6 ++++++ .../sdk/context/contextvars_context.py | 11 ++++++----- .../sdk/context/threadlocal_context.py | 5 ++++- .../src/opentelemetry/sdk/trace/export/__init__.py | 13 ++++++++++--- opentelemetry-sdk/tests/context/test_asyncio.py | 13 ++++++------- 6 files changed, 33 insertions(+), 19 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/context.py b/opentelemetry-api/src/opentelemetry/context/context.py index 90a32c71a8..af67294382 100644 --- a/opentelemetry-api/src/opentelemetry/context/context.py +++ b/opentelemetry-api/src/opentelemetry/context/context.py @@ -14,7 +14,6 @@ from abc import ABC, abstractmethod -from copy import deepcopy class Context(ABC): @@ -53,10 +52,9 @@ def remove_value(self, key: str) -> None: key: The key for the value to remove. """ + @abstractmethod def copy(self) -> "Context": """Return a copy of this context""" - return deepcopy(self) - __all__ = ["Context"] diff --git a/opentelemetry-api/src/opentelemetry/context/default_context.py b/opentelemetry-api/src/opentelemetry/context/default_context.py index 02d8d76114..f9a4d3af36 100644 --- a/opentelemetry-api/src/opentelemetry/context/default_context.py +++ b/opentelemetry-api/src/opentelemetry/context/default_context.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import typing +from copy import deepcopy from opentelemetry.context.context import Context @@ -37,5 +38,10 @@ def remove_value(self, key: str) -> None: """Remove a value from this context""" self._values.pop(key, None) + def copy(self) -> "Context": + """Return a copy of this context.""" + + return deepcopy(self) + __all__ = ["DefaultContext"] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py index 601b9cbba6..0ba2c83755 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py @@ -26,10 +26,8 @@ class ContextVarsContext(Context): - """ - An implementation of the Context interface - which wraps ContextVar under the hood. This is the prefered - implementation for usage with Python 3.5+ + """An implementation of the Context interface which wraps ContextVar under + the hood. This is the prefered implementation for usage with Python 3.5+ """ def __init__(self) -> None: @@ -61,7 +59,10 @@ def copy(self) -> Context: context_copy = ContextVarsContext() for key, value in self._contextvars.items(): - context_copy.set_value(key, copy(value.get())) + try: + context_copy.set_value(key, copy(value.get())) + except (KeyError, LookupError): + pass return context_copy diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py index 145891c653..917ad38fb0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py @@ -43,7 +43,10 @@ def get_value(self, key: str) -> "object": def remove_value(self, key: str) -> None: """Remove a value from this context""" - delattr(self._thread_local, key) + try: + delattr(self._thread_local, key) + except AttributeError: + pass def copy(self) -> Context: """Return a copy of this context""" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 677bb61e15..19f525cee3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -18,7 +18,12 @@ import typing from enum import Enum -from opentelemetry.context import remove_value, set_current, set_value +from opentelemetry.context import ( + get_current, + remove_value, + set_current, + set_value, +) from opentelemetry.util import time_ns from .. import Span, SpanProcessor @@ -73,13 +78,14 @@ def on_start(self, span: Span) -> None: pass def on_end(self, span: Span) -> None: + backup_context = get_current() set_current(set_value("suppress_instrumentation", True)) try: self.span_exporter.export((span,)) # pylint: disable=broad-except except Exception: logger.exception("Exception while exporting Span.") - set_current(remove_value("suppress_instrumentation")) + set_current(backup_context) def shutdown(self) -> None: self.span_exporter.shutdown() @@ -183,6 +189,7 @@ def export(self) -> None: while idx < self.max_export_batch_size and self.queue: self.spans_list[idx] = self.queue.pop() idx += 1 + backup_context = get_current() set_current(set_value("suppress_instrumentation", True)) try: # Ignore type b/c the Optional[None]+slicing is too "clever" @@ -191,7 +198,7 @@ def export(self) -> None: # pylint: disable=broad-except except Exception: logger.exception("Exception while exporting Span batch.") - set_current(remove_value("suppress_instrumentation")) + set_current(backup_context) # clean up list for index in range(idx): self.spans_list[index] = None diff --git a/opentelemetry-sdk/tests/context/test_asyncio.py b/opentelemetry-sdk/tests/context/test_asyncio.py index 9c38762107..3c8bc63e1a 100644 --- a/opentelemetry-sdk/tests/context/test_asyncio.py +++ b/opentelemetry-sdk/tests/context/test_asyncio.py @@ -71,12 +71,12 @@ def test_with_asyncio(self): for name in _SPAN_NAMES: self.submit_another_task(name) - stop_loop_when( - self.loop, - lambda: len(self.memory_exporter.get_finished_spans()) >= 6, - timeout=5.0, - ) - self.loop.run_forever() + stop_loop_when( + self.loop, + lambda: len(self.memory_exporter.get_finished_spans()) >= 5, + timeout=5.0, + ) + self.loop.run_forever() span_list = self.memory_exporter.get_finished_spans() span_names_list = [span.name for span in span_list] expected = [ @@ -94,7 +94,6 @@ def test_with_asyncio(self): expected_parent = next( span for span in span_list if span.name == "asyncio_test" ) - # FIXME for span in span_list: if span is expected_parent: continue From 02c7c4cf179aee5e5ce70986beaa2f202641c5a6 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Thu, 6 Feb 2020 22:52:08 -0800 Subject: [PATCH 33/56] simplifying code, removing unnecessary methods Signed-off-by: Alex Boten --- .../src/opentelemetry/context/context.py | 10 ++++- .../opentelemetry/context/default_context.py | 13 +++++- .../distributedcontext/__init__.py | 6 +-- .../trace/propagation/__init__.py | 45 ++----------------- .../sdk/context/threadlocal_context.py | 4 +- .../src/opentelemetry/sdk/trace/__init__.py | 23 +++++----- 6 files changed, 38 insertions(+), 63 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/context.py b/opentelemetry-api/src/opentelemetry/context/context.py index af67294382..a719b0c23a 100644 --- a/opentelemetry-api/src/opentelemetry/context/context.py +++ b/opentelemetry-api/src/opentelemetry/context/context.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. - +import typing from abc import ABC, abstractmethod @@ -56,5 +56,13 @@ def remove_value(self, key: str) -> None: def copy(self) -> "Context": """Return a copy of this context""" + @abstractmethod + def snapshot(self) -> typing.Dict[str, "object"]: + """Returns the contents of a context.""" + + @abstractmethod + def apply(self, snapshot: typing.Dict[str, "object"]) -> None: + """Sets the contents of a context.""" + __all__ = ["Context"] diff --git a/opentelemetry-api/src/opentelemetry/context/default_context.py b/opentelemetry-api/src/opentelemetry/context/default_context.py index f9a4d3af36..ef5df78fef 100644 --- a/opentelemetry-api/src/opentelemetry/context/default_context.py +++ b/opentelemetry-api/src/opentelemetry/context/default_context.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import typing -from copy import deepcopy +from copy import copy from opentelemetry.context.context import Context @@ -40,8 +40,17 @@ def remove_value(self, key: str) -> None: def copy(self) -> "Context": """Return a copy of this context.""" + context_copy = DefaultContext() + for key, value in self._values.items(): + context_copy.set_value(key, copy(value)) + return context_copy - return deepcopy(self) + def snapshot(self) -> typing.Dict[str, "object"]: + return dict((key, value) for key, value in self._values.items()) + + def apply(self, snapshot: typing.Dict[str, "object"]) -> None: + for name in snapshot: + self.set_value(name, snapshot[name]) __all__ = ["DefaultContext"] diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py index 36232225f8..8f1020e462 100644 --- a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py +++ b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py @@ -17,7 +17,7 @@ import typing from contextlib import contextmanager -from opentelemetry.context import Context, get_current, get_value +from opentelemetry.context import Context, get_value, set_current, set_value PRINTABLE = frozenset( itertools.chain( @@ -141,6 +141,4 @@ def distributed_context_from_context( def with_distributed_context( dctx: DistributedContext, context: typing.Optional[Context] = None ) -> None: - if context is not None: - return context.set_value(_DISTRIBUTED_CONTEXT_KEY, dctx) - return get_current().set_value(_DISTRIBUTED_CONTEXT_KEY, dctx) + set_current(set_value(_DISTRIBUTED_CONTEXT_KEY, dctx, context=context)) diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py index d700d82912..9fd2d82e1a 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py @@ -13,52 +13,15 @@ # limitations under the License. from typing import Optional -from opentelemetry.context import Context, get_current, get_value +from opentelemetry.context import Context, get_value, set_current, set_value from opentelemetry.trace import INVALID_SPAN_CONTEXT, Span, SpanContext _SPAN_CONTEXT_KEY = "extracted-span-context" _SPAN_KEY = "current-span" -def span_context_from_context( - context: Optional[Context] = None, -) -> SpanContext: - span = span_from_context(context=context) - if span is not None: - return span.get_context() - sc = get_value(_SPAN_CONTEXT_KEY, context=context) - if sc is not None: - return sc - - return INVALID_SPAN_CONTEXT - - -def with_span_context( - span_context: SpanContext, context: Optional[Context] = None -) -> None: - if context is not None: - context.set_value(_SPAN_CONTEXT_KEY, span_context) - return get_current().set_value(_SPAN_CONTEXT_KEY, span_context) - - -def _get_span_key(name: Optional[str] = None) -> str: +def get_span_key(tracer_source_id: Optional[str] = None) -> str: key = _SPAN_KEY - if name is not None: - key = "{}-{}".format(key, name) + if tracer_source_id is not None: + key = "{}-{}".format(key, tracer_source_id) return key - - -def span_from_context( - context: Optional[Context] = None, name: Optional[str] = None -) -> Span: - key = _get_span_key(name) - return get_value(key, context) # type: ignore - - -def with_span( - span: Span, context: Optional[Context] = None, name: Optional[str] = None -) -> None: - key = _get_span_key(name) - if context is not None: - return context.set_value(key, span) - return get_current().set_value(key, span) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py index 917ad38fb0..ffbffbcc43 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py @@ -58,12 +58,12 @@ def copy(self) -> Context: return context_copy - def snapshot(self) -> typing.Dict: + def snapshot(self) -> typing.Dict[str, "object"]: return dict( (key, value) for key, value in self._thread_local.__dict__.items() ) - def apply(self, snapshot: typing.Dict) -> None: + def apply(self, snapshot: typing.Dict[str, "object"]) -> None: for name in snapshot: self.set_value(name, snapshot[name]) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 89f9ce7c20..0bc228bdcf 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -22,12 +22,12 @@ from types import TracebackType from typing import Iterator, Optional, Sequence, Tuple, Type +from opentelemetry import context as context_api from opentelemetry import trace as trace_api -from opentelemetry.context import Context, get_value from opentelemetry.sdk import util from opentelemetry.sdk.util import BoundedDict, BoundedList from opentelemetry.trace import SpanContext, sampling -from opentelemetry.trace.propagation import span_from_context, with_span +from opentelemetry.trace.propagation import get_span_key from opentelemetry.trace.status import Status, StatusCanonicalCode from opentelemetry.util import time_ns, types @@ -529,12 +529,14 @@ def use_span( ) -> Iterator[trace_api.Span]: """See `opentelemetry.trace.Tracer.use_span`.""" try: - span_snapshot = self.source.get_current_span() - self.source.set_current_span(span) + context_snapshot = context_api.get_current() + context_api.set_current( + context_api.set_value(self.source.key, span) + ) try: yield span finally: - self.source.set_current_span(span_snapshot) + context_api.set_current(context_snapshot) except Exception as error: # pylint: disable=broad-except if ( @@ -565,7 +567,7 @@ def __init__( ): # TODO: How should multiple TracerSources behave? Should they get their own contexts? # This could be done by adding `str(id(self))` to the slot name. - self._name = str(id(self)) + self.key = get_span_key(tracer_source_id=str(id(self))) self._active_span_processor = MultiSpanProcessor() self.sampler = sampler self._atexit_handler = None @@ -587,13 +589,8 @@ def get_tracer( ), ) - def get_current_span(self, context: Optional[Context] = None) -> Span: - return span_from_context(context=context, name=self._name) - - def set_current_span( - self, span, context: Optional[Context] = None - ) -> Span: - return with_span(span, context=context, name=self._name) + def get_current_span(self) -> Span: + return context_api.get_value(self.key) # type: ignore def add_span_processor(self, span_processor: SpanProcessor) -> None: """Registers a new :class:`SpanProcessor` for this `TracerSource`. From ae2cbc4a88d1dfb050b70d6a7444ae0657213974 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Thu, 6 Feb 2020 23:16:41 -0800 Subject: [PATCH 34/56] cleaning up docs --- .../src/opentelemetry/context/__init__.py | 22 +++++++------------ .../src/opentelemetry/context/context.py | 20 ++++++++--------- .../opentelemetry/context/default_context.py | 13 ++++++----- .../sdk/context/contextvars_context.py | 10 +++++---- .../sdk/context/threadlocal_context.py | 13 ++++++----- .../tests/context/test_asyncio.py | 3 +-- 6 files changed, 39 insertions(+), 42 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index f08931fed5..b01f92df7c 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -25,8 +25,7 @@ def get_value(key: str, context: typing.Optional[Context] = None) -> "object": - """ - To access the local state of an concern, the Context API + """To access the local state of an concern, the Context API provides a function which takes a context and a key as input, and returns a value. @@ -40,8 +39,7 @@ def get_value(key: str, context: typing.Optional[Context] = None) -> "object": def set_value( key: str, value: "object", context: typing.Optional[Context] = None ) -> Context: - """ - To record the local state of a cross-cutting concern, the + """To record the local state of a cross-cutting concern, the Context API provides a function which takes a context, a key, and a value as input, and returns an updated context which contains the new value. @@ -59,9 +57,9 @@ def set_value( def remove_value( key: str, context: typing.Optional[Context] = None ) -> Context: - """ - To remove a value, this method returns a new context with the key cleared. - Note that the removed value still remains present in the old context. + """To remove a value, this method returns a new context with the key + cleared. Note that the removed value still remains present in the old + context. Args: key: The key of the entry to remove @@ -73,8 +71,7 @@ def remove_value( def get_current() -> Context: - """ - To access the context associated with program execution, + """To access the context associated with program execution, the Context API provides a function which takes no arguments and returns a Context. """ @@ -96,8 +93,7 @@ def get_current() -> Context: def set_current(context: Context) -> None: - """ - To associate a context with program execution, the Context + """To associate a context with program execution, the Context API provides a function which takes a Context. Args: @@ -110,9 +106,7 @@ def set_current(context: Context) -> None: def with_current_context( func: typing.Callable[..., "object"] ) -> typing.Callable[..., "object"]: - """ - Capture the current context and apply it to the provided func. - """ + """Capture the current context and apply it to the provided func.""" caller_context = get_current().snapshot() diff --git a/opentelemetry-api/src/opentelemetry/context/context.py b/opentelemetry-api/src/opentelemetry/context/context.py index a719b0c23a..401a481366 100644 --- a/opentelemetry-api/src/opentelemetry/context/context.py +++ b/opentelemetry-api/src/opentelemetry/context/context.py @@ -17,8 +17,7 @@ class Context(ABC): - """ - The Context interface provides a wrapper for the different + """The Context interface provides a wrapper for the different mechanisms that are used to propagate context in Python. Implementations can be made available via entry_points and selected through environment variables. @@ -26,8 +25,7 @@ class Context(ABC): @abstractmethod def set_value(self, key: str, value: "object") -> None: - """ - Set a value in this context + """Set a value in this context. Args: key: The key for the value to set. @@ -36,8 +34,7 @@ def set_value(self, key: str, value: "object") -> None: @abstractmethod def get_value(self, key: str) -> "object": - """ - Get a value from this context + """Get a value from this context. Args: key: The key for the value to retrieve. @@ -45,8 +42,7 @@ def get_value(self, key: str) -> "object": @abstractmethod def remove_value(self, key: str) -> None: - """ - Remove a value from this context + """Remove a value from this context. Args: key: The key for the value to remove. @@ -54,7 +50,7 @@ def remove_value(self, key: str) -> None: @abstractmethod def copy(self) -> "Context": - """Return a copy of this context""" + """Return a copy of this context.""" @abstractmethod def snapshot(self) -> typing.Dict[str, "object"]: @@ -62,7 +58,11 @@ def snapshot(self) -> typing.Dict[str, "object"]: @abstractmethod def apply(self, snapshot: typing.Dict[str, "object"]) -> None: - """Sets the contents of a context.""" + """Sets the contents of a context. + + Args: + snapshot: The contents to set. + """ __all__ = ["Context"] diff --git a/opentelemetry-api/src/opentelemetry/context/default_context.py b/opentelemetry-api/src/opentelemetry/context/default_context.py index ef5df78fef..d700e036c9 100644 --- a/opentelemetry-api/src/opentelemetry/context/default_context.py +++ b/opentelemetry-api/src/opentelemetry/context/default_context.py @@ -18,8 +18,7 @@ class DefaultContext(Context): - """ - A default implementation of the Context interface using + """A default implementation of the Context interface using a dictionary to store values. """ @@ -27,28 +26,30 @@ def __init__(self) -> None: self._values = {} # type: typing.Dict[str, object] def set_value(self, key: str, value: "object") -> None: - """Set a value in this context""" + """See `opentelemetry.context.Context.set_value`.""" self._values[key] = value def get_value(self, key: str) -> "object": - """Get a value from this context""" + """See `opentelemetry.context.Context.get_value`.""" return self._values.get(key) def remove_value(self, key: str) -> None: - """Remove a value from this context""" + """See `opentelemetry.context.Context.remove_value`.""" self._values.pop(key, None) def copy(self) -> "Context": - """Return a copy of this context.""" + """See `opentelemetry.context.Context.copy`.""" context_copy = DefaultContext() for key, value in self._values.items(): context_copy.set_value(key, copy(value)) return context_copy def snapshot(self) -> typing.Dict[str, "object"]: + """See `opentelemetry.context.Context.snapshot`.""" return dict((key, value) for key, value in self._values.items()) def apply(self, snapshot: typing.Dict[str, "object"]) -> None: + """See `opentelemetry.context.Context.apply`.""" for name in snapshot: self.set_value(name, snapshot[name]) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py index 0ba2c83755..954442b5b0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py @@ -34,14 +34,14 @@ def __init__(self) -> None: self._contextvars = {} # type: typing.Dict[str, ContextVar[object]] def set_value(self, key: str, value: "object") -> None: - """Set a value in this context""" + """See `opentelemetry.context.Context.set_value`.""" if key not in self._contextvars.keys(): self._contextvars[key] = ContextVar(key) self._contextvars[key].set(value) def get_value(self, key: str) -> "object": - """Get a value from this context""" + """See `opentelemetry.context.Context.get_value`.""" if key in self._contextvars: try: return self._contextvars[key].get() @@ -50,11 +50,11 @@ def get_value(self, key: str) -> "object": return None def remove_value(self, key: str) -> None: - """Remove a value from this context""" + """See `opentelemetry.context.Context.remove_value`.""" self._contextvars.pop(key, None) def copy(self) -> Context: - """Return a copy of this context""" + """See `opentelemetry.context.Context.copy`.""" context_copy = ContextVarsContext() @@ -67,11 +67,13 @@ def copy(self) -> Context: return context_copy def snapshot(self) -> typing.Dict: + """See `opentelemetry.context.Context.snapshot`.""" return dict( (key, value.get()) for key, value in self._contextvars.items() ) def apply(self, snapshot: typing.Dict) -> None: + """See `opentelemetry.context.Context.apply`.""" for name in snapshot: self.set_value(name, snapshot[name]) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py index ffbffbcc43..e4f805980f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py @@ -20,8 +20,7 @@ class ThreadLocalContext(Context): - """ - An implementation of the Context interface + """An implementation of the Context interface which uses thread-local storage under the hood. This implementation is available for usage with Python 3.4. """ @@ -30,11 +29,11 @@ def __init__(self) -> None: self._thread_local = threading.local() def set_value(self, key: str, value: "object") -> None: - """Set a value in this context""" + """See `opentelemetry.context.Context.set_value`.""" setattr(self._thread_local, key, value) def get_value(self, key: str) -> "object": - """Get a value from this context""" + """See `opentelemetry.context.Context.get_value`.""" try: got = getattr(self._thread_local, key) # type: object return got @@ -42,14 +41,14 @@ def get_value(self, key: str) -> "object": return None def remove_value(self, key: str) -> None: - """Remove a value from this context""" + """See `opentelemetry.context.Context.remove_value`.""" try: delattr(self._thread_local, key) except AttributeError: pass def copy(self) -> Context: - """Return a copy of this context""" + """See `opentelemetry.context.Context.copy`.""" context_copy = ThreadLocalContext() @@ -59,11 +58,13 @@ def copy(self) -> Context: return context_copy def snapshot(self) -> typing.Dict[str, "object"]: + """See `opentelemetry.context.Context.snapshot`.""" return dict( (key, value) for key, value in self._thread_local.__dict__.items() ) def apply(self, snapshot: typing.Dict[str, "object"]) -> None: + """See `opentelemetry.context.Context.apply`.""" for name in snapshot: self.set_value(name, snapshot[name]) diff --git a/opentelemetry-sdk/tests/context/test_asyncio.py b/opentelemetry-sdk/tests/context/test_asyncio.py index 3c8bc63e1a..5a46a00865 100644 --- a/opentelemetry-sdk/tests/context/test_asyncio.py +++ b/opentelemetry-sdk/tests/context/test_asyncio.py @@ -33,8 +33,7 @@ def stop_loop_when(loop, cond_func, timeout=5.0): - """ - Registers a periodic callback that stops the loop when cond_func() == True. + """Registers a periodic callback that stops the loop when cond_func() == True. Compatible with both Tornado and asyncio. """ if cond_func() or timeout <= 0.0: From 01e0054b08499a26fe23a709b90eabe1fe6050dc Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Fri, 7 Feb 2020 14:49:25 -0800 Subject: [PATCH 35/56] Splitting the Context interface This interface was really attempting to do two things: - acting as an immutable Context object as per the otep - providing an interface on top of threadlocal/contextvars As part of this change: - RuntimeContext is the interface on top of threadlocal/contextvars - Context is now an immutable class Signed-off-by: Alex Boten --- opentelemetry-api/setup.py | 2 +- .../src/opentelemetry/context/__init__.py | 66 ++++++++++++------- .../src/opentelemetry/context/context.py | 8 +-- .../opentelemetry/context/default_context.py | 24 +++---- .../trace/propagation/__init__.py | 1 - opentelemetry-sdk/setup.py | 4 +- .../sdk/context/contextvars_context.py | 36 ++++------ .../sdk/context/threadlocal_context.py | 24 +++---- .../tests/context/test_asyncio.py | 18 +++-- .../tests/context/test_context.py | 23 +++++-- .../tests/context/test_threads.py | 10 ++- .../tests/trace/export/test_export.py | 7 -- 12 files changed, 128 insertions(+), 95 deletions(-) diff --git a/opentelemetry-api/setup.py b/opentelemetry-api/setup.py index 3d65aeb9e2..fad86f171b 100644 --- a/opentelemetry-api/setup.py +++ b/opentelemetry-api/setup.py @@ -60,7 +60,7 @@ "opentelemetry_context": [ "default_context = " "opentelemetry.context.default_context:" - "DefaultContext", + "DefaultRuntimeContext", ] }, ) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index b01f92df7c..3a5742b4b9 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -18,14 +18,31 @@ from pkg_resources import iter_entry_points -from opentelemetry.context.context import Context +from opentelemetry.context.context import RuntimeContext logger = logging.getLogger(__name__) +_CONTEXT_RUNTIME = None # type: typing.Optional[RuntimeContext] _CONTEXT = None # type: typing.Optional[Context] +class Context: + def __init__( + self, values: typing.Optional[typing.Dict[str, object]] = None + ): + if values: + self._data = values + else: + self._data = _CONTEXT_RUNTIME.snapshot() # type: ignore + + def get_value(self, key: str) -> "object": + return self._data.get(key) + + def snapshot(self) -> typing.Dict[str, object]: + return dict((key, value) for key, value in self._data.items()) + + def get_value(key: str, context: typing.Optional[Context] = None) -> "object": - """To access the local state of an concern, the Context API + """To access the local state of an concern, the RuntimeContext API provides a function which takes a context and a key as input, and returns a value. @@ -40,7 +57,7 @@ def set_value( key: str, value: "object", context: typing.Optional[Context] = None ) -> Context: """To record the local state of a cross-cutting concern, the - Context API provides a function which takes a context, a + RuntimeContext API provides a function which takes a context, a key, and a value as input, and returns an updated context which contains the new value. @@ -49,9 +66,11 @@ def set_value( value: The value of the entry to set context: The context to copy, if None, the current context is used """ - new_context = context.copy() if context else get_current().copy() - new_context.set_value(key, value) - return new_context + if context is None: + context = get_current() + new_values = context.snapshot() + new_values[key] = value + return Context(new_values) def remove_value( @@ -65,18 +84,20 @@ def remove_value( key: The key of the entry to remove context: The context to copy, if None, the current context is used """ - new_context = context.copy() if context else get_current().copy() - new_context.remove_value(key) - return new_context + if context is None: + context = get_current() + new_values = context.snapshot() + new_values.pop(key, None) + return Context(new_values) def get_current() -> Context: """To access the context associated with program execution, - the Context API provides a function which takes no arguments - and returns a Context. + the RuntimeContext API provides a function which takes no arguments + and returns a RuntimeContext. """ - global _CONTEXT # pylint: disable=global-statement - if _CONTEXT is None: + global _CONTEXT_RUNTIME # pylint: disable=global-statement + if _CONTEXT_RUNTIME is None: # FIXME use a better implementation of a configuration manager to avoid having # to get configuration values straight from environment variables @@ -84,11 +105,15 @@ def get_current() -> Context: "OPENTELEMETRY_CONTEXT", "default_context" ) # type: str try: - _CONTEXT = next( + _CONTEXT_RUNTIME = next( iter_entry_points("opentelemetry_context", configured_context) ).load()() except Exception: # pylint: disable=broad-except logger.error("Failed to load context: %s", configured_context) + + global _CONTEXT # pylint: disable=global-statement + if _CONTEXT is None: + set_current(Context()) return _CONTEXT # type: ignore @@ -108,20 +133,17 @@ def with_current_context( ) -> typing.Callable[..., "object"]: """Capture the current context and apply it to the provided func.""" - caller_context = get_current().snapshot() + caller_context = get_current() def call_with_current_context( *args: "object", **kwargs: "object" ) -> "object": try: - backup = get_current().snapshot() - new_context = get_current().copy() - new_context.apply(caller_context) - set_current(new_context) + backup = get_current() + set_current(caller_context) return func(*args, **kwargs) finally: - new_context.apply(backup) - set_current(new_context) + set_current(backup) return call_with_current_context @@ -132,5 +154,5 @@ def call_with_current_context( "remove_value", "get_current", "set_current", - "Context", + "RuntimeContext", ] diff --git a/opentelemetry-api/src/opentelemetry/context/context.py b/opentelemetry-api/src/opentelemetry/context/context.py index 401a481366..b5412c6c46 100644 --- a/opentelemetry-api/src/opentelemetry/context/context.py +++ b/opentelemetry-api/src/opentelemetry/context/context.py @@ -16,8 +16,8 @@ from abc import ABC, abstractmethod -class Context(ABC): - """The Context interface provides a wrapper for the different +class RuntimeContext(ABC): + """The RuntimeContext interface provides a wrapper for the different mechanisms that are used to propagate context in Python. Implementations can be made available via entry_points and selected through environment variables. @@ -49,7 +49,7 @@ def remove_value(self, key: str) -> None: """ @abstractmethod - def copy(self) -> "Context": + def copy(self) -> "RuntimeContext": """Return a copy of this context.""" @abstractmethod @@ -65,4 +65,4 @@ def apply(self, snapshot: typing.Dict[str, "object"]) -> None: """ -__all__ = ["Context"] +__all__ = ["RuntimeContext"] diff --git a/opentelemetry-api/src/opentelemetry/context/default_context.py b/opentelemetry-api/src/opentelemetry/context/default_context.py index d700e036c9..f7bcd11fcd 100644 --- a/opentelemetry-api/src/opentelemetry/context/default_context.py +++ b/opentelemetry-api/src/opentelemetry/context/default_context.py @@ -14,11 +14,11 @@ import typing from copy import copy -from opentelemetry.context.context import Context +from opentelemetry.context.context import RuntimeContext -class DefaultContext(Context): - """A default implementation of the Context interface using +class DefaultRuntimeContext(RuntimeContext): + """A default implementation of the RuntimeContext interface using a dictionary to store values. """ @@ -26,32 +26,32 @@ def __init__(self) -> None: self._values = {} # type: typing.Dict[str, object] def set_value(self, key: str, value: "object") -> None: - """See `opentelemetry.context.Context.set_value`.""" + """See `opentelemetry.context.RuntimeContext.set_value`.""" self._values[key] = value def get_value(self, key: str) -> "object": - """See `opentelemetry.context.Context.get_value`.""" + """See `opentelemetry.context.RuntimeContext.get_value`.""" return self._values.get(key) def remove_value(self, key: str) -> None: - """See `opentelemetry.context.Context.remove_value`.""" + """See `opentelemetry.context.RuntimeContext.remove_value`.""" self._values.pop(key, None) - def copy(self) -> "Context": - """See `opentelemetry.context.Context.copy`.""" - context_copy = DefaultContext() + def copy(self) -> "RuntimeContext": + """See `opentelemetry.context.RuntimeContext.copy`.""" + context_copy = DefaultRuntimeContext() for key, value in self._values.items(): context_copy.set_value(key, copy(value)) return context_copy def snapshot(self) -> typing.Dict[str, "object"]: - """See `opentelemetry.context.Context.snapshot`.""" + """See `opentelemetry.context.RuntimeContext.snapshot`.""" return dict((key, value) for key, value in self._values.items()) def apply(self, snapshot: typing.Dict[str, "object"]) -> None: - """See `opentelemetry.context.Context.apply`.""" + """See `opentelemetry.context.RuntimeContext.apply`.""" for name in snapshot: self.set_value(name, snapshot[name]) -__all__ = ["DefaultContext"] +__all__ = ["DefaultRuntimeContext"] diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py index 9fd2d82e1a..602088a2c1 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py @@ -13,7 +13,6 @@ # limitations under the License. from typing import Optional -from opentelemetry.context import Context, get_value, set_current, set_value from opentelemetry.trace import INVALID_SPAN_CONTEXT, Span, SpanContext _SPAN_CONTEXT_KEY = "extracted-span-context" diff --git a/opentelemetry-sdk/setup.py b/opentelemetry-sdk/setup.py index 4bd81111e0..7e88bb3bfe 100644 --- a/opentelemetry-sdk/setup.py +++ b/opentelemetry-sdk/setup.py @@ -60,10 +60,10 @@ "opentelemetry_context": [ "contextvars_context = " "opentelemetry.sdk.context.contextvars_context:" - "ContextVarsContext", + "ContextVarsRuntimeContext", "threadlocal_context = " "opentelemetry.sdk.context.threadlocal_context:" - "ThreadLocalContext", + "ThreadLocalRuntimeContext", ] }, ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py index 954442b5b0..80130e3044 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py @@ -13,10 +13,9 @@ # limitations under the License. import typing from contextvars import ContextVar -from copy import copy from sys import version_info -from opentelemetry.context.context import Context +from opentelemetry.context.context import RuntimeContext if (3, 5, 3) <= version_info < (3, 7): import aiocontextvars # type: ignore # pylint:disable=unused-import @@ -25,8 +24,8 @@ import opentelemetry.sdk.context.aiocontextvarsfix # pylint:disable=unused-import -class ContextVarsContext(Context): - """An implementation of the Context interface which wraps ContextVar under +class ContextVarsRuntimeContext(RuntimeContext): + """An implementation of the RuntimeContext interface which wraps ContextVar under the hood. This is the prefered implementation for usage with Python 3.5+ """ @@ -34,14 +33,14 @@ def __init__(self) -> None: self._contextvars = {} # type: typing.Dict[str, ContextVar[object]] def set_value(self, key: str, value: "object") -> None: - """See `opentelemetry.context.Context.set_value`.""" + """See `opentelemetry.context.RuntimeContext.set_value`.""" if key not in self._contextvars.keys(): self._contextvars[key] = ContextVar(key) self._contextvars[key].set(value) def get_value(self, key: str) -> "object": - """See `opentelemetry.context.Context.get_value`.""" + """See `opentelemetry.context.RuntimeContext.get_value`.""" if key in self._contextvars: try: return self._contextvars[key].get() @@ -50,32 +49,25 @@ def get_value(self, key: str) -> "object": return None def remove_value(self, key: str) -> None: - """See `opentelemetry.context.Context.remove_value`.""" + """See `opentelemetry.context.RuntimeContext.remove_value`.""" self._contextvars.pop(key, None) - def copy(self) -> Context: - """See `opentelemetry.context.Context.copy`.""" - - context_copy = ContextVarsContext() - - for key, value in self._contextvars.items(): - try: - context_copy.set_value(key, copy(value.get())) - except (KeyError, LookupError): - pass - - return context_copy + def copy(self) -> RuntimeContext: + """See `opentelemetry.context.RuntimeContext.copy`.""" + # under the hood, ContextVars returns a copy on set + # we dont need to do any copying ourselves + return self def snapshot(self) -> typing.Dict: - """See `opentelemetry.context.Context.snapshot`.""" + """See `opentelemetry.context.RuntimeContext.snapshot`.""" return dict( (key, value.get()) for key, value in self._contextvars.items() ) def apply(self, snapshot: typing.Dict) -> None: - """See `opentelemetry.context.Context.apply`.""" + """See `opentelemetry.context.RuntimeContext.apply`.""" for name in snapshot: self.set_value(name, snapshot[name]) -__all__ = ["ContextVarsContext"] +__all__ = ["ContextVarsRuntimeContext"] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py index e4f805980f..7ecee74a78 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py @@ -16,11 +16,11 @@ import typing from copy import copy -from opentelemetry.context import Context +from opentelemetry.context import RuntimeContext -class ThreadLocalContext(Context): - """An implementation of the Context interface +class ThreadLocalRuntimeContext(RuntimeContext): + """An implementation of the RuntimeContext interface which uses thread-local storage under the hood. This implementation is available for usage with Python 3.4. """ @@ -29,11 +29,11 @@ def __init__(self) -> None: self._thread_local = threading.local() def set_value(self, key: str, value: "object") -> None: - """See `opentelemetry.context.Context.set_value`.""" + """See `opentelemetry.context.RuntimeContext.set_value`.""" setattr(self._thread_local, key, value) def get_value(self, key: str) -> "object": - """See `opentelemetry.context.Context.get_value`.""" + """See `opentelemetry.context.RuntimeContext.get_value`.""" try: got = getattr(self._thread_local, key) # type: object return got @@ -41,16 +41,16 @@ def get_value(self, key: str) -> "object": return None def remove_value(self, key: str) -> None: - """See `opentelemetry.context.Context.remove_value`.""" + """See `opentelemetry.context.RuntimeContext.remove_value`.""" try: delattr(self._thread_local, key) except AttributeError: pass - def copy(self) -> Context: - """See `opentelemetry.context.Context.copy`.""" + def copy(self) -> RuntimeContext: + """See `opentelemetry.context.RuntimeContext.copy`.""" - context_copy = ThreadLocalContext() + context_copy = ThreadLocalRuntimeContext() for key, value in self._thread_local.__dict__.items(): context_copy.set_value(key, copy(value)) @@ -58,15 +58,15 @@ def copy(self) -> Context: return context_copy def snapshot(self) -> typing.Dict[str, "object"]: - """See `opentelemetry.context.Context.snapshot`.""" + """See `opentelemetry.context.RuntimeContext.snapshot`.""" return dict( (key, value) for key, value in self._thread_local.__dict__.items() ) def apply(self, snapshot: typing.Dict[str, "object"]) -> None: - """See `opentelemetry.context.Context.apply`.""" + """See `opentelemetry.context.RuntimeContext.apply`.""" for name in snapshot: self.set_value(name, snapshot[name]) -__all__ = ["ThreadLocalContext"] +__all__ = ["ThreadLocalRuntimeContext"] diff --git a/opentelemetry-sdk/tests/context/test_asyncio.py b/opentelemetry-sdk/tests/context/test_asyncio.py index 5a46a00865..9776327253 100644 --- a/opentelemetry-sdk/tests/context/test_asyncio.py +++ b/opentelemetry-sdk/tests/context/test_asyncio.py @@ -14,10 +14,13 @@ import asyncio import unittest +from unittest.mock import patch -from opentelemetry import context +from opentelemetry import context as context_api from opentelemetry.sdk import trace -from opentelemetry.sdk.context.contextvars_context import ContextVarsContext +from opentelemetry.sdk.context.contextvars_context import ( + ContextVarsRuntimeContext, +) from opentelemetry.sdk.trace import export from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, @@ -47,14 +50,14 @@ def stop_loop_when(loop, cond_func, timeout=5.0): class TestAsyncio(unittest.TestCase): async def task(self, name): with self.tracer.start_as_current_span(name): - context.set_value("say-something", "bar") + context_api.set_value("say-something", "bar") def submit_another_task(self, name): self.loop.create_task(self.task(name)) def setUp(self): - self.previous_context = context.get_current() - context.set_current(ContextVarsContext()) + self.previous_context = context_api.get_current() + context_api.set_current(context_api.Context()) self.tracer_source = trace.TracerSource() self.tracer = self.tracer_source.get_tracer(__name__) self.memory_exporter = InMemorySpanExporter() @@ -63,8 +66,11 @@ def setUp(self): self.loop = asyncio.get_event_loop() def tearDown(self): - context.set_current(self.previous_context) + context_api.set_current(self.previous_context) + @patch( + "opentelemetry.context._CONTEXT_RUNTIME", ContextVarsRuntimeContext() + ) def test_with_asyncio(self): with self.tracer.start_as_current_span("asyncio_test"): for name in _SPAN_NAMES: diff --git a/opentelemetry-sdk/tests/context/test_context.py b/opentelemetry-sdk/tests/context/test_context.py index d2af5d5fc6..4447a1c61c 100644 --- a/opentelemetry-sdk/tests/context/test_context.py +++ b/opentelemetry-sdk/tests/context/test_context.py @@ -13,10 +13,15 @@ # limitations under the License. import unittest +from unittest.mock import patch from opentelemetry import context -from opentelemetry.sdk.context.contextvars_context import ContextVarsContext -from opentelemetry.sdk.context.threadlocal_context import ThreadLocalContext +from opentelemetry.sdk.context.contextvars_context import ( + ContextVarsRuntimeContext, +) +from opentelemetry.sdk.context.threadlocal_context import ( + ThreadLocalRuntimeContext, +) def do_work() -> None: @@ -26,11 +31,13 @@ def do_work() -> None: class TestThreadLocalContext(unittest.TestCase): def setUp(self): self.previous_context = context.get_current() - context.set_current(ThreadLocalContext()) def tearDown(self): context.set_current(self.previous_context) + @patch( + "opentelemetry.context._CONTEXT_RUNTIME", ThreadLocalRuntimeContext() + ) def test_context(self): self.assertIsNone(context.get_value("say-something")) empty_context = context.get_current() @@ -45,6 +52,9 @@ def test_context(self): self.assertEqual(second_context.get_value("say-something"), "foo") self.assertEqual(third_context.get_value("say-something"), "bar") + @patch( + "opentelemetry.context._CONTEXT_RUNTIME", ThreadLocalRuntimeContext() + ) def test_set_value(self): first = context.set_value("a", "yyy") second = context.set_value("a", "zzz") @@ -58,11 +68,13 @@ def test_set_value(self): class TestContextVarsContext(unittest.TestCase): def setUp(self): self.previous_context = context.get_current() - context.set_current(ContextVarsContext()) def tearDown(self): context.set_current(self.previous_context) + @patch( + "opentelemetry.context._CONTEXT_RUNTIME", ContextVarsRuntimeContext() + ) def test_context(self): self.assertIsNone(context.get_value("say-something")) empty_context = context.get_current() @@ -77,6 +89,9 @@ def test_context(self): self.assertEqual(second_context.get_value("say-something"), "foo") self.assertEqual(third_context.get_value("say-something"), "bar") + @patch( + "opentelemetry.context._CONTEXT_RUNTIME", ContextVarsRuntimeContext() + ) def test_set_value(self): first = context.set_value("a", "yyy") second = context.set_value("a", "zzz") diff --git a/opentelemetry-sdk/tests/context/test_threads.py b/opentelemetry-sdk/tests/context/test_threads.py index f747a1c71d..60a435c3cb 100644 --- a/opentelemetry-sdk/tests/context/test_threads.py +++ b/opentelemetry-sdk/tests/context/test_threads.py @@ -14,10 +14,13 @@ import unittest from multiprocessing.dummy import Pool +from unittest.mock import patch from opentelemetry import context from opentelemetry.sdk import trace -from opentelemetry.sdk.context.threadlocal_context import ThreadLocalContext +from opentelemetry.sdk.context.threadlocal_context import ( + ThreadLocalRuntimeContext, +) from opentelemetry.sdk.trace import export from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, @@ -39,7 +42,7 @@ def do_work(self, name="default"): def setUp(self): self.previous_context = context.get_current() - context.set_current(ThreadLocalContext()) + context.set_current(context.Context()) self.tracer_source = trace.TracerSource() self.tracer = self.tracer_source.get_tracer(__name__) self.memory_exporter = InMemorySpanExporter() @@ -49,6 +52,9 @@ def setUp(self): def tearDown(self): context.set_current(self.previous_context) + @patch( + "opentelemetry.context._CONTEXT_RUNTIME", ThreadLocalRuntimeContext() + ) def test_with_threads(self): with self.tracer.start_as_current_span("threads_test"): pool = Pool(5) # create a thread pool diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index b98ec51f47..43299ebe6a 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -17,9 +17,7 @@ from unittest import mock from opentelemetry import trace as trace_api -from opentelemetry.context import set_current from opentelemetry.sdk import trace -from opentelemetry.sdk.context.contextvars_context import ContextVarsContext from opentelemetry.sdk.trace import export @@ -99,11 +97,6 @@ def _create_start_and_end_span(name, span_processor): class TestBatchExportSpanProcessor(unittest.TestCase): - @classmethod - def setUpClass(cls): - # reset the current context - set_current(ContextVarsContext()) - def test_shutdown(self): spans_names_list = [] From ebcf0bd201b9690c024137ebf9dac37aef2aa9f9 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Sun, 9 Feb 2020 21:15:15 -0800 Subject: [PATCH 36/56] update RuntimeContext when setting current Signed-off-by: Alex Boten --- .../src/opentelemetry/context/__init__.py | 11 ++++------- .../src/opentelemetry/context/default_context.py | 3 +++ .../sdk/context/contextvars_context.py | 13 ++++++++++--- .../sdk/context/threadlocal_context.py | 3 +++ 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 3a5742b4b9..276db57a8b 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -22,7 +22,6 @@ logger = logging.getLogger(__name__) _CONTEXT_RUNTIME = None # type: typing.Optional[RuntimeContext] -_CONTEXT = None # type: typing.Optional[Context] class Context: @@ -111,10 +110,7 @@ def get_current() -> Context: except Exception: # pylint: disable=broad-except logger.error("Failed to load context: %s", configured_context) - global _CONTEXT # pylint: disable=global-statement - if _CONTEXT is None: - set_current(Context()) - return _CONTEXT # type: ignore + return Context(_CONTEXT_RUNTIME.snapshot()) def set_current(context: Context) -> None: @@ -124,8 +120,9 @@ def set_current(context: Context) -> None: Args: context: The context to use as current. """ - global _CONTEXT # pylint: disable=global-statement - _CONTEXT = context + if _CONTEXT_RUNTIME is None: + get_current() + _CONTEXT_RUNTIME.apply(context.snapshot()) def with_current_context( diff --git a/opentelemetry-api/src/opentelemetry/context/default_context.py b/opentelemetry-api/src/opentelemetry/context/default_context.py index f7bcd11fcd..01e42a361d 100644 --- a/opentelemetry-api/src/opentelemetry/context/default_context.py +++ b/opentelemetry-api/src/opentelemetry/context/default_context.py @@ -50,6 +50,9 @@ def snapshot(self) -> typing.Dict[str, "object"]: def apply(self, snapshot: typing.Dict[str, "object"]) -> None: """See `opentelemetry.context.RuntimeContext.apply`.""" + diff = set(self._values) - set(snapshot) + for key in diff: + self._values.pop(key, None) for name in snapshot: self.set_value(name, snapshot[name]) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py index 80130e3044..8615b5636d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py @@ -60,12 +60,19 @@ def copy(self) -> RuntimeContext: def snapshot(self) -> typing.Dict: """See `opentelemetry.context.RuntimeContext.snapshot`.""" - return dict( - (key, value.get()) for key, value in self._contextvars.items() - ) + values = {} + for key, value in self._contextvars.items(): + try: + values[key] = value.get() + except (KeyError, LookupError): + pass + return values def apply(self, snapshot: typing.Dict) -> None: """See `opentelemetry.context.RuntimeContext.apply`.""" + diff = set(self._contextvars) - set(snapshot) + for key in diff: + self._contextvars.pop(key, None) for name in snapshot: self.set_value(name, snapshot[name]) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py index 7ecee74a78..142aa55db7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py @@ -65,6 +65,9 @@ def snapshot(self) -> typing.Dict[str, "object"]: def apply(self, snapshot: typing.Dict[str, "object"]) -> None: """See `opentelemetry.context.RuntimeContext.apply`.""" + diff = set(self._thread_local.__dict__) - set(snapshot) + for key in diff: + self._thread_local.__dict__.pop(key, None) for name in snapshot: self.set_value(name, snapshot[name]) From 6f9780b327272152935a210d44ce962ccfd63af0 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Mon, 10 Feb 2020 14:34:02 -0800 Subject: [PATCH 37/56] add set_current/get_current on RuntimeContext to store current Context in correct scope Signed-off-by: Alex Boten --- .../src/opentelemetry/context/__init__.py | 24 ++---------- .../src/opentelemetry/context/context.py | 33 ++++++++++++---- .../opentelemetry/context/default_context.py | 27 ++++++------- .../sdk/context/contextvars_context.py | 29 +++++++------- .../sdk/context/threadlocal_context.py | 38 +++++++++---------- 5 files changed, 74 insertions(+), 77 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 276db57a8b..aa0d533494 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -18,28 +18,12 @@ from pkg_resources import iter_entry_points -from opentelemetry.context.context import RuntimeContext +from opentelemetry.context.context import Context, RuntimeContext logger = logging.getLogger(__name__) _CONTEXT_RUNTIME = None # type: typing.Optional[RuntimeContext] -class Context: - def __init__( - self, values: typing.Optional[typing.Dict[str, object]] = None - ): - if values: - self._data = values - else: - self._data = _CONTEXT_RUNTIME.snapshot() # type: ignore - - def get_value(self, key: str) -> "object": - return self._data.get(key) - - def snapshot(self) -> typing.Dict[str, object]: - return dict((key, value) for key, value in self._data.items()) - - def get_value(key: str, context: typing.Optional[Context] = None) -> "object": """To access the local state of an concern, the RuntimeContext API provides a function which takes a context and a key as input, @@ -110,7 +94,7 @@ def get_current() -> Context: except Exception: # pylint: disable=broad-except logger.error("Failed to load context: %s", configured_context) - return Context(_CONTEXT_RUNTIME.snapshot()) + return _CONTEXT_RUNTIME.get_current() def set_current(context: Context) -> None: @@ -120,9 +104,7 @@ def set_current(context: Context) -> None: Args: context: The context to use as current. """ - if _CONTEXT_RUNTIME is None: - get_current() - _CONTEXT_RUNTIME.apply(context.snapshot()) + _CONTEXT_RUNTIME.set_current(Context(context.snapshot())) def with_current_context( diff --git a/opentelemetry-api/src/opentelemetry/context/context.py b/opentelemetry-api/src/opentelemetry/context/context.py index b5412c6c46..f44feeb028 100644 --- a/opentelemetry-api/src/opentelemetry/context/context.py +++ b/opentelemetry-api/src/opentelemetry/context/context.py @@ -16,6 +16,23 @@ from abc import ABC, abstractmethod +class Context: + def __init__( + self, values: typing.Optional[typing.Dict[str, object]] = None + ): + if values is None: + values = {} + self._data = values + + def get_value(self, key: str) -> "object": + return self._data.get(key) + + def snapshot(self) -> typing.Dict[str, object]: + if self._data: + return dict((key, value) for key, value in self._data.items()) + return {} + + class RuntimeContext(ABC): """The RuntimeContext interface provides a wrapper for the different mechanisms that are used to propagate context in Python. @@ -48,21 +65,21 @@ def remove_value(self, key: str) -> None: key: The key for the value to remove. """ - @abstractmethod - def copy(self) -> "RuntimeContext": - """Return a copy of this context.""" - @abstractmethod def snapshot(self) -> typing.Dict[str, "object"]: """Returns the contents of a context.""" @abstractmethod - def apply(self, snapshot: typing.Dict[str, "object"]) -> None: - """Sets the contents of a context. - + def set_current(self, context: Context) -> None: + """ Sets the current Context object. + Args: - snapshot: The contents to set. + context: The Context to set. """ + @abstractmethod + def get_current(self) -> Context: + """ Returns the current Context object. """ + __all__ = ["RuntimeContext"] diff --git a/opentelemetry-api/src/opentelemetry/context/default_context.py b/opentelemetry-api/src/opentelemetry/context/default_context.py index 01e42a361d..d155be819c 100644 --- a/opentelemetry-api/src/opentelemetry/context/default_context.py +++ b/opentelemetry-api/src/opentelemetry/context/default_context.py @@ -12,9 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. import typing -from copy import copy -from opentelemetry.context.context import RuntimeContext +from opentelemetry.context.context import Context, RuntimeContext class DefaultRuntimeContext(RuntimeContext): @@ -24,6 +23,7 @@ class DefaultRuntimeContext(RuntimeContext): def __init__(self) -> None: self._values = {} # type: typing.Dict[str, object] + self._current_context = None def set_value(self, key: str, value: "object") -> None: """See `opentelemetry.context.RuntimeContext.set_value`.""" @@ -37,24 +37,19 @@ def remove_value(self, key: str) -> None: """See `opentelemetry.context.RuntimeContext.remove_value`.""" self._values.pop(key, None) - def copy(self) -> "RuntimeContext": - """See `opentelemetry.context.RuntimeContext.copy`.""" - context_copy = DefaultRuntimeContext() - for key, value in self._values.items(): - context_copy.set_value(key, copy(value)) - return context_copy - def snapshot(self) -> typing.Dict[str, "object"]: """See `opentelemetry.context.RuntimeContext.snapshot`.""" return dict((key, value) for key, value in self._values.items()) - def apply(self, snapshot: typing.Dict[str, "object"]) -> None: - """See `opentelemetry.context.RuntimeContext.apply`.""" - diff = set(self._values) - set(snapshot) - for key in diff: - self._values.pop(key, None) - for name in snapshot: - self.set_value(name, snapshot[name]) + def set_current(self, context: Context) -> None: + """See `opentelemetry.context.RuntimeContext.set_current`.""" + self._current_context = context + + def get_current(self) -> Context: + """See `opentelemetry.context.RuntimeContext.get_current`.""" + if self._current_context is None: + self._current_context = Context(self.snapshot()) + return self._current_context __all__ = ["DefaultRuntimeContext"] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py index 8615b5636d..e74d0c802c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py @@ -15,6 +15,7 @@ from contextvars import ContextVar from sys import version_info +from opentelemetry.context import Context from opentelemetry.context.context import RuntimeContext if (3, 5, 3) <= version_info < (3, 7): @@ -24,6 +25,9 @@ import opentelemetry.sdk.context.aiocontextvarsfix # pylint:disable=unused-import +_CONTEXT_KEY = "current_context" + + class ContextVarsRuntimeContext(RuntimeContext): """An implementation of the RuntimeContext interface which wraps ContextVar under the hood. This is the prefered implementation for usage with Python 3.5+ @@ -31,6 +35,7 @@ class ContextVarsRuntimeContext(RuntimeContext): def __init__(self) -> None: self._contextvars = {} # type: typing.Dict[str, ContextVar[object]] + self._current_context = ContextVar(_CONTEXT_KEY) def set_value(self, key: str, value: "object") -> None: """See `opentelemetry.context.RuntimeContext.set_value`.""" @@ -52,12 +57,6 @@ def remove_value(self, key: str) -> None: """See `opentelemetry.context.RuntimeContext.remove_value`.""" self._contextvars.pop(key, None) - def copy(self) -> RuntimeContext: - """See `opentelemetry.context.RuntimeContext.copy`.""" - # under the hood, ContextVars returns a copy on set - # we dont need to do any copying ourselves - return self - def snapshot(self) -> typing.Dict: """See `opentelemetry.context.RuntimeContext.snapshot`.""" values = {} @@ -68,13 +67,17 @@ def snapshot(self) -> typing.Dict: pass return values - def apply(self, snapshot: typing.Dict) -> None: - """See `opentelemetry.context.RuntimeContext.apply`.""" - diff = set(self._contextvars) - set(snapshot) - for key in diff: - self._contextvars.pop(key, None) - for name in snapshot: - self.set_value(name, snapshot[name]) + def set_current(self, context: Context) -> None: + """See `opentelemetry.context.RuntimeContext.set_current`.""" + self._current_context.set(context) + + def get_current(self) -> Context: + """See `opentelemetry.context.RuntimeContext.get_current`.""" + try: + return self._current_context.get() + except LookupError: + self.set_current(Context(self.snapshot())) + return self._current_context.get() __all__ = ["ContextVarsRuntimeContext"] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py index 142aa55db7..29fd42a938 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py @@ -14,9 +14,11 @@ import threading import typing -from copy import copy -from opentelemetry.context import RuntimeContext +from opentelemetry.context import Context, RuntimeContext + + +_CONTEXT_KEY = "current_context" class ThreadLocalRuntimeContext(RuntimeContext): @@ -27,6 +29,7 @@ class ThreadLocalRuntimeContext(RuntimeContext): def __init__(self) -> None: self._thread_local = threading.local() + self._current_context = threading.local() def set_value(self, key: str, value: "object") -> None: """See `opentelemetry.context.RuntimeContext.set_value`.""" @@ -47,29 +50,26 @@ def remove_value(self, key: str) -> None: except AttributeError: pass - def copy(self) -> RuntimeContext: - """See `opentelemetry.context.RuntimeContext.copy`.""" - - context_copy = ThreadLocalRuntimeContext() - - for key, value in self._thread_local.__dict__.items(): - context_copy.set_value(key, copy(value)) - - return context_copy - def snapshot(self) -> typing.Dict[str, "object"]: """See `opentelemetry.context.RuntimeContext.snapshot`.""" return dict( (key, value) for key, value in self._thread_local.__dict__.items() ) - def apply(self, snapshot: typing.Dict[str, "object"]) -> None: - """See `opentelemetry.context.RuntimeContext.apply`.""" - diff = set(self._thread_local.__dict__) - set(snapshot) - for key in diff: - self._thread_local.__dict__.pop(key, None) - for name in snapshot: - self.set_value(name, snapshot[name]) + def set_current(self, context: Context) -> None: + """See `opentelemetry.context.RuntimeContext.set_current`.""" + setattr(self._current_context, _CONTEXT_KEY, context) + + def get_current(self) -> Context: + """See `opentelemetry.context.RuntimeContext.get_current`.""" + try: + got = getattr(self._current_context, _CONTEXT_KEY) # type: object + except AttributeError: + setattr( + self._current_context, _CONTEXT_KEY, Context(self.snapshot()), + ) + got = getattr(self._current_context, _CONTEXT_KEY) + return got __all__ = ["ThreadLocalRuntimeContext"] From 49b6abd4aae4262f56c90979042f03227f5b73d1 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Mon, 10 Feb 2020 16:14:28 -0800 Subject: [PATCH 38/56] cleaning up mypy and fixing 3.4 tests Signed-off-by: Alex Boten --- .../src/opentelemetry/context/__init__.py | 4 +- .../src/opentelemetry/context/context.py | 2 +- .../opentelemetry/context/default_context.py | 2 +- .../distributedcontext/__init__.py | 3 +- .../sdk/context/threadlocal_context.py | 1 - opentelemetry-sdk/tests/conftest.py | 2 +- .../tests/context/test_asyncio.py | 50 ++++++++++++++++++- .../tests/context/test_context.py | 40 --------------- 8 files changed, 56 insertions(+), 48 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index aa0d533494..24f7722113 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -94,7 +94,7 @@ def get_current() -> Context: except Exception: # pylint: disable=broad-except logger.error("Failed to load context: %s", configured_context) - return _CONTEXT_RUNTIME.get_current() + return _CONTEXT_RUNTIME.get_current() # type: ignore def set_current(context: Context) -> None: @@ -104,7 +104,7 @@ def set_current(context: Context) -> None: Args: context: The context to use as current. """ - _CONTEXT_RUNTIME.set_current(Context(context.snapshot())) + _CONTEXT_RUNTIME.set_current(Context(context.snapshot())) # type: ignore def with_current_context( diff --git a/opentelemetry-api/src/opentelemetry/context/context.py b/opentelemetry-api/src/opentelemetry/context/context.py index f44feeb028..038d3ba9d5 100644 --- a/opentelemetry-api/src/opentelemetry/context/context.py +++ b/opentelemetry-api/src/opentelemetry/context/context.py @@ -72,7 +72,7 @@ def snapshot(self) -> typing.Dict[str, "object"]: @abstractmethod def set_current(self, context: Context) -> None: """ Sets the current Context object. - + Args: context: The Context to set. """ diff --git a/opentelemetry-api/src/opentelemetry/context/default_context.py b/opentelemetry-api/src/opentelemetry/context/default_context.py index d155be819c..0d450cdb3e 100644 --- a/opentelemetry-api/src/opentelemetry/context/default_context.py +++ b/opentelemetry-api/src/opentelemetry/context/default_context.py @@ -23,7 +23,7 @@ class DefaultRuntimeContext(RuntimeContext): def __init__(self) -> None: self._values = {} # type: typing.Dict[str, object] - self._current_context = None + self._current_context = None # type: typing.Optional[Context] def set_value(self, key: str, value: "object") -> None: """See `opentelemetry.context.RuntimeContext.set_value`.""" diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py index 8f1020e462..a89d982550 100644 --- a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py +++ b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py @@ -17,7 +17,8 @@ import typing from contextlib import contextmanager -from opentelemetry.context import Context, get_value, set_current, set_value +from opentelemetry.context import get_value, set_current, set_value +from opentelemetry.context.context import Context PRINTABLE = frozenset( itertools.chain( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py index 29fd42a938..3d10b53ff2 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py @@ -17,7 +17,6 @@ from opentelemetry.context import Context, RuntimeContext - _CONTEXT_KEY = "current_context" diff --git a/opentelemetry-sdk/tests/conftest.py b/opentelemetry-sdk/tests/conftest.py index 40428bc43f..15f185b408 100644 --- a/opentelemetry-sdk/tests/conftest.py +++ b/opentelemetry-sdk/tests/conftest.py @@ -18,7 +18,7 @@ def pytest_sessionstart(session): # pylint: disable=unused-argument - if (3, 4) >= version_info: + if version_info < (3, 5): # contextvars are not supported in 3.4, use thread-local storage environ["OPENTELEMETRY_CONTEXT"] = "threadlocal_context" else: diff --git a/opentelemetry-sdk/tests/context/test_asyncio.py b/opentelemetry-sdk/tests/context/test_asyncio.py index 9776327253..21445791b3 100644 --- a/opentelemetry-sdk/tests/context/test_asyncio.py +++ b/opentelemetry-sdk/tests/context/test_asyncio.py @@ -26,6 +26,12 @@ InMemorySpanExporter, ) +try: + import contextvars # pylint: disable=unused-import +except ImportError: + raise unittest.SkipTest("contextvars not available") + + _SPAN_NAMES = [ "test_span1", "test_span2", @@ -47,8 +53,13 @@ def stop_loop_when(loop, cond_func, timeout=5.0): loop.call_later(0.1, stop_loop_when, loop, cond_func, timeout) +def do_work() -> None: + context_api.set_current(context_api.set_value("say-something", "bar")) + + class TestAsyncio(unittest.TestCase): - async def task(self, name): + @asyncio.coroutine + def task(self, name): with self.tracer.start_as_current_span(name): context_api.set_value("say-something", "bar") @@ -103,3 +114,40 @@ def test_with_asyncio(self): if span is expected_parent: continue self.assertEqual(span.parent, expected_parent) + + +class TestContextVarsContext(unittest.TestCase): + def setUp(self): + self.previous_context = context_api.get_current() + + def tearDown(self): + context_api.set_current(self.previous_context) + + @patch( + "opentelemetry.context._CONTEXT_RUNTIME", ContextVarsRuntimeContext() + ) + def test_context(self): + self.assertIsNone(context_api.get_value("say-something")) + empty_context = context_api.get_current() + second_context = context_api.set_value("say-something", "foo") + self.assertEqual(second_context.get_value("say-something"), "foo") + + do_work() + self.assertEqual(context_api.get_value("say-something"), "bar") + third_context = context_api.get_current() + + self.assertIsNone(empty_context.get_value("say-something")) + self.assertEqual(second_context.get_value("say-something"), "foo") + self.assertEqual(third_context.get_value("say-something"), "bar") + + @patch( + "opentelemetry.context._CONTEXT_RUNTIME", ContextVarsRuntimeContext() + ) + def test_set_value(self): + first = context_api.set_value("a", "yyy") + second = context_api.set_value("a", "zzz") + third = context_api.set_value("a", "---", first) + self.assertEqual("yyy", context_api.get_value("a", context=first)) + self.assertEqual("zzz", context_api.get_value("a", context=second)) + self.assertEqual("---", context_api.get_value("a", context=third)) + self.assertEqual(None, context_api.get_value("a")) diff --git a/opentelemetry-sdk/tests/context/test_context.py b/opentelemetry-sdk/tests/context/test_context.py index 4447a1c61c..fa4f5dccc7 100644 --- a/opentelemetry-sdk/tests/context/test_context.py +++ b/opentelemetry-sdk/tests/context/test_context.py @@ -16,9 +16,6 @@ from unittest.mock import patch from opentelemetry import context -from opentelemetry.sdk.context.contextvars_context import ( - ContextVarsRuntimeContext, -) from opentelemetry.sdk.context.threadlocal_context import ( ThreadLocalRuntimeContext, ) @@ -63,40 +60,3 @@ def test_set_value(self): self.assertEqual("zzz", context.get_value("a", context=second)) self.assertEqual("---", context.get_value("a", context=third)) self.assertEqual(None, context.get_value("a")) - - -class TestContextVarsContext(unittest.TestCase): - def setUp(self): - self.previous_context = context.get_current() - - def tearDown(self): - context.set_current(self.previous_context) - - @patch( - "opentelemetry.context._CONTEXT_RUNTIME", ContextVarsRuntimeContext() - ) - def test_context(self): - self.assertIsNone(context.get_value("say-something")) - empty_context = context.get_current() - second_context = context.set_value("say-something", "foo") - self.assertEqual(second_context.get_value("say-something"), "foo") - - do_work() - self.assertEqual(context.get_value("say-something"), "bar") - third_context = context.get_current() - - self.assertIsNone(empty_context.get_value("say-something")) - self.assertEqual(second_context.get_value("say-something"), "foo") - self.assertEqual(third_context.get_value("say-something"), "bar") - - @patch( - "opentelemetry.context._CONTEXT_RUNTIME", ContextVarsRuntimeContext() - ) - def test_set_value(self): - first = context.set_value("a", "yyy") - second = context.set_value("a", "zzz") - third = context.set_value("a", "---", first) - self.assertEqual("yyy", context.get_value("a", context=first)) - self.assertEqual("zzz", context.get_value("a", context=second)) - self.assertEqual("---", context.get_value("a", context=third)) - self.assertEqual(None, context.get_value("a")) From 4c5083ac42d620f5f350ad159381be24f1244810 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Mon, 10 Feb 2020 16:50:34 -0800 Subject: [PATCH 39/56] cleaning up tests to use context api interface Signed-off-by: Alex Boten --- .../src/opentelemetry/context/__init__.py | 8 +-- .../src/opentelemetry/context/context.py | 18 ++----- .../tests/context/test_context.py | 25 ++++++---- .../tests/context/test_asyncio.py | 49 ++++++++++--------- .../tests/context/test_context.py | 21 ++++---- 5 files changed, 58 insertions(+), 63 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 24f7722113..71d418a80a 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -33,7 +33,7 @@ def get_value(key: str, context: typing.Optional[Context] = None) -> "object": key: The key of the value to retrieve. context: The context from which to retrieve the value, if None, the current context is used. """ - return context.get_value(key) if context else get_current().get_value(key) + return context.get(key) if context is not None else get_current().get(key) def set_value( @@ -51,7 +51,7 @@ def set_value( """ if context is None: context = get_current() - new_values = context.snapshot() + new_values = context.copy() new_values[key] = value return Context(new_values) @@ -69,7 +69,7 @@ def remove_value( """ if context is None: context = get_current() - new_values = context.snapshot() + new_values = context.copy() new_values.pop(key, None) return Context(new_values) @@ -104,7 +104,7 @@ def set_current(context: Context) -> None: Args: context: The context to use as current. """ - _CONTEXT_RUNTIME.set_current(Context(context.snapshot())) # type: ignore + _CONTEXT_RUNTIME.set_current(Context(context.copy())) # type: ignore def with_current_context( diff --git a/opentelemetry-api/src/opentelemetry/context/context.py b/opentelemetry-api/src/opentelemetry/context/context.py index 038d3ba9d5..ecd7f0b9fe 100644 --- a/opentelemetry-api/src/opentelemetry/context/context.py +++ b/opentelemetry-api/src/opentelemetry/context/context.py @@ -16,21 +16,9 @@ from abc import ABC, abstractmethod -class Context: - def __init__( - self, values: typing.Optional[typing.Dict[str, object]] = None - ): - if values is None: - values = {} - self._data = values - - def get_value(self, key: str) -> "object": - return self._data.get(key) - - def snapshot(self) -> typing.Dict[str, object]: - if self._data: - return dict((key, value) for key, value in self._data.items()) - return {} +class Context(dict): + def __setitem__(self, key, value): + raise ValueError class RuntimeContext(ABC): diff --git a/opentelemetry-api/tests/context/test_context.py b/opentelemetry-api/tests/context/test_context.py index e1b8238eaf..43ea12c0ad 100644 --- a/opentelemetry-api/tests/context/test_context.py +++ b/opentelemetry-api/tests/context/test_context.py @@ -18,23 +18,23 @@ def do_work() -> None: - context.set_current(context.set_value("say-something", "bar")) + context.set_current(context.set_value("say", "bar")) class TestContext(unittest.TestCase): def test_context(self): - self.assertIsNone(context.get_value("say-something")) - empty_context = context.get_current() - second_context = context.set_value("say-something", "foo") - self.assertEqual(second_context.get_value("say-something"), "foo") + self.assertIsNone(context.get_value("say")) + empty = context.get_current() + second = context.set_value("say", "foo") + self.assertEqual(context.get_value("say", context=second), "foo") do_work() - self.assertEqual(context.get_value("say-something"), "bar") - third_context = context.get_current() + self.assertEqual(context.get_value("say"), "bar") + third = context.get_current() - self.assertIsNone(empty_context.get_value("say-something")) - self.assertEqual(second_context.get_value("say-something"), "foo") - self.assertEqual(third_context.get_value("say-something"), "bar") + self.assertIsNone(context.get_value("say", context=empty)) + self.assertEqual(context.get_value("say", context=second), "foo") + self.assertEqual(context.get_value("say", context=third), "bar") def test_set_value(self): first = context.set_value("a", "yyy") @@ -44,3 +44,8 @@ def test_set_value(self): self.assertEqual("zzz", context.get_value("a", context=second)) self.assertEqual("---", context.get_value("a", context=third)) self.assertEqual(None, context.get_value("a")) + + def test_context_is_immutable(self): + with self.assertRaises(ValueError): + # ensure a context + context.get_current()["test"] = "cant-change-immutable" diff --git a/opentelemetry-sdk/tests/context/test_asyncio.py b/opentelemetry-sdk/tests/context/test_asyncio.py index 21445791b3..d3db88fe66 100644 --- a/opentelemetry-sdk/tests/context/test_asyncio.py +++ b/opentelemetry-sdk/tests/context/test_asyncio.py @@ -16,7 +16,7 @@ import unittest from unittest.mock import patch -from opentelemetry import context as context_api +from opentelemetry import context from opentelemetry.sdk import trace from opentelemetry.sdk.context.contextvars_context import ( ContextVarsRuntimeContext, @@ -54,21 +54,21 @@ def stop_loop_when(loop, cond_func, timeout=5.0): def do_work() -> None: - context_api.set_current(context_api.set_value("say-something", "bar")) + context.set_current(context.set_value("say", "bar")) class TestAsyncio(unittest.TestCase): @asyncio.coroutine def task(self, name): with self.tracer.start_as_current_span(name): - context_api.set_value("say-something", "bar") + context.set_value("say", "bar") def submit_another_task(self, name): self.loop.create_task(self.task(name)) def setUp(self): - self.previous_context = context_api.get_current() - context_api.set_current(context_api.Context()) + self.previous_context = context.get_current() + context.set_current(context.Context()) self.tracer_source = trace.TracerSource() self.tracer = self.tracer_source.get_tracer(__name__) self.memory_exporter = InMemorySpanExporter() @@ -77,7 +77,7 @@ def setUp(self): self.loop = asyncio.get_event_loop() def tearDown(self): - context_api.set_current(self.previous_context) + context.set_current(self.previous_context) @patch( "opentelemetry.context._CONTEXT_RUNTIME", ContextVarsRuntimeContext() @@ -118,36 +118,37 @@ def test_with_asyncio(self): class TestContextVarsContext(unittest.TestCase): def setUp(self): - self.previous_context = context_api.get_current() + self.previous_context = context.get_current() def tearDown(self): - context_api.set_current(self.previous_context) + context.set_current(self.previous_context) @patch( "opentelemetry.context._CONTEXT_RUNTIME", ContextVarsRuntimeContext() ) def test_context(self): - self.assertIsNone(context_api.get_value("say-something")) - empty_context = context_api.get_current() - second_context = context_api.set_value("say-something", "foo") - self.assertEqual(second_context.get_value("say-something"), "foo") + self.assertIsNone(context.get_value("say")) + empty = context.get_current() + second = context.set_value("say", "foo") + + self.assertEqual(context.get_value("say", context=second), "foo") do_work() - self.assertEqual(context_api.get_value("say-something"), "bar") - third_context = context_api.get_current() + self.assertEqual(context.get_value("say"), "bar") + third = context.get_current() - self.assertIsNone(empty_context.get_value("say-something")) - self.assertEqual(second_context.get_value("say-something"), "foo") - self.assertEqual(third_context.get_value("say-something"), "bar") + self.assertIsNone(context.get_value("say", context=empty)) + self.assertEqual(context.get_value("say", context=second), "foo") + self.assertEqual(context.get_value("say", context=third), "bar") @patch( "opentelemetry.context._CONTEXT_RUNTIME", ContextVarsRuntimeContext() ) def test_set_value(self): - first = context_api.set_value("a", "yyy") - second = context_api.set_value("a", "zzz") - third = context_api.set_value("a", "---", first) - self.assertEqual("yyy", context_api.get_value("a", context=first)) - self.assertEqual("zzz", context_api.get_value("a", context=second)) - self.assertEqual("---", context_api.get_value("a", context=third)) - self.assertEqual(None, context_api.get_value("a")) + first = context.set_value("a", "yyy") + second = context.set_value("a", "zzz") + third = context.set_value("a", "---", first) + self.assertEqual("yyy", context.get_value("a", context=first)) + self.assertEqual("zzz", context.get_value("a", context=second)) + self.assertEqual("---", context.get_value("a", context=third)) + self.assertEqual(None, context.get_value("a")) diff --git a/opentelemetry-sdk/tests/context/test_context.py b/opentelemetry-sdk/tests/context/test_context.py index fa4f5dccc7..13ee5ae231 100644 --- a/opentelemetry-sdk/tests/context/test_context.py +++ b/opentelemetry-sdk/tests/context/test_context.py @@ -22,7 +22,7 @@ def do_work() -> None: - context.set_current(context.set_value("say-something", "bar")) + context.set_current(context.set_value("say", "bar")) class TestThreadLocalContext(unittest.TestCase): @@ -36,18 +36,19 @@ def tearDown(self): "opentelemetry.context._CONTEXT_RUNTIME", ThreadLocalRuntimeContext() ) def test_context(self): - self.assertIsNone(context.get_value("say-something")) - empty_context = context.get_current() - second_context = context.set_value("say-something", "foo") - self.assertEqual(second_context.get_value("say-something"), "foo") + self.assertIsNone(context.get_value("say")) + empty = context.get_current() + second = context.set_value("say", "foo") + + self.assertEqual(context.get_value("say", context=second), "foo") do_work() - self.assertEqual(context.get_value("say-something"), "bar") - third_context = context.get_current() + self.assertEqual(context.get_value("say"), "bar") + third = context.get_current() - self.assertIsNone(empty_context.get_value("say-something")) - self.assertEqual(second_context.get_value("say-something"), "foo") - self.assertEqual(third_context.get_value("say-something"), "bar") + self.assertIsNone(context.get_value("say", context=empty)) + self.assertEqual(context.get_value("say", context=second), "foo") + self.assertEqual(context.get_value("say", context=third), "bar") @patch( "opentelemetry.context._CONTEXT_RUNTIME", ThreadLocalRuntimeContext() From 5a8a9fb75d0936cf6a1910a93ce4f4e849598e93 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Mon, 10 Feb 2020 17:22:30 -0800 Subject: [PATCH 40/56] fix mypy Signed-off-by: Alex Boten --- opentelemetry-api/src/opentelemetry/context/context.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/context.py b/opentelemetry-api/src/opentelemetry/context/context.py index ecd7f0b9fe..922f397c20 100644 --- a/opentelemetry-api/src/opentelemetry/context/context.py +++ b/opentelemetry-api/src/opentelemetry/context/context.py @@ -16,8 +16,8 @@ from abc import ABC, abstractmethod -class Context(dict): - def __setitem__(self, key, value): +class Context(typing.Dict[str, object]): + def __setitem__(self, key: str, value: object) -> None: raise ValueError From d5da10c75474bfc4af65862aebad1e2e678723d9 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Mon, 10 Feb 2020 17:27:14 -0800 Subject: [PATCH 41/56] fix 3.4 tests Signed-off-by: Alex Boten --- opentelemetry-sdk/tests/context/test_asyncio.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opentelemetry-sdk/tests/context/test_asyncio.py b/opentelemetry-sdk/tests/context/test_asyncio.py index d3db88fe66..f5ec32b174 100644 --- a/opentelemetry-sdk/tests/context/test_asyncio.py +++ b/opentelemetry-sdk/tests/context/test_asyncio.py @@ -18,9 +18,6 @@ from opentelemetry import context from opentelemetry.sdk import trace -from opentelemetry.sdk.context.contextvars_context import ( - ContextVarsRuntimeContext, -) from opentelemetry.sdk.trace import export from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, @@ -28,6 +25,9 @@ try: import contextvars # pylint: disable=unused-import + from opentelemetry.sdk.context.contextvars_context import ( + ContextVarsRuntimeContext, + ) except ImportError: raise unittest.SkipTest("contextvars not available") From 7995ac3f680af7baf69f3b954b6c82ca609bb607 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Mon, 10 Feb 2020 17:33:01 -0800 Subject: [PATCH 42/56] fixing docs Signed-off-by: Alex Boten --- opentelemetry-api/src/opentelemetry/context/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/context/context.py b/opentelemetry-api/src/opentelemetry/context/context.py index 922f397c20..027f8b75b9 100644 --- a/opentelemetry-api/src/opentelemetry/context/context.py +++ b/opentelemetry-api/src/opentelemetry/context/context.py @@ -70,4 +70,4 @@ def get_current(self) -> Context: """ Returns the current Context object. """ -__all__ = ["RuntimeContext"] +__all__ = ["Context", "RuntimeContext"] From 37f155596e79256f4887b0f705d52ac491e8d6b3 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 11 Feb 2020 09:55:12 -0800 Subject: [PATCH 43/56] return the old context when calling set_current to allow for restore Signed-off-by: Alex Boten --- .../src/opentelemetry/context/__init__.py | 45 +++++++++++-------- .../tests/context/test_context.py | 11 +++++ .../tests/context/test_asyncio.py | 6 +-- .../tests/context/test_context.py | 4 +- .../tests/context/test_threads.py | 2 +- 5 files changed, 44 insertions(+), 24 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 71d418a80a..91018fab17 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -21,7 +21,7 @@ from opentelemetry.context.context import Context, RuntimeContext logger = logging.getLogger(__name__) -_CONTEXT_RUNTIME = None # type: typing.Optional[RuntimeContext] +_RUNTIME_CONTEXT = None # type: typing.Optional[RuntimeContext] def get_value(key: str, context: typing.Optional[Context] = None) -> "object": @@ -74,37 +74,46 @@ def remove_value( return Context(new_values) +def _init_runtime(): + # FIXME use a better implementation of a configuration manager to avoid having + # to get configuration values straight from environment variables + + global _RUNTIME_CONTEXT # pylint: disable=global-statement + configured_context = environ.get( + "OPENTELEMETRY_CONTEXT", "default_context" + ) # type: str + try: + _RUNTIME_CONTEXT = next( + iter_entry_points("opentelemetry_context", configured_context) + ).load()() + except Exception: # pylint: disable=broad-except + logger.error("Failed to load context: %s", configured_context) + + def get_current() -> Context: """To access the context associated with program execution, the RuntimeContext API provides a function which takes no arguments and returns a RuntimeContext. """ - global _CONTEXT_RUNTIME # pylint: disable=global-statement - if _CONTEXT_RUNTIME is None: - # FIXME use a better implementation of a configuration manager to avoid having - # to get configuration values straight from environment variables - - configured_context = environ.get( - "OPENTELEMETRY_CONTEXT", "default_context" - ) # type: str - try: - _CONTEXT_RUNTIME = next( - iter_entry_points("opentelemetry_context", configured_context) - ).load()() - except Exception: # pylint: disable=broad-except - logger.error("Failed to load context: %s", configured_context) - return _CONTEXT_RUNTIME.get_current() # type: ignore + if _RUNTIME_CONTEXT is None: + _init_runtime() + + return _RUNTIME_CONTEXT.get_current() # type: ignore -def set_current(context: Context) -> None: +def set_current(context: Context) -> Context: """To associate a context with program execution, the Context API provides a function which takes a Context. Args: context: The context to use as current. """ - _CONTEXT_RUNTIME.set_current(Context(context.copy())) # type: ignore + if _RUNTIME_CONTEXT is None: + _init_runtime() + old_context = _RUNTIME_CONTEXT.get_current() + _RUNTIME_CONTEXT.set_current(Context(context.copy())) # type: ignore + return old_context def with_current_context( diff --git a/opentelemetry-api/tests/context/test_context.py b/opentelemetry-api/tests/context/test_context.py index 43ea12c0ad..722393cbef 100644 --- a/opentelemetry-api/tests/context/test_context.py +++ b/opentelemetry-api/tests/context/test_context.py @@ -15,6 +15,7 @@ import unittest from opentelemetry import context +from opentelemetry.context.context import Context def do_work() -> None: @@ -22,6 +23,9 @@ def do_work() -> None: class TestContext(unittest.TestCase): + def setUp(self): + context.set_current(Context()) + def test_context(self): self.assertIsNone(context.get_value("say")) empty = context.get_current() @@ -49,3 +53,10 @@ def test_context_is_immutable(self): with self.assertRaises(ValueError): # ensure a context context.get_current()["test"] = "cant-change-immutable" + + def test_set_current(self): + context.set_current(context.set_value("a", "yyy")) + + old_context = context.set_current(context.set_value("a", "zzz")) + self.assertEqual("yyy", context.get_value("a", context=old_context)) + self.assertEqual("zzz", context.get_value("a")) diff --git a/opentelemetry-sdk/tests/context/test_asyncio.py b/opentelemetry-sdk/tests/context/test_asyncio.py index f5ec32b174..2ebf690e1f 100644 --- a/opentelemetry-sdk/tests/context/test_asyncio.py +++ b/opentelemetry-sdk/tests/context/test_asyncio.py @@ -80,7 +80,7 @@ def tearDown(self): context.set_current(self.previous_context) @patch( - "opentelemetry.context._CONTEXT_RUNTIME", ContextVarsRuntimeContext() + "opentelemetry.context._RUNTIME_CONTEXT", ContextVarsRuntimeContext() ) def test_with_asyncio(self): with self.tracer.start_as_current_span("asyncio_test"): @@ -124,7 +124,7 @@ def tearDown(self): context.set_current(self.previous_context) @patch( - "opentelemetry.context._CONTEXT_RUNTIME", ContextVarsRuntimeContext() + "opentelemetry.context._RUNTIME_CONTEXT", ContextVarsRuntimeContext() ) def test_context(self): self.assertIsNone(context.get_value("say")) @@ -142,7 +142,7 @@ def test_context(self): self.assertEqual(context.get_value("say", context=third), "bar") @patch( - "opentelemetry.context._CONTEXT_RUNTIME", ContextVarsRuntimeContext() + "opentelemetry.context._RUNTIME_CONTEXT", ContextVarsRuntimeContext() ) def test_set_value(self): first = context.set_value("a", "yyy") diff --git a/opentelemetry-sdk/tests/context/test_context.py b/opentelemetry-sdk/tests/context/test_context.py index 13ee5ae231..db40e49004 100644 --- a/opentelemetry-sdk/tests/context/test_context.py +++ b/opentelemetry-sdk/tests/context/test_context.py @@ -33,7 +33,7 @@ def tearDown(self): context.set_current(self.previous_context) @patch( - "opentelemetry.context._CONTEXT_RUNTIME", ThreadLocalRuntimeContext() + "opentelemetry.context._RUNTIME_CONTEXT", ThreadLocalRuntimeContext() ) def test_context(self): self.assertIsNone(context.get_value("say")) @@ -51,7 +51,7 @@ def test_context(self): self.assertEqual(context.get_value("say", context=third), "bar") @patch( - "opentelemetry.context._CONTEXT_RUNTIME", ThreadLocalRuntimeContext() + "opentelemetry.context._RUNTIME_CONTEXT", ThreadLocalRuntimeContext() ) def test_set_value(self): first = context.set_value("a", "yyy") diff --git a/opentelemetry-sdk/tests/context/test_threads.py b/opentelemetry-sdk/tests/context/test_threads.py index 60a435c3cb..21b89c85d8 100644 --- a/opentelemetry-sdk/tests/context/test_threads.py +++ b/opentelemetry-sdk/tests/context/test_threads.py @@ -53,7 +53,7 @@ def tearDown(self): context.set_current(self.previous_context) @patch( - "opentelemetry.context._CONTEXT_RUNTIME", ThreadLocalRuntimeContext() + "opentelemetry.context._RUNTIME_CONTEXT", ThreadLocalRuntimeContext() ) def test_with_threads(self): with self.tracer.start_as_current_span("threads_test"): From 81f06a9abc55a2fb5c11827b8f93e5e37f6099eb Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 11 Feb 2020 10:12:44 -0800 Subject: [PATCH 44/56] mypy fixes Signed-off-by: Alex Boten --- .../src/opentelemetry/context/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 91018fab17..212e21c7c0 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -16,6 +16,7 @@ import typing from os import environ + from pkg_resources import iter_entry_points from opentelemetry.context.context import Context, RuntimeContext @@ -74,7 +75,7 @@ def remove_value( return Context(new_values) -def _init_runtime(): +def _init_runtime() -> None: # FIXME use a better implementation of a configuration manager to avoid having # to get configuration values straight from environment variables @@ -99,7 +100,7 @@ def get_current() -> Context: if _RUNTIME_CONTEXT is None: _init_runtime() - return _RUNTIME_CONTEXT.get_current() # type: ignore + return _RUNTIME_CONTEXT.get_current() # type:ignore def set_current(context: Context) -> Context: @@ -111,9 +112,9 @@ def set_current(context: Context) -> Context: """ if _RUNTIME_CONTEXT is None: _init_runtime() - old_context = _RUNTIME_CONTEXT.get_current() - _RUNTIME_CONTEXT.set_current(Context(context.copy())) # type: ignore - return old_context + old_context = _RUNTIME_CONTEXT.get_current() # type:ignore + _RUNTIME_CONTEXT.set_current(Context(context.copy())) # type:ignore + return old_context # type:ignore def with_current_context( From f29111a4b52a304cb5e026dc2fc2d06c914a520f Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 11 Feb 2020 10:43:15 -0800 Subject: [PATCH 45/56] removing snapshot from runtime context Signed-off-by: Alex Boten --- .../src/opentelemetry/context/context.py | 14 +++++--------- .../opentelemetry/context/default_context.py | 7 ++----- .../sdk/context/contextvars_context.py | 18 +++++++----------- .../sdk/context/threadlocal_context.py | 12 +++++------- 4 files changed, 19 insertions(+), 32 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/context.py b/opentelemetry-api/src/opentelemetry/context/context.py index 027f8b75b9..e17af68dfe 100644 --- a/opentelemetry-api/src/opentelemetry/context/context.py +++ b/opentelemetry-api/src/opentelemetry/context/context.py @@ -30,7 +30,7 @@ class RuntimeContext(ABC): @abstractmethod def set_value(self, key: str, value: "object") -> None: - """Set a value in this context. + """Set a value in this `RuntimeContext`. Args: key: The key for the value to set. @@ -39,7 +39,7 @@ def set_value(self, key: str, value: "object") -> None: @abstractmethod def get_value(self, key: str) -> "object": - """Get a value from this context. + """Get a value from this `RuntimeContext`. Args: key: The key for the value to retrieve. @@ -47,19 +47,15 @@ def get_value(self, key: str) -> "object": @abstractmethod def remove_value(self, key: str) -> None: - """Remove a value from this context. + """Remove a value from this `RuntimeContext`. Args: key: The key for the value to remove. """ - @abstractmethod - def snapshot(self) -> typing.Dict[str, "object"]: - """Returns the contents of a context.""" - @abstractmethod def set_current(self, context: Context) -> None: - """ Sets the current Context object. + """ Sets the current `Context` object. Args: context: The Context to set. @@ -67,7 +63,7 @@ def set_current(self, context: Context) -> None: @abstractmethod def get_current(self) -> Context: - """ Returns the current Context object. """ + """ Returns the current `Context` object. """ __all__ = ["Context", "RuntimeContext"] diff --git a/opentelemetry-api/src/opentelemetry/context/default_context.py b/opentelemetry-api/src/opentelemetry/context/default_context.py index 0d450cdb3e..a936a4a0a7 100644 --- a/opentelemetry-api/src/opentelemetry/context/default_context.py +++ b/opentelemetry-api/src/opentelemetry/context/default_context.py @@ -37,10 +37,6 @@ def remove_value(self, key: str) -> None: """See `opentelemetry.context.RuntimeContext.remove_value`.""" self._values.pop(key, None) - def snapshot(self) -> typing.Dict[str, "object"]: - """See `opentelemetry.context.RuntimeContext.snapshot`.""" - return dict((key, value) for key, value in self._values.items()) - def set_current(self, context: Context) -> None: """See `opentelemetry.context.RuntimeContext.set_current`.""" self._current_context = context @@ -48,7 +44,8 @@ def set_current(self, context: Context) -> None: def get_current(self) -> Context: """See `opentelemetry.context.RuntimeContext.get_current`.""" if self._current_context is None: - self._current_context = Context(self.snapshot()) + values = dict((key, value) for key, value in self._values.items()) + self._current_context = Context(values) return self._current_context diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py index e74d0c802c..fa1e8038bc 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py @@ -57,16 +57,6 @@ def remove_value(self, key: str) -> None: """See `opentelemetry.context.RuntimeContext.remove_value`.""" self._contextvars.pop(key, None) - def snapshot(self) -> typing.Dict: - """See `opentelemetry.context.RuntimeContext.snapshot`.""" - values = {} - for key, value in self._contextvars.items(): - try: - values[key] = value.get() - except (KeyError, LookupError): - pass - return values - def set_current(self, context: Context) -> None: """See `opentelemetry.context.RuntimeContext.set_current`.""" self._current_context.set(context) @@ -76,7 +66,13 @@ def get_current(self) -> Context: try: return self._current_context.get() except LookupError: - self.set_current(Context(self.snapshot())) + values = {} + for key, value in self._contextvars.items(): + try: + values[key] = value.get() + except (KeyError, LookupError): + pass + self.set_current(Context(values)) return self._current_context.get() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py index 3d10b53ff2..2589a01422 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py @@ -49,12 +49,6 @@ def remove_value(self, key: str) -> None: except AttributeError: pass - def snapshot(self) -> typing.Dict[str, "object"]: - """See `opentelemetry.context.RuntimeContext.snapshot`.""" - return dict( - (key, value) for key, value in self._thread_local.__dict__.items() - ) - def set_current(self, context: Context) -> None: """See `opentelemetry.context.RuntimeContext.set_current`.""" setattr(self._current_context, _CONTEXT_KEY, context) @@ -64,8 +58,12 @@ def get_current(self) -> Context: try: got = getattr(self._current_context, _CONTEXT_KEY) # type: object except AttributeError: + values = dict( + (key, value) + for key, value in self._thread_local.__dict__.items() + ) setattr( - self._current_context, _CONTEXT_KEY, Context(self.snapshot()), + self._current_context, _CONTEXT_KEY, Context(values), ) got = getattr(self._current_context, _CONTEXT_KEY) return got From 5376e6049257f924f4312e8135a76591dc64d97e Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 11 Feb 2020 11:27:41 -0800 Subject: [PATCH 46/56] cleaning up unused methods Signed-off-by: Alex Boten --- .../src/opentelemetry/context/context.py | 25 ------------------- .../opentelemetry/context/default_context.py | 12 --------- .../sdk/context/contextvars_context.py | 20 --------------- .../sdk/context/threadlocal_context.py | 19 -------------- 4 files changed, 76 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/context.py b/opentelemetry-api/src/opentelemetry/context/context.py index e17af68dfe..033fcc86ce 100644 --- a/opentelemetry-api/src/opentelemetry/context/context.py +++ b/opentelemetry-api/src/opentelemetry/context/context.py @@ -28,31 +28,6 @@ class RuntimeContext(ABC): selected through environment variables. """ - @abstractmethod - def set_value(self, key: str, value: "object") -> None: - """Set a value in this `RuntimeContext`. - - Args: - key: The key for the value to set. - value: The value to set. - """ - - @abstractmethod - def get_value(self, key: str) -> "object": - """Get a value from this `RuntimeContext`. - - Args: - key: The key for the value to retrieve. - """ - - @abstractmethod - def remove_value(self, key: str) -> None: - """Remove a value from this `RuntimeContext`. - - Args: - key: The key for the value to remove. - """ - @abstractmethod def set_current(self, context: Context) -> None: """ Sets the current `Context` object. diff --git a/opentelemetry-api/src/opentelemetry/context/default_context.py b/opentelemetry-api/src/opentelemetry/context/default_context.py index a936a4a0a7..fefc0fe623 100644 --- a/opentelemetry-api/src/opentelemetry/context/default_context.py +++ b/opentelemetry-api/src/opentelemetry/context/default_context.py @@ -25,18 +25,6 @@ def __init__(self) -> None: self._values = {} # type: typing.Dict[str, object] self._current_context = None # type: typing.Optional[Context] - def set_value(self, key: str, value: "object") -> None: - """See `opentelemetry.context.RuntimeContext.set_value`.""" - self._values[key] = value - - def get_value(self, key: str) -> "object": - """See `opentelemetry.context.RuntimeContext.get_value`.""" - return self._values.get(key) - - def remove_value(self, key: str) -> None: - """See `opentelemetry.context.RuntimeContext.remove_value`.""" - self._values.pop(key, None) - def set_current(self, context: Context) -> None: """See `opentelemetry.context.RuntimeContext.set_current`.""" self._current_context = context diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py index fa1e8038bc..ba1ce60ef6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py @@ -37,26 +37,6 @@ def __init__(self) -> None: self._contextvars = {} # type: typing.Dict[str, ContextVar[object]] self._current_context = ContextVar(_CONTEXT_KEY) - def set_value(self, key: str, value: "object") -> None: - """See `opentelemetry.context.RuntimeContext.set_value`.""" - if key not in self._contextvars.keys(): - self._contextvars[key] = ContextVar(key) - - self._contextvars[key].set(value) - - def get_value(self, key: str) -> "object": - """See `opentelemetry.context.RuntimeContext.get_value`.""" - if key in self._contextvars: - try: - return self._contextvars[key].get() - except (KeyError, LookupError): - pass - return None - - def remove_value(self, key: str) -> None: - """See `opentelemetry.context.RuntimeContext.remove_value`.""" - self._contextvars.pop(key, None) - def set_current(self, context: Context) -> None: """See `opentelemetry.context.RuntimeContext.set_current`.""" self._current_context.set(context) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py index 2589a01422..a656f6862a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py @@ -30,25 +30,6 @@ def __init__(self) -> None: self._thread_local = threading.local() self._current_context = threading.local() - def set_value(self, key: str, value: "object") -> None: - """See `opentelemetry.context.RuntimeContext.set_value`.""" - setattr(self._thread_local, key, value) - - def get_value(self, key: str) -> "object": - """See `opentelemetry.context.RuntimeContext.get_value`.""" - try: - got = getattr(self._thread_local, key) # type: object - return got - except AttributeError: - return None - - def remove_value(self, key: str) -> None: - """See `opentelemetry.context.RuntimeContext.remove_value`.""" - try: - delattr(self._thread_local, key) - except AttributeError: - pass - def set_current(self, context: Context) -> None: """See `opentelemetry.context.RuntimeContext.set_current`.""" setattr(self._current_context, _CONTEXT_KEY, context) From e7d0286b0bf52c0786d41ceb71c51152448fe58d Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 11 Feb 2020 11:34:31 -0800 Subject: [PATCH 47/56] lint fixes Signed-off-by: Alex Boten --- opentelemetry-api/src/opentelemetry/context/__init__.py | 1 - .../src/opentelemetry/sdk/context/threadlocal_context.py | 1 - 2 files changed, 2 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 212e21c7c0..6edb3ee591 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -16,7 +16,6 @@ import typing from os import environ - from pkg_resources import iter_entry_points from opentelemetry.context.context import Context, RuntimeContext diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py index a656f6862a..79d3bf0af9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py @@ -13,7 +13,6 @@ # limitations under the License. import threading -import typing from opentelemetry.context import Context, RuntimeContext From 0cee4627c73445a8c8e51725765122756f48fdb6 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 11 Feb 2020 13:43:44 -0800 Subject: [PATCH 48/56] further sinmplifying RuntimeContext Signed-off-by: Alex Boten --- .../src/opentelemetry/context/__init__.py | 36 ++++++++----------- .../opentelemetry/context/default_context.py | 4 +-- .../sdk/context/contextvars_context.py | 9 +---- .../sdk/context/threadlocal_context.py | 7 +--- 4 files changed, 18 insertions(+), 38 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 6edb3ee591..6d23e2930a 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -74,30 +74,26 @@ def remove_value( return Context(new_values) -def _init_runtime() -> None: - # FIXME use a better implementation of a configuration manager to avoid having - # to get configuration values straight from environment variables - - global _RUNTIME_CONTEXT # pylint: disable=global-statement - configured_context = environ.get( - "OPENTELEMETRY_CONTEXT", "default_context" - ) # type: str - try: - _RUNTIME_CONTEXT = next( - iter_entry_points("opentelemetry_context", configured_context) - ).load()() - except Exception: # pylint: disable=broad-except - logger.error("Failed to load context: %s", configured_context) - - def get_current() -> Context: """To access the context associated with program execution, the RuntimeContext API provides a function which takes no arguments and returns a RuntimeContext. """ + global _RUNTIME_CONTEXT # pylint: disable=global-statement if _RUNTIME_CONTEXT is None: - _init_runtime() + # FIXME use a better implementation of a configuration manager to avoid having + # to get configuration values straight from environment variables + + configured_context = environ.get( + "OPENTELEMETRY_CONTEXT", "default_context" + ) # type: str + try: + _RUNTIME_CONTEXT = next( + iter_entry_points("opentelemetry_context", configured_context) + ).load()() + except Exception: # pylint: disable=broad-except + logger.error("Failed to load context: %s", configured_context) return _RUNTIME_CONTEXT.get_current() # type:ignore @@ -109,11 +105,9 @@ def set_current(context: Context) -> Context: Args: context: The context to use as current. """ - if _RUNTIME_CONTEXT is None: - _init_runtime() - old_context = _RUNTIME_CONTEXT.get_current() # type:ignore + old_context = get_current() _RUNTIME_CONTEXT.set_current(Context(context.copy())) # type:ignore - return old_context # type:ignore + return old_context def with_current_context( diff --git a/opentelemetry-api/src/opentelemetry/context/default_context.py b/opentelemetry-api/src/opentelemetry/context/default_context.py index fefc0fe623..f36e317621 100644 --- a/opentelemetry-api/src/opentelemetry/context/default_context.py +++ b/opentelemetry-api/src/opentelemetry/context/default_context.py @@ -22,7 +22,6 @@ class DefaultRuntimeContext(RuntimeContext): """ def __init__(self) -> None: - self._values = {} # type: typing.Dict[str, object] self._current_context = None # type: typing.Optional[Context] def set_current(self, context: Context) -> None: @@ -32,8 +31,7 @@ def set_current(self, context: Context) -> None: def get_current(self) -> Context: """See `opentelemetry.context.RuntimeContext.get_current`.""" if self._current_context is None: - values = dict((key, value) for key, value in self._values.items()) - self._current_context = Context(values) + self._current_context = Context() return self._current_context diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py index ba1ce60ef6..a1a876ee83 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py @@ -34,7 +34,6 @@ class ContextVarsRuntimeContext(RuntimeContext): """ def __init__(self) -> None: - self._contextvars = {} # type: typing.Dict[str, ContextVar[object]] self._current_context = ContextVar(_CONTEXT_KEY) def set_current(self, context: Context) -> None: @@ -46,13 +45,7 @@ def get_current(self) -> Context: try: return self._current_context.get() except LookupError: - values = {} - for key, value in self._contextvars.items(): - try: - values[key] = value.get() - except (KeyError, LookupError): - pass - self.set_current(Context(values)) + self.set_current(Context()) return self._current_context.get() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py index 79d3bf0af9..d41a5e22d5 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py @@ -26,7 +26,6 @@ class ThreadLocalRuntimeContext(RuntimeContext): """ def __init__(self) -> None: - self._thread_local = threading.local() self._current_context = threading.local() def set_current(self, context: Context) -> None: @@ -38,12 +37,8 @@ def get_current(self) -> Context: try: got = getattr(self._current_context, _CONTEXT_KEY) # type: object except AttributeError: - values = dict( - (key, value) - for key, value in self._thread_local.__dict__.items() - ) setattr( - self._current_context, _CONTEXT_KEY, Context(values), + self._current_context, _CONTEXT_KEY, Context(), ) got = getattr(self._current_context, _CONTEXT_KEY) return got From f8032c1c355f9664cef108d406ef1d7a6b0418ad Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Tue, 11 Feb 2020 13:45:12 -0800 Subject: [PATCH 49/56] lint fix Signed-off-by: Alex Boten --- .../src/opentelemetry/sdk/context/contextvars_context.py | 1 - 1 file changed, 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py index a1a876ee83..72b8520cef 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py @@ -11,7 +11,6 @@ # 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 contextvars import ContextVar from sys import version_info From 02b8f7171f112b7074dc1874417da4001776fe66 Mon Sep 17 00:00:00 2001 From: alrex Date: Wed, 12 Feb 2020 08:57:23 -0800 Subject: [PATCH 50/56] Update opentelemetry-api/src/opentelemetry/context/__init__.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Mauricio Vásquez --- opentelemetry-api/src/opentelemetry/context/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 6d23e2930a..6e9091f4d8 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -25,7 +25,7 @@ def get_value(key: str, context: typing.Optional[Context] = None) -> "object": - """To access the local state of an concern, the RuntimeContext API + """To access the local state of a concern, the RuntimeContext API provides a function which takes a context and a key as input, and returns a value. From 727ae5029055d2e26f2f528773a60ed6f318f2fc Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 12 Feb 2020 09:15:18 -0800 Subject: [PATCH 51/56] adding test, updating copyright on new files Signed-off-by: Alex Boten --- opentelemetry-api/src/opentelemetry/context/context.py | 2 +- .../src/opentelemetry/context/default_context.py | 8 ++------ .../src/opentelemetry/trace/propagation/__init__.py | 2 +- opentelemetry-api/tests/context/test_context.py | 5 ++++- .../src/opentelemetry/sdk/context/aiocontextvarsfix.py | 2 +- .../src/opentelemetry/sdk/context/contextvars_context.py | 2 +- .../src/opentelemetry/sdk/context/threadlocal_context.py | 2 +- opentelemetry-sdk/tests/conftest.py | 2 +- opentelemetry-sdk/tests/context/test_asyncio.py | 2 +- opentelemetry-sdk/tests/context/test_context.py | 2 +- opentelemetry-sdk/tests/context/test_threads.py | 2 +- 11 files changed, 15 insertions(+), 16 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/context.py b/opentelemetry-api/src/opentelemetry/context/context.py index 033fcc86ce..148312a884 100644 --- a/opentelemetry-api/src/opentelemetry/context/context.py +++ b/opentelemetry-api/src/opentelemetry/context/context.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/src/opentelemetry/context/default_context.py b/opentelemetry-api/src/opentelemetry/context/default_context.py index f36e317621..6c83f839d3 100644 --- a/opentelemetry-api/src/opentelemetry/context/default_context.py +++ b/opentelemetry-api/src/opentelemetry/context/default_context.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,8 +11,6 @@ # 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.context.context import Context, RuntimeContext @@ -22,7 +20,7 @@ class DefaultRuntimeContext(RuntimeContext): """ def __init__(self) -> None: - self._current_context = None # type: typing.Optional[Context] + self._current_context = Context() def set_current(self, context: Context) -> None: """See `opentelemetry.context.RuntimeContext.set_current`.""" @@ -30,8 +28,6 @@ def set_current(self, context: Context) -> None: def get_current(self) -> Context: """See `opentelemetry.context.RuntimeContext.get_current`.""" - if self._current_context is None: - self._current_context = Context() return self._current_context diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py index 602088a2c1..67d8a76a53 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-api/tests/context/test_context.py b/opentelemetry-api/tests/context/test_context.py index 722393cbef..2536e5149b 100644 --- a/opentelemetry-api/tests/context/test_context.py +++ b/opentelemetry-api/tests/context/test_context.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -60,3 +60,6 @@ def test_set_current(self): old_context = context.set_current(context.set_value("a", "zzz")) self.assertEqual("yyy", context.get_value("a", context=old_context)) self.assertEqual("zzz", context.get_value("a")) + + context.set_current(old_context) + self.assertEqual("yyy", context.get_value("a")) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/aiocontextvarsfix.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/aiocontextvarsfix.py index 21287eb699..6aa1779378 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/aiocontextvarsfix.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/aiocontextvarsfix.py @@ -1,5 +1,5 @@ # type: ignore -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py index 72b8520cef..f3fa9c7fc7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py index d41a5e22d5..27f04129c9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/tests/conftest.py b/opentelemetry-sdk/tests/conftest.py index 15f185b408..59e306f130 100644 --- a/opentelemetry-sdk/tests/conftest.py +++ b/opentelemetry-sdk/tests/conftest.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/tests/context/test_asyncio.py b/opentelemetry-sdk/tests/context/test_asyncio.py index 2ebf690e1f..5dc3637598 100644 --- a/opentelemetry-sdk/tests/context/test_asyncio.py +++ b/opentelemetry-sdk/tests/context/test_asyncio.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/tests/context/test_context.py b/opentelemetry-sdk/tests/context/test_context.py index db40e49004..88a63109d1 100644 --- a/opentelemetry-sdk/tests/context/test_context.py +++ b/opentelemetry-sdk/tests/context/test_context.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/opentelemetry-sdk/tests/context/test_threads.py b/opentelemetry-sdk/tests/context/test_threads.py index 21b89c85d8..e8552b9135 100644 --- a/opentelemetry-sdk/tests/context/test_threads.py +++ b/opentelemetry-sdk/tests/context/test_threads.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From f25992ff312b5941f07108b232b89af6db6564af Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 12 Feb 2020 09:50:39 -0800 Subject: [PATCH 52/56] Removing unnecessary copy Signed-off-by: Alex Boten --- opentelemetry-api/src/opentelemetry/context/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 6e9091f4d8..1d6bff15d0 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -106,7 +106,7 @@ def set_current(context: Context) -> Context: context: The context to use as current. """ old_context = get_current() - _RUNTIME_CONTEXT.set_current(Context(context.copy())) # type:ignore + _RUNTIME_CONTEXT.set_current(Context(context)) # type:ignore return old_context From 91d2ebc5e2d9a4fa6c106855dcad7083f1e6341f Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 12 Feb 2020 10:34:04 -0800 Subject: [PATCH 53/56] set default for contextvars Signed-off-by: Alex Boten --- .../src/opentelemetry/sdk/context/contextvars_context.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py index f3fa9c7fc7..c26a94aaab 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py @@ -33,7 +33,7 @@ class ContextVarsRuntimeContext(RuntimeContext): """ def __init__(self) -> None: - self._current_context = ContextVar(_CONTEXT_KEY) + self._current_context = ContextVar(_CONTEXT_KEY, default=Context()) def set_current(self, context: Context) -> None: """See `opentelemetry.context.RuntimeContext.set_current`.""" @@ -41,11 +41,7 @@ def set_current(self, context: Context) -> None: def get_current(self) -> Context: """See `opentelemetry.context.RuntimeContext.get_current`.""" - try: - return self._current_context.get() - except LookupError: - self.set_current(Context()) - return self._current_context.get() + return self._current_context.get() __all__ = ["ContextVarsRuntimeContext"] From 335678ea52338b2516c04ad0e88dfa7b7e24bb16 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 12 Feb 2020 16:38:25 -0800 Subject: [PATCH 54/56] feedback from review Signed-off-by: Alex Boten --- .../sdk/context/contextvars_context.py | 9 +++++---- .../sdk/context/threadlocal_context.py | 15 ++++++--------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py index c26a94aaab..0a350e2699 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/contextvars_context.py @@ -24,16 +24,17 @@ import opentelemetry.sdk.context.aiocontextvarsfix # pylint:disable=unused-import -_CONTEXT_KEY = "current_context" - - class ContextVarsRuntimeContext(RuntimeContext): """An implementation of the RuntimeContext interface which wraps ContextVar under the hood. This is the prefered implementation for usage with Python 3.5+ """ + _CONTEXT_KEY = "current_context" + def __init__(self) -> None: - self._current_context = ContextVar(_CONTEXT_KEY, default=Context()) + self._current_context = ContextVar( + self._CONTEXT_KEY, default=Context() + ) def set_current(self, context: Context) -> None: """See `opentelemetry.context.RuntimeContext.set_current`.""" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py index 27f04129c9..26d4329c52 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/threadlocal_context.py @@ -16,8 +16,6 @@ from opentelemetry.context import Context, RuntimeContext -_CONTEXT_KEY = "current_context" - class ThreadLocalRuntimeContext(RuntimeContext): """An implementation of the RuntimeContext interface @@ -25,23 +23,22 @@ class ThreadLocalRuntimeContext(RuntimeContext): implementation is available for usage with Python 3.4. """ + _CONTEXT_KEY = "current_context" + def __init__(self) -> None: self._current_context = threading.local() def set_current(self, context: Context) -> None: """See `opentelemetry.context.RuntimeContext.set_current`.""" - setattr(self._current_context, _CONTEXT_KEY, context) + setattr(self._current_context, self._CONTEXT_KEY, context) def get_current(self) -> Context: """See `opentelemetry.context.RuntimeContext.get_current`.""" - try: - got = getattr(self._current_context, _CONTEXT_KEY) # type: object - except AttributeError: + if not hasattr(self._current_context, self._CONTEXT_KEY): setattr( - self._current_context, _CONTEXT_KEY, Context(), + self._current_context, self._CONTEXT_KEY, Context(), ) - got = getattr(self._current_context, _CONTEXT_KEY) - return got + return getattr(self._current_context, self._CONTEXT_KEY) __all__ = ["ThreadLocalRuntimeContext"] From 619fc1bb01d5d8a9e744cbb6528232fc8ee59723 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Wed, 12 Feb 2020 21:07:01 -0800 Subject: [PATCH 55/56] minor fix Signed-off-by: Alex Boten --- opentelemetry-api/src/opentelemetry/context/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 1d6bff15d0..84fb40a50c 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -106,7 +106,7 @@ def set_current(context: Context) -> Context: context: The context to use as current. """ old_context = get_current() - _RUNTIME_CONTEXT.set_current(Context(context)) # type:ignore + _RUNTIME_CONTEXT.set_current(context) # type:ignore return old_context From 1f9bd7677a0ae2b7d427445aabf12ce4a8ce25e9 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Thu, 13 Feb 2020 13:17:57 -0800 Subject: [PATCH 56/56] review feedback Signed-off-by: Alex Boten --- .../src/opentelemetry/context/__init__.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 84fb40a50c..63de570abc 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -128,13 +128,3 @@ def call_with_current_context( set_current(backup) return call_with_current_context - - -__all__ = [ - "get_value", - "set_value", - "remove_value", - "get_current", - "set_current", - "RuntimeContext", -]