From 85c45435479d2c8f618397ba628ce869997ed7d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Tue, 25 Jun 2019 10:11:37 +0200 Subject: [PATCH 01/20] WIP --- opentelemetry-api/opentelemetry/backend.py | 97 ++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 opentelemetry-api/opentelemetry/backend.py diff --git a/opentelemetry-api/opentelemetry/backend.py b/opentelemetry-api/opentelemetry/backend.py new file mode 100644 index 00000000000..5681d6eae56 --- /dev/null +++ b/opentelemetry-api/opentelemetry/backend.py @@ -0,0 +1,97 @@ +# 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. + + +""" +The OpenTelemetry backend module defines and implements the API to access an +implementation (backend) of the OpenTelemetry API. +""" + +from typing import Callable, Any +import traceback +import sys +import os +import importlib + +from .trace import Tracer + +# REMOVE {{{ +# Callback that creates the default implementation +_PROVIDER: Callable[[type], object] = None + +_PROVIDER_TB: str = None + +def register_provider(set_provider_cb: Callable): + global _PROVIDER # pylint:disable=global-statement + global _PROVIDER_TB # pylint:disable=global-statement + + if _PROVIDER: + raise ValueError( + "A provider was already registered. Original" + " registration's traceback: " + str(_PROVIDER_TB)) + assert _PROVIDER_TB is None, 'Inconsistent state: ' + str(_PROVIDER_TB) + _PROVIDER_TB = ''.join(traceback.format_stack()) + if _IMPLEMENTATION: + _PROVIDER = True + else: + _PROVIDER = set_provider_cb +# }}} + +DEFAULT_BACKEND_MODNAME = 'opentelemetry.sdk.internal.backend_impl' + +def _get_fallback_impl(type_: type) -> Any: + """Gets the fallback implementation for `type_`. + + `type_` must be a OpenTelemetry API type like `Tracer`. + + First, the function tries to find a module that provides a `get_opentelemetry_backend_impl` + function (with the same signature as this function). The following modules are tried: + + 1. `$OPENTELEMETRY_PYTHON_BACKEND_` (e.g. `OPENTELEMETRY_PYTHON_BACKEND_TRACER`) + 2. `$OPENTELEMETRY_PYTHON_BACKEND_DEFAULT` (e.g. `OPENTELEMETRY_PYTHON_BACKEND_TRACER`) + 3. The OpenTelemetry SDK's tracer module. + + Note that if any of the environment variables is set to an nonempty value, further steps + are not tried, even if the modulename set there is invalid or fails to load. The no-op API + implementation is returned instead. + """ + if not sys.flags.ignore_environment: + backend_modname = os.getenv('OPENTELEMETRY_PYTHON_BACKEND_' + type_.__name__.upper()) + if not backend_modname: + backend_modname = os.getenv('OPENTELEMETRY_PYTHON_BACKEND_DEFAULT') + if not backend_modname: + backend_modname = DEFAULT_BACKEND_MODNAME + if backend_modname: + try: + backend_mod = importlib.import_module(backend_modname) + except (ImportError, SyntaxError): + # TODO Log/warn + return type_() + try: + backend_fn = getattr(backend_mod, 'get_backend_impl') + except AttributeError: + # TODO Log/warn + return type_() + result = backend_fn(type_) + if not result: + # This is an expected case. + return type_() + # TODO: Warn if backend_fn returns type_(): It should return None to indicate using the + # default. + + + + +def get_tracer() -> Tracer: + return _selectimpl(Tracer) From c952681095b37eaecddce03f8b044beed7c89c77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Tue, 25 Jun 2019 18:32:39 +0200 Subject: [PATCH 02/20] WIP I was too "clever", the getter needs to be rewritten --- opentelemetry-api/opentelemetry/backend.py | 131 +++++++++++++-------- opentelemetry-api/tests/test_backend.py | 65 ++++++++++ 2 files changed, 145 insertions(+), 51 deletions(-) create mode 100644 opentelemetry-api/tests/test_backend.py diff --git a/opentelemetry-api/opentelemetry/backend.py b/opentelemetry-api/opentelemetry/backend.py index 5681d6eae56..e256f059ca7 100644 --- a/opentelemetry-api/opentelemetry/backend.py +++ b/opentelemetry-api/opentelemetry/backend.py @@ -18,42 +18,27 @@ implementation (backend) of the OpenTelemetry API. """ -from typing import Callable, Any -import traceback import sys import os import importlib +from typing import TypeVar, Type, Callable +from functools import wraps from .trace import Tracer -# REMOVE {{{ -# Callback that creates the default implementation -_PROVIDER: Callable[[type], object] = None +### Generic private code {{{1 -_PROVIDER_TB: str = None +_T = TypeVar('_T') -def register_provider(set_provider_cb: Callable): - global _PROVIDER # pylint:disable=global-statement - global _PROVIDER_TB # pylint:disable=global-statement +_DEFAULT_BACKEND_MODNAME = 'opentelemetry.sdk.internal.backend_impl' +_UNIT_TEST_IGNORE_ENV = False - if _PROVIDER: - raise ValueError( - "A provider was already registered. Original" - " registration's traceback: " + str(_PROVIDER_TB)) - assert _PROVIDER_TB is None, 'Inconsistent state: ' + str(_PROVIDER_TB) - _PROVIDER_TB = ''.join(traceback.format_stack()) - if _IMPLEMENTATION: - _PROVIDER = True - else: - _PROVIDER = set_provider_cb -# }}} +_tracer = None -DEFAULT_BACKEND_MODNAME = 'opentelemetry.sdk.internal.backend_impl' +def _get_fallback_impl(api_type: Type[_T]) -> _T: + """Gets the fallback implementation for `api_type`. -def _get_fallback_impl(type_: type) -> Any: - """Gets the fallback implementation for `type_`. - - `type_` must be a OpenTelemetry API type like `Tracer`. + `api_type` must be a OpenTelemetry API type like `Tracer`. First, the function tries to find a module that provides a `get_opentelemetry_backend_impl` function (with the same signature as this function). The following modules are tried: @@ -66,32 +51,76 @@ def _get_fallback_impl(type_: type) -> Any: are not tried, even if the modulename set there is invalid or fails to load. The no-op API implementation is returned instead. """ - if not sys.flags.ignore_environment: - backend_modname = os.getenv('OPENTELEMETRY_PYTHON_BACKEND_' + type_.__name__.upper()) + backend_modname = None + if not _UNIT_TEST_IGNORE_ENV and not sys.flags.ignore_environment: + backend_modname = os.getenv('OPENTELEMETRY_PYTHON_BACKEND_' + api_type.__name__.upper()) + breakpoint() if not backend_modname: backend_modname = os.getenv('OPENTELEMETRY_PYTHON_BACKEND_DEFAULT') - if not backend_modname: - backend_modname = DEFAULT_BACKEND_MODNAME - if backend_modname: - try: - backend_mod = importlib.import_module(backend_modname) - except (ImportError, SyntaxError): - # TODO Log/warn - return type_() - try: - backend_fn = getattr(backend_mod, 'get_backend_impl') - except AttributeError: - # TODO Log/warn - return type_() - result = backend_fn(type_) - if not result: - # This is an expected case. - return type_() - # TODO: Warn if backend_fn returns type_(): It should return None to indicate using the - # default. - - - - -def get_tracer() -> Tracer: + if not backend_modname: + backend_modname = _DEFAULT_BACKEND_MODNAME + try: + backend_mod = importlib.import_module(backend_modname) + except (ImportError, SyntaxError): + # TODO Log/warn + return api_type() + try: + # Note: We use such a long name to avoid called + backend_fn: Callable[[Type[_T]], object] = getattr(backend_mod, 'get_opentelemetry_backend_impl') + except AttributeError: + # TODO Log/warn + return api_type() + result = backend_fn(api_type) + if result and isinstance(result, api_type): + # TODO: Warn if backend_fn returns api_type(): It should return None to indicate using the + # default. + return result + return api_type() + +_GETTER_TPL = ''' +def make_getter(obj=obj): + @wraps(original_func) + def dyn_{getter_name}() -> api_type: return obj + + return dyn_{getter_name} +''' + +def _set_backend_object(api_type: Type[_T], obj: _T) -> None: + """Set the backend object by compiling a function that returns it without accessing a global for + maximum performance.""" + + if not isinstance(obj, api_type): + raise ValueError('obj is not an instance of api_type. obj\'s type: ' + str(api_type)) + + # Note: At some time, instead of lower(), we might need to convert to snake_case here. + getter_name = api_type.__name__.lower() + + getter_src = _GETTER_TPL.format(getter_name=getter_name) + getter_code = compile(getter_src, ''.format(api_type.__name__), 'exec') + original_func = globals()[getter_name] + if hasattr(original_func, '__wraps__'): + original_func = original_func.__wraps__ + assert not hasattr(original_func, '__wraps__') + scope = dict( + api_type=api_type, + wraps=wraps, + original_func=original_func, + obj=obj) + exec(getter_code, scope) #pylint:disable=exec-used + globals()[getter_name] = scope['make_getter']() + + +def _selectimpl(api_type: Type[_T]) -> _T: + impl = _get_fallback_impl(api_type) + assert impl + _set_backend_object(api_type, impl) + return impl + + +### Public code (basically copy & paste for each type) {{{1 + +def tracer() -> Tracer: return _selectimpl(Tracer) + +def set_tracer(tracer_implementation: Tracer) -> None: + _set_backend_object(Tracer, tracer_implementation) diff --git a/opentelemetry-api/tests/test_backend.py b/opentelemetry-api/tests/test_backend.py new file mode 100644 index 00000000000..185e6a1b813 --- /dev/null +++ b/opentelemetry-api/tests/test_backend.py @@ -0,0 +1,65 @@ +from importlib import reload +import sys +import os + +from opentelemetry import backend +from opentelemetry.trace import Tracer + +class DummyTracer(Tracer): + pass + +dummy_tracer = None + +def get_opentelemetry_backend_impl(type_): + global dummy_tracer + assert type_ is Tracer + dummy_tracer = DummyTracer() + return dummy_tracer + +#pylint:disable=redefined-outer-name + +def test_get_default(backend=backend): + backend = reload(backend) + backend._UNIT_TEST_IGNORE_ENV = True + tracer = backend.tracer() + assert type(tracer) is Tracer + +def test_get_set(backend=backend): + backend = reload(backend) + set_tracer = DummyTracer() + backend.set_tracer(set_tracer) + tracer = backend.tracer() + assert tracer is set_tracer + + # Set again + set_tracer = DummyTracer() + backend.set_tracer(set_tracer) + tracer = backend.tracer() + assert tracer is set_tracer + +def test_get_set_import_from(backend=backend): + backend = reload(backend) + get_tracer = backend.tracer # Simulate `import tracer from backend as get_tracer` + set_tracer = DummyTracer() + backend.set_tracer(set_tracer) + tracer = get_tracer() + assert tracer is set_tracer + + # Set again + set_tracer = DummyTracer() + backend.set_tracer(set_tracer) + tracer = get_tracer() + assert tracer is set_tracer + +def test_get_envvar(monkeypatch, backend=backend): + global dummy_tracer + + backend = reload(backend) + assert not sys.flags.ignore_environment # Test is not runnable with this! + monkeypatch.setenv('OPENTELEMETRY_PYTHON_BACKEND_TRACER', __name__) + try: + tracer = backend.tracer() + assert tracer is dummy_tracer + finally: + dummy_tracer = None + assert type(tracer) is DummyTracer From 719df6efa83b3c750e57cffe1328f256dae7dcec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Tue, 25 Jun 2019 19:21:58 +0200 Subject: [PATCH 03/20] Global tracer registry: First somewhat satisfying iteration. --- opentelemetry-api/opentelemetry/backend.py | 81 ++++++++++++---------- opentelemetry-api/tests/test_backend.py | 24 ++++--- 2 files changed, 56 insertions(+), 49 deletions(-) diff --git a/opentelemetry-api/opentelemetry/backend.py b/opentelemetry-api/opentelemetry/backend.py index e256f059ca7..d969490e489 100644 --- a/opentelemetry-api/opentelemetry/backend.py +++ b/opentelemetry-api/opentelemetry/backend.py @@ -16,6 +16,25 @@ """ The OpenTelemetry backend module defines and implements the API to access an implementation (backend) of the OpenTelemetry API. + +By default, if you call a getter function (e.g., :func:`tracer`) and the corresponding setter (e.g., +:func:`set_tracer`) wasn't called, you will get a default implementation, which is selected as follows: + + 1. If the environment variable OPENTELEMETRY_PYTHON_BACKEND_` (e.g., + OPENTELEMETRY_PYTHON_BACKEND_TRACER) is set to an nonempty value, an attempt is made to + import a module with that name and call a function `get_opentelemetry_backend_impl` in it. + The function receives the API type that is expected as an argument and should return an + instance of it (e.g., the argument is :cls:`opentelemetry.trace.Tracer` and the function should + return an instance of a :cls:`~Tracer` (probably of a derived type). + 2. If the variable is not set, `$OPENTELEMETRY_PYTHON_BACKEND_DEFAULT` is tried instead. + 3. If that variable was also not set, an attempt is made to import and use the OpenTelemetry + SDK. + 4. Otherwise (if no variable was set and the SDK was not importable, or an error occured when + trying to instantiate the implementation object) the default implementation that ships with the + API distribution (a fast no-op implementation) is used. + +If you called the setter or if you call the setter later (not recommended), the object you set is +used instead. """ import sys @@ -36,9 +55,10 @@ _tracer = None def _get_fallback_impl(api_type: Type[_T]) -> _T: - """Gets the fallback implementation for `api_type`. + # TODO: Move (most of) this to module docstring. + """Gets the fallback implementation for ``api_type``. - `api_type` must be a OpenTelemetry API type like `Tracer`. + ``api_type`` must be a OpenTelemetry API type like :cls:`Tracer`. First, the function tries to find a module that provides a `get_opentelemetry_backend_impl` function (with the same signature as this function). The following modules are tried: @@ -54,7 +74,6 @@ def _get_fallback_impl(api_type: Type[_T]) -> _T: backend_modname = None if not _UNIT_TEST_IGNORE_ENV and not sys.flags.ignore_environment: backend_modname = os.getenv('OPENTELEMETRY_PYTHON_BACKEND_' + api_type.__name__.upper()) - breakpoint() if not backend_modname: backend_modname = os.getenv('OPENTELEMETRY_PYTHON_BACKEND_DEFAULT') if not backend_modname: @@ -65,7 +84,8 @@ def _get_fallback_impl(api_type: Type[_T]) -> _T: # TODO Log/warn return api_type() try: - # Note: We use such a long name to avoid called + # Note: We use such a long name to avoid calling a function that is not intended for this + # API. backend_fn: Callable[[Type[_T]], object] = getattr(backend_mod, 'get_opentelemetry_backend_impl') except AttributeError: # TODO Log/warn @@ -77,50 +97,35 @@ def _get_fallback_impl(api_type: Type[_T]) -> _T: return result return api_type() -_GETTER_TPL = ''' -def make_getter(obj=obj): - @wraps(original_func) - def dyn_{getter_name}() -> api_type: return obj - - return dyn_{getter_name} -''' - -def _set_backend_object(api_type: Type[_T], obj: _T) -> None: - """Set the backend object by compiling a function that returns it without accessing a global for - maximum performance.""" - - if not isinstance(obj, api_type): - raise ValueError('obj is not an instance of api_type. obj\'s type: ' + str(api_type)) - - # Note: At some time, instead of lower(), we might need to convert to snake_case here. - getter_name = api_type.__name__.lower() - - getter_src = _GETTER_TPL.format(getter_name=getter_name) - getter_code = compile(getter_src, ''.format(api_type.__name__), 'exec') - original_func = globals()[getter_name] - if hasattr(original_func, '__wraps__'): - original_func = original_func.__wraps__ - assert not hasattr(original_func, '__wraps__') - scope = dict( - api_type=api_type, - wraps=wraps, - original_func=original_func, - obj=obj) - exec(getter_code, scope) #pylint:disable=exec-used - globals()[getter_name] = scope['make_getter']() - - def _selectimpl(api_type: Type[_T]) -> _T: impl = _get_fallback_impl(api_type) assert impl - _set_backend_object(api_type, impl) + globals()[api_type.__name__.lower()] = impl return impl +def _set_backend_object(api_type: Type[_T], impl_object: _T) -> None: + if impl_object is None: + raise ValueError('None is not allowed as a backend implementation.') + if not isinstance(impl_object, api_type): + raise ValueError('The object does not implement the required base class.') + globals()['_' + api_type.__name__.lower()] = impl_object ### Public code (basically copy & paste for each type) {{{1 def tracer() -> Tracer: + """Gets the current global :cls:`Tracer` object. + + If there isn't one set yet, a default will be used (see module documentation). + """ + + if _tracer: + return _tracer return _selectimpl(Tracer) def set_tracer(tracer_implementation: Tracer) -> None: + """Sets the global :cls:`Tracer` object. + + Further calls to ``tracer`` will return ``tracer_implementation``. + """ + _set_backend_object(Tracer, tracer_implementation) diff --git a/opentelemetry-api/tests/test_backend.py b/opentelemetry-api/tests/test_backend.py index 185e6a1b813..697a546a2e2 100644 --- a/opentelemetry-api/tests/test_backend.py +++ b/opentelemetry-api/tests/test_backend.py @@ -1,6 +1,7 @@ from importlib import reload import sys -import os + +import pytest from opentelemetry import backend from opentelemetry.trace import Tracer @@ -8,15 +9,15 @@ class DummyTracer(Tracer): pass -dummy_tracer = None +DUMMY_TRACER = None def get_opentelemetry_backend_impl(type_): - global dummy_tracer + global DUMMY_TRACER #pylint:disable=global-statement assert type_ is Tracer - dummy_tracer = DummyTracer() - return dummy_tracer + DUMMY_TRACER = DummyTracer() + return DUMMY_TRACER -#pylint:disable=redefined-outer-name +#pylint:disable=redefined-outer-name,protected-access,unidiomatic-typecheck def test_get_default(backend=backend): backend = reload(backend) @@ -51,15 +52,16 @@ def test_get_set_import_from(backend=backend): tracer = get_tracer() assert tracer is set_tracer -def test_get_envvar(monkeypatch, backend=backend): - global dummy_tracer +@pytest.mark.parametrize('envvar_suffix', ['TRACER', 'DEFAULT']) +def test_get_envvar(envvar_suffix, monkeypatch, backend=backend): + global DUMMY_TRACER #pylint:disable=global-statement backend = reload(backend) assert not sys.flags.ignore_environment # Test is not runnable with this! - monkeypatch.setenv('OPENTELEMETRY_PYTHON_BACKEND_TRACER', __name__) + monkeypatch.setenv('OPENTELEMETRY_PYTHON_BACKEND_' + envvar_suffix, __name__) try: tracer = backend.tracer() - assert tracer is dummy_tracer + assert tracer is DUMMY_TRACER finally: - dummy_tracer = None + DUMMY_TRACER = None assert type(tracer) is DummyTracer From 8631647325deec57e57ce9fcc966f4229e45e2b8 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Tue, 25 Jun 2019 14:31:00 -0700 Subject: [PATCH 04/20] Add API docs for backend module --- docs/index.rst | 1 + docs/opentelemetry.backend.rst | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 docs/opentelemetry.backend.rst diff --git a/docs/index.rst b/docs/index.rst index aed678db966..159565bfb50 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,6 +16,7 @@ abstract types for OpenTelemetry implementations. :caption: Contents: opentelemetry.trace + opentelemetry.backend Indices and tables diff --git a/docs/opentelemetry.backend.rst b/docs/opentelemetry.backend.rst new file mode 100644 index 00000000000..e37ce551fd7 --- /dev/null +++ b/docs/opentelemetry.backend.rst @@ -0,0 +1,4 @@ +opentelemetry.backend module +============================ + +.. automodule:: opentelemetry.backend From 893a4494de7b95dee6574eb0fd393c58810691ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Wed, 26 Jun 2019 15:54:31 +0200 Subject: [PATCH 05/20] Fix documentation (and overlong lines within). --- opentelemetry-api/opentelemetry/backend.py | 43 ++++++++++++---------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/opentelemetry-api/opentelemetry/backend.py b/opentelemetry-api/opentelemetry/backend.py index d969490e489..99fb4d5a5e4 100644 --- a/opentelemetry-api/opentelemetry/backend.py +++ b/opentelemetry-api/opentelemetry/backend.py @@ -14,27 +14,30 @@ """ -The OpenTelemetry backend module defines and implements the API to access an -implementation (backend) of the OpenTelemetry API. +The OpenTelemetry backend module provides access to a selected implementation (backend) of the +OpenTelemetry API. By default, if you call a getter function (e.g., :func:`tracer`) and the corresponding setter (e.g., -:func:`set_tracer`) wasn't called, you will get a default implementation, which is selected as follows: +:func:`set_tracer`) wasn't called, you will get a default implementation, which is selected as +follows: - 1. If the environment variable OPENTELEMETRY_PYTHON_BACKEND_` (e.g., - OPENTELEMETRY_PYTHON_BACKEND_TRACER) is set to an nonempty value, an attempt is made to - import a module with that name and call a function `get_opentelemetry_backend_impl` in it. + 1. If the environment variable :samp:`OPENTELEMETRY_PYTHON_BACKEND_{getter-name}` (e.g., + ``OPENTELEMETRY_PYTHON_BACKEND_TRACER``) is set to an nonempty value, an attempt is made to + import a module with that name and call a function ``get_opentelemetry_backend_impl`` in it. The function receives the API type that is expected as an argument and should return an - instance of it (e.g., the argument is :cls:`opentelemetry.trace.Tracer` and the function should - return an instance of a :cls:`~Tracer` (probably of a derived type). - 2. If the variable is not set, `$OPENTELEMETRY_PYTHON_BACKEND_DEFAULT` is tried instead. + instance of it (e.g., the argument is :class:`opentelemetry.trace.Tracer` and the function + should return an instance of a :class:`~opentelemetry.trace.Tracer` (probably of a derived + type). + 2. If that variable is not set, ``OPENTELEMETRY_PYTHON_BACKEND_DEFAULT`` is tried instead. 3. If that variable was also not set, an attempt is made to import and use the OpenTelemetry SDK. 4. Otherwise (if no variable was set and the SDK was not importable, or an error occured when - trying to instantiate the implementation object) the default implementation that ships with the - API distribution (a fast no-op implementation) is used. + trying to instantiate the implementation object) the default implementation that ships with + the API distribution (a fast no-op implementation) is used. -If you called the setter or if you call the setter later (not recommended), the object you set is -used instead. +If you called the setter for an object before initializing it that search is not performed and +instead the object you set is used instead. It is also possible to call the setter later to override +the set tracer but this is not recommended. """ import sys @@ -58,13 +61,13 @@ def _get_fallback_impl(api_type: Type[_T]) -> _T: # TODO: Move (most of) this to module docstring. """Gets the fallback implementation for ``api_type``. - ``api_type`` must be a OpenTelemetry API type like :cls:`Tracer`. + ``api_type`` must be a OpenTelemetry API type like :class:`Tracer`. First, the function tries to find a module that provides a `get_opentelemetry_backend_impl` function (with the same signature as this function). The following modules are tried: - 1. `$OPENTELEMETRY_PYTHON_BACKEND_` (e.g. `OPENTELEMETRY_PYTHON_BACKEND_TRACER`) - 2. `$OPENTELEMETRY_PYTHON_BACKEND_DEFAULT` (e.g. `OPENTELEMETRY_PYTHON_BACKEND_TRACER`) + 1. ``$OPENTELEMETRY_PYTHON_BACKEND_`` (e.g. ``OPENTELEMETRY_PYTHON_BACKEND_TRACER`) + 2. ``$OPENTELEMETRY_PYTHON_BACKEND_DEFAULT`` (e.g. ``OPENTELEMETRY_PYTHON_BACKEND_TRACER`) 3. The OpenTelemetry SDK's tracer module. Note that if any of the environment variables is set to an nonempty value, further steps @@ -113,7 +116,7 @@ def _set_backend_object(api_type: Type[_T], impl_object: _T) -> None: ### Public code (basically copy & paste for each type) {{{1 def tracer() -> Tracer: - """Gets the current global :cls:`Tracer` object. + """Gets the current global :class:`~opentelemetry.trace.Tracer` object. If there isn't one set yet, a default will be used (see module documentation). """ @@ -123,9 +126,11 @@ def tracer() -> Tracer: return _selectimpl(Tracer) def set_tracer(tracer_implementation: Tracer) -> None: - """Sets the global :cls:`Tracer` object. + """Sets the global :class:`~opentelemetry.trace.Tracer` object. - Further calls to ``tracer`` will return ``tracer_implementation``. + Args: + tracer_implementation: The tracer object that should be returned by further calls to + :func:`tracer`. """ _set_backend_object(Tracer, tracer_implementation) From ea42a79365ed657569dd93c732cef144070b0a60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Wed, 26 Jun 2019 15:54:47 +0200 Subject: [PATCH 06/20] Sort imports. --- opentelemetry-api/opentelemetry/backend.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/opentelemetry-api/opentelemetry/backend.py b/opentelemetry-api/opentelemetry/backend.py index 99fb4d5a5e4..0cbcfc10a80 100644 --- a/opentelemetry-api/opentelemetry/backend.py +++ b/opentelemetry-api/opentelemetry/backend.py @@ -40,11 +40,10 @@ the set tracer but this is not recommended. """ -import sys -import os +from typing import Type, TypeVar import importlib -from typing import TypeVar, Type, Callable -from functools import wraps +import os +import sys from .trace import Tracer From f0ab57f3f11ff0b7a6b3189352446481a86e0ae2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Wed, 26 Jun 2019 15:59:51 +0200 Subject: [PATCH 07/20] Remove _UNIT_TEST_IGNORE_ENV. We should be able to ensure clean envvars for the test run. --- opentelemetry-api/opentelemetry/backend.py | 3 +-- opentelemetry-api/tests/test_backend.py | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/opentelemetry-api/opentelemetry/backend.py b/opentelemetry-api/opentelemetry/backend.py index 0cbcfc10a80..ceaf92e5578 100644 --- a/opentelemetry-api/opentelemetry/backend.py +++ b/opentelemetry-api/opentelemetry/backend.py @@ -52,7 +52,6 @@ _T = TypeVar('_T') _DEFAULT_BACKEND_MODNAME = 'opentelemetry.sdk.internal.backend_impl' -_UNIT_TEST_IGNORE_ENV = False _tracer = None @@ -74,7 +73,7 @@ def _get_fallback_impl(api_type: Type[_T]) -> _T: implementation is returned instead. """ backend_modname = None - if not _UNIT_TEST_IGNORE_ENV and not sys.flags.ignore_environment: + if not sys.flags.ignore_environment: backend_modname = os.getenv('OPENTELEMETRY_PYTHON_BACKEND_' + api_type.__name__.upper()) if not backend_modname: backend_modname = os.getenv('OPENTELEMETRY_PYTHON_BACKEND_DEFAULT') diff --git a/opentelemetry-api/tests/test_backend.py b/opentelemetry-api/tests/test_backend.py index 697a546a2e2..06499ea6639 100644 --- a/opentelemetry-api/tests/test_backend.py +++ b/opentelemetry-api/tests/test_backend.py @@ -21,7 +21,6 @@ def get_opentelemetry_backend_impl(type_): def test_get_default(backend=backend): backend = reload(backend) - backend._UNIT_TEST_IGNORE_ENV = True tracer = backend.tracer() assert type(tracer) is Tracer From 5fa65ebb794d6d286aa0c2fd71f580cd2abc8784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Wed, 26 Jun 2019 16:00:26 +0200 Subject: [PATCH 08/20] Misc cleanup. --- opentelemetry-api/opentelemetry/backend.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/opentelemetry-api/opentelemetry/backend.py b/opentelemetry-api/opentelemetry/backend.py index ceaf92e5578..a01e671adde 100644 --- a/opentelemetry-api/opentelemetry/backend.py +++ b/opentelemetry-api/opentelemetry/backend.py @@ -47,13 +47,13 @@ from .trace import Tracer -### Generic private code {{{1 +### Generic private code ### _T = TypeVar('_T') _DEFAULT_BACKEND_MODNAME = 'opentelemetry.sdk.internal.backend_impl' -_tracer = None +_tracer = None #pylint:disable=invalid-name def _get_fallback_impl(api_type: Type[_T]) -> _T: # TODO: Move (most of) this to module docstring. @@ -87,7 +87,7 @@ def _get_fallback_impl(api_type: Type[_T]) -> _T: try: # Note: We use such a long name to avoid calling a function that is not intended for this # API. - backend_fn: Callable[[Type[_T]], object] = getattr(backend_mod, 'get_opentelemetry_backend_impl') + backend_fn = getattr(backend_mod, 'get_opentelemetry_backend_impl') except AttributeError: # TODO Log/warn return api_type() @@ -111,7 +111,7 @@ def _set_backend_object(api_type: Type[_T], impl_object: _T) -> None: raise ValueError('The object does not implement the required base class.') globals()['_' + api_type.__name__.lower()] = impl_object -### Public code (basically copy & paste for each type) {{{1 +### Public code (basically copy & paste for each type) ### def tracer() -> Tracer: """Gets the current global :class:`~opentelemetry.trace.Tracer` object. From 1d5c219232545ceb694db49c938eac294410f721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Thu, 27 Jun 2019 14:01:06 +0200 Subject: [PATCH 09/20] Fix backend.py not being included in wheel. --- opentelemetry-api/setup.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/opentelemetry-api/setup.py b/opentelemetry-api/setup.py index de97ec88c14..73af43f8d6a 100644 --- a/opentelemetry-api/setup.py +++ b/opentelemetry-api/setup.py @@ -18,7 +18,7 @@ base_dir = os.path.dirname(__file__) package_info = {} -with open(os.path.join(base_dir, "src", "opentelemetry", "internal", "version.py")) as f: +with open(os.path.join(base_dir, "opentelemetry", "internal", "version.py")) as f: exec(f.read(), package_info) setuptools.setup( @@ -41,12 +41,10 @@ include_package_data=True, long_description=open("README.rst").read(), install_requires=[ - "typing; python_version<'3.5'", ], extras_require={}, license="Apache-2.0", - package_dir={"": "src"}, - packages=setuptools.find_namespace_packages(where="src"), + packages=setuptools.find_namespace_packages(include=["opentelemetry", "opentelemetry.*"]), url="https://github.com/open-telemetry/opentelemetry-python/tree/master/opentelemetry-api", zip_safe=False, ) From 2804f9fe922b8eb846cb060fd13619652c350d14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Fri, 28 Jun 2019 13:34:56 +0200 Subject: [PATCH 10/20] Rewrite backend/loader. --- opentelemetry-api/opentelemetry/loader.py | 132 ++++++++++++++++++ .../src/opentelemetry/trace/__init__.py | 29 ++++ 2 files changed, 161 insertions(+) create mode 100644 opentelemetry-api/opentelemetry/loader.py diff --git a/opentelemetry-api/opentelemetry/loader.py b/opentelemetry-api/opentelemetry/loader.py new file mode 100644 index 00000000000..033cd3e3abb --- /dev/null +++ b/opentelemetry-api/opentelemetry/loader.py @@ -0,0 +1,132 @@ +# 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. + + +""" +The OpenTelemetry loader module is mainly used internally to load the +implementation for global objects like :func:`opentelemetry.trace.tracer`. + +By default, if you call a getter function (e.g., :func:`tracer`) and the corresponding setter (e.g., +:func:`set_tracer`) wasn't called, you will get a default implementation, which is selected as +follows: + + 1. If the environment variable + :samp:`OPENTELEMETRY_PYTHON_IMPLEMENTATION_{getter-name}` (e.g., + ``OPENTELEMETRY_PYTHON_IMPLEMENTATION_TRACER``) is set to an nonempty + value, an attempt is made to import a module with that name and call a + function ``get_opentelemetry_implementation`` in it. The function + receives the API type that is expected as an argument and should return + an instance of it (e.g., the argument is + :class:`opentelemetry.trace.Tracer` and the function should return an + instance of a :class:`~opentelemetry.trace.Tracer` (probably of a derived + type). + 2. Otherwise, ``OPENTELEMETRY_PYTHON_IMPLEMENTATION_DEFAULT`` is tried instead. + 3. Otherwise, if a :samp:`set_preferred_{}_implementation` was called, + the module set there is used. + 4. Otherwise, if :func:`set_preferred_default_implementation` was called, + the module set there is used. + 5. Otherwise, an attempt is made to import and use the OpenTelemetry SDK. + 6. Otherwise the default implementation that ships with the API distribution + (a fast no-op implementation) is used. + +If any of the above steps fails (e.g., a module is loaded but does not define +the required function or a module name is set but the module fails to load), the +search immediatelly skips to the last step. +""" + +from typing import Type, TypeVar, Optional +import importlib +import os +import sys + +_T = TypeVar('_T') + +_DEFAULT_IMPLEMENTATION_MODNAME = 'opentelemetry.sdk.internal.implementation_impl' + +_tracer = None #pylint:disable=invalid-name + +def _try_load_impl_from_modname( + implementation_modname: str, api_type: Type[_T]) -> Optional[_T]: + try: + implementation_mod = importlib.import_module(implementation_modname) + except (ImportError, SyntaxError): + # TODO Log/warn + return None + + return _try_load_impl_from_mod(True, implementation_mod, api_type) + +_preferred_implementation = dict() + +def _try_load_impl_from_mod( + swallow_errors: bool, implementation_mod: object, api_type: Type[_T]) -> Optional[_T]: + + try: + # Note: We use such a long name to avoid calling a function that is not intended for this + # API. + implementation_fn = getattr(implementation_mod, 'get_opentelemetry_implementation') + except AttributeError: + if not swallow_errors: + raise + + # TODO Log/warn + return None + result = implementation_fn(api_type) + if result is None: + return result + if not isinstance(result, api_type): + if swallow_errors: + # TODO Warn if wrong type is returned + return None + raise TypeError('Expected a {} but got a {}'.format( + api_type.__qualname__, type(result).__qualname__)) + # TODO: Warn if implementation_fn returns api_type(): It should return None to indicate using the + # default. + return result + +def _try_load_configured_impl(api_type: Type[_T]) -> Optional[_T]: + """Attempts to find any specially configured implementation. If none is + configured, or a load error occurs, returns `None` + """ + + implementation_modname = None + if sys.flags.ignore_environment: + return None + implementation_modname = os.getenv( + 'OPENTELEMETRY_PYTHON_IMPLEMENTATION_' + api_type.__name__.upper()) + if implementation_modname: + return _try_load_impl_from_modname(api_type, implementation_modname) + implementation_modname = os.getenv('OPENTELEMETRY_PYTHON_IMPLEMENTATION_DEFAULT') + if implementation_modname: + return _try_load_impl_from_modname(api_type, implementation_modname) + if _preferred_implementation[api_type] is not None: + return _try_load_impl_from_mod(True, _preferred_implementation, api_type) + if _preferred_implementation[None] is not None: + return _try_load_impl_from_mod(True, _preferred_implementation, api_type) + return None + + # Public to other opentelemetry-api modules +def _load_default_impl(api_type: Type[_T]) -> _T: + """Tries to load a configured implementation, if unsuccessful, returns a + fast no-op implemenation that is always available. + """ + + result = _try_load_configured_impl(api_type) + if result is None: + return api_type() + return result + +def set_preferred_default_implementation(implementation_module: object) -> None: + """Sets a module that may be queried for a default implementation. See the + module docs for more details.""" + _preferred_implementation[None] = implementation_module diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 10f4691f6e8..ba18cef4777 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -119,6 +119,7 @@ def __init__(self, state: 'TraceState') -> None: pass +from opentelemetry import _implementation_loader class Tracer: """Handles span creation and in-process context propagation. @@ -249,3 +250,31 @@ class TraceOptions(int): # TODO class TraceState(typing.Dict[str, str]): pass + + +_TRACER = None + +def tracer() -> Tracer: + """Gets the current global :class:`~.Tracer` object. + If there isn't one set yet, a default will be loaded.""" + + global _TRACER #pylint:disable=global-statement + + if _TRACER is None: + _TRACER = _implementation_loader.load_default_impl(Tracer) + +def set_preferred_tracer_implementation(impl_mod: object) -> None: + """Sets a module from which to load the tracer implementation. + + See :mod:`opentelemetry.loader` for details. + + Args: + impl_mod: A module that implements a callback to create the + tracer. + """ + if _TRACER: + raise RuntimeError("Tracer already loaded.") + + #pylint:disable=protected-access + _implementation_loader._preferred_module[Tracer] = impl_mod + #pylint:enable=protected-access From 1d5d1b79ca4983920da1d8f01a3fde29b5c2191b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Fri, 28 Jun 2019 15:39:45 +0200 Subject: [PATCH 11/20] Rewrite backend/loader more, fix pylint, mypy. --- opentelemetry-api/opentelemetry/backend.py | 134 ------------------ opentelemetry-api/opentelemetry/loader.py | 99 +++++++------ .../src/opentelemetry/trace/__init__.py | 22 +-- opentelemetry-api/tests/test_backend.py | 66 --------- opentelemetry-api/tests/test_loader.py | 66 +++++++++ 5 files changed, 136 insertions(+), 251 deletions(-) delete mode 100644 opentelemetry-api/opentelemetry/backend.py delete mode 100644 opentelemetry-api/tests/test_backend.py create mode 100644 opentelemetry-api/tests/test_loader.py diff --git a/opentelemetry-api/opentelemetry/backend.py b/opentelemetry-api/opentelemetry/backend.py deleted file mode 100644 index a01e671adde..00000000000 --- a/opentelemetry-api/opentelemetry/backend.py +++ /dev/null @@ -1,134 +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. - - -""" -The OpenTelemetry backend module provides access to a selected implementation (backend) of the -OpenTelemetry API. - -By default, if you call a getter function (e.g., :func:`tracer`) and the corresponding setter (e.g., -:func:`set_tracer`) wasn't called, you will get a default implementation, which is selected as -follows: - - 1. If the environment variable :samp:`OPENTELEMETRY_PYTHON_BACKEND_{getter-name}` (e.g., - ``OPENTELEMETRY_PYTHON_BACKEND_TRACER``) is set to an nonempty value, an attempt is made to - import a module with that name and call a function ``get_opentelemetry_backend_impl`` in it. - The function receives the API type that is expected as an argument and should return an - instance of it (e.g., the argument is :class:`opentelemetry.trace.Tracer` and the function - should return an instance of a :class:`~opentelemetry.trace.Tracer` (probably of a derived - type). - 2. If that variable is not set, ``OPENTELEMETRY_PYTHON_BACKEND_DEFAULT`` is tried instead. - 3. If that variable was also not set, an attempt is made to import and use the OpenTelemetry - SDK. - 4. Otherwise (if no variable was set and the SDK was not importable, or an error occured when - trying to instantiate the implementation object) the default implementation that ships with - the API distribution (a fast no-op implementation) is used. - -If you called the setter for an object before initializing it that search is not performed and -instead the object you set is used instead. It is also possible to call the setter later to override -the set tracer but this is not recommended. -""" - -from typing import Type, TypeVar -import importlib -import os -import sys - -from .trace import Tracer - -### Generic private code ### - -_T = TypeVar('_T') - -_DEFAULT_BACKEND_MODNAME = 'opentelemetry.sdk.internal.backend_impl' - -_tracer = None #pylint:disable=invalid-name - -def _get_fallback_impl(api_type: Type[_T]) -> _T: - # TODO: Move (most of) this to module docstring. - """Gets the fallback implementation for ``api_type``. - - ``api_type`` must be a OpenTelemetry API type like :class:`Tracer`. - - First, the function tries to find a module that provides a `get_opentelemetry_backend_impl` - function (with the same signature as this function). The following modules are tried: - - 1. ``$OPENTELEMETRY_PYTHON_BACKEND_`` (e.g. ``OPENTELEMETRY_PYTHON_BACKEND_TRACER`) - 2. ``$OPENTELEMETRY_PYTHON_BACKEND_DEFAULT`` (e.g. ``OPENTELEMETRY_PYTHON_BACKEND_TRACER`) - 3. The OpenTelemetry SDK's tracer module. - - Note that if any of the environment variables is set to an nonempty value, further steps - are not tried, even if the modulename set there is invalid or fails to load. The no-op API - implementation is returned instead. - """ - backend_modname = None - if not sys.flags.ignore_environment: - backend_modname = os.getenv('OPENTELEMETRY_PYTHON_BACKEND_' + api_type.__name__.upper()) - if not backend_modname: - backend_modname = os.getenv('OPENTELEMETRY_PYTHON_BACKEND_DEFAULT') - if not backend_modname: - backend_modname = _DEFAULT_BACKEND_MODNAME - try: - backend_mod = importlib.import_module(backend_modname) - except (ImportError, SyntaxError): - # TODO Log/warn - return api_type() - try: - # Note: We use such a long name to avoid calling a function that is not intended for this - # API. - backend_fn = getattr(backend_mod, 'get_opentelemetry_backend_impl') - except AttributeError: - # TODO Log/warn - return api_type() - result = backend_fn(api_type) - if result and isinstance(result, api_type): - # TODO: Warn if backend_fn returns api_type(): It should return None to indicate using the - # default. - return result - return api_type() - -def _selectimpl(api_type: Type[_T]) -> _T: - impl = _get_fallback_impl(api_type) - assert impl - globals()[api_type.__name__.lower()] = impl - return impl - -def _set_backend_object(api_type: Type[_T], impl_object: _T) -> None: - if impl_object is None: - raise ValueError('None is not allowed as a backend implementation.') - if not isinstance(impl_object, api_type): - raise ValueError('The object does not implement the required base class.') - globals()['_' + api_type.__name__.lower()] = impl_object - -### Public code (basically copy & paste for each type) ### - -def tracer() -> Tracer: - """Gets the current global :class:`~opentelemetry.trace.Tracer` object. - - If there isn't one set yet, a default will be used (see module documentation). - """ - - if _tracer: - return _tracer - return _selectimpl(Tracer) - -def set_tracer(tracer_implementation: Tracer) -> None: - """Sets the global :class:`~opentelemetry.trace.Tracer` object. - - Args: - tracer_implementation: The tracer object that should be returned by further calls to - :func:`tracer`. - """ - - _set_backend_object(Tracer, tracer_implementation) diff --git a/opentelemetry-api/opentelemetry/loader.py b/opentelemetry-api/opentelemetry/loader.py index 033cd3e3abb..e9558490b97 100644 --- a/opentelemetry-api/opentelemetry/loader.py +++ b/opentelemetry-api/opentelemetry/loader.py @@ -17,9 +17,9 @@ The OpenTelemetry loader module is mainly used internally to load the implementation for global objects like :func:`opentelemetry.trace.tracer`. -By default, if you call a getter function (e.g., :func:`tracer`) and the corresponding setter (e.g., -:func:`set_tracer`) wasn't called, you will get a default implementation, which is selected as -follows: +By default, if you call a getter function (e.g., :func:`tracer`) and the +corresponding setter (e.g., :func:`set_tracer`) wasn't called, you will get a +default implementation, which is selected as follows: 1. If the environment variable :samp:`OPENTELEMETRY_PYTHON_IMPLEMENTATION_{getter-name}` (e.g., @@ -29,32 +29,38 @@ receives the API type that is expected as an argument and should return an instance of it (e.g., the argument is :class:`opentelemetry.trace.Tracer` and the function should return an - instance of a :class:`~opentelemetry.trace.Tracer` (probably of a derived - type). - 2. Otherwise, ``OPENTELEMETRY_PYTHON_IMPLEMENTATION_DEFAULT`` is tried instead. - 3. Otherwise, if a :samp:`set_preferred_{}_implementation` was called, - the module set there is used. + instance of a :class:`~opentelemetry.trace.Tracer` (probably of a + derived type). + 2. Otherwise, ``OPENTELEMETRY_PYTHON_IMPLEMENTATION_DEFAULT`` is tried + instead. + 3. Otherwise, if a :samp:`set_preferred_{}_implementation` was + called, the module set there is used. 4. Otherwise, if :func:`set_preferred_default_implementation` was called, the module set there is used. 5. Otherwise, an attempt is made to import and use the OpenTelemetry SDK. - 6. Otherwise the default implementation that ships with the API distribution - (a fast no-op implementation) is used. + 6. Otherwise the default implementation that ships with the API + distribution (a fast no-op implementation) is used. If any of the above steps fails (e.g., a module is loaded but does not define -the required function or a module name is set but the module fails to load), the -search immediatelly skips to the last step. +the required function or a module name is set but the module fails to load), +the search immediatelly skips to the last step. """ -from typing import Type, TypeVar, Optional +from typing import Type, TypeVar, Optional, Callable, Dict import importlib import os import sys _T = TypeVar('_T') -_DEFAULT_IMPLEMENTATION_MODNAME = 'opentelemetry.sdk.internal.implementation_impl' +_UntrustedImplFactory = Callable[[Type[_T]], object] + +#ImplementationFactory = Callable[[Type[_T]], _T] -_tracer = None #pylint:disable=invalid-name +_DEFAULT_IMPLEMENTATION_MODNAME = ( + 'opentelemetry.sdk.internal.implementation_impl') + +_CALLBACKS_BY_TYPE: Dict[Optional[type], _UntrustedImplFactory] = dict() def _try_load_impl_from_modname( implementation_modname: str, api_type: Type[_T]) -> Optional[_T]: @@ -64,36 +70,41 @@ def _try_load_impl_from_modname( # TODO Log/warn return None - return _try_load_impl_from_mod(True, implementation_mod, api_type) - -_preferred_implementation = dict() + return _try_load_impl_from_mod(implementation_mod, api_type) def _try_load_impl_from_mod( - swallow_errors: bool, implementation_mod: object, api_type: Type[_T]) -> Optional[_T]: + implementation_mod: object, api_type: Type[_T]) -> Optional[_T]: try: - # Note: We use such a long name to avoid calling a function that is not intended for this - # API. - implementation_fn = getattr(implementation_mod, 'get_opentelemetry_implementation') - except AttributeError: - if not swallow_errors: - raise + # Note: We use such a long name to avoid calling a function that is not + # intended for this API. + implementation_fn = getattr( + implementation_mod, + 'get_opentelemetry_implementation') # type: _UntrustedImplFactory + except AttributeError: # TODO Log/warn return None + + return _try_load_impl_from_callback(implementation_fn, api_type) + +def _try_load_impl_from_callback( + implementation_fn: _UntrustedImplFactory, + api_type: Type[_T] + ) -> Optional[_T]: result = implementation_fn(api_type) if result is None: - return result + return None if not isinstance(result, api_type): - if swallow_errors: - # TODO Warn if wrong type is returned - return None - raise TypeError('Expected a {} but got a {}'.format( - api_type.__qualname__, type(result).__qualname__)) - # TODO: Warn if implementation_fn returns api_type(): It should return None to indicate using the - # default. + # TODO Warn if wrong type is returned + return None + + # TODO: Warn if implementation_fn returns api_type(): It should return None + # to indicate using the default. + return result + def _try_load_configured_impl(api_type: Type[_T]) -> Optional[_T]: """Attempts to find any specially configured implementation. If none is configured, or a load error occurs, returns `None` @@ -105,14 +116,17 @@ def _try_load_configured_impl(api_type: Type[_T]) -> Optional[_T]: implementation_modname = os.getenv( 'OPENTELEMETRY_PYTHON_IMPLEMENTATION_' + api_type.__name__.upper()) if implementation_modname: - return _try_load_impl_from_modname(api_type, implementation_modname) - implementation_modname = os.getenv('OPENTELEMETRY_PYTHON_IMPLEMENTATION_DEFAULT') + return _try_load_impl_from_modname(implementation_modname, api_type) + implementation_modname = os.getenv( + 'OPENTELEMETRY_PYTHON_IMPLEMENTATION_DEFAULT') if implementation_modname: - return _try_load_impl_from_modname(api_type, implementation_modname) - if _preferred_implementation[api_type] is not None: - return _try_load_impl_from_mod(True, _preferred_implementation, api_type) - if _preferred_implementation[None] is not None: - return _try_load_impl_from_mod(True, _preferred_implementation, api_type) + return _try_load_impl_from_modname(implementation_modname, api_type) + callback = _CALLBACKS_BY_TYPE.get(api_type) + if callback is not None: + return _try_load_impl_from_callback(callback, api_type) + callback = _CALLBACKS_BY_TYPE.get(None) + if callback is not None: + return _try_load_impl_from_callback(callback, api_type) return None # Public to other opentelemetry-api modules @@ -126,7 +140,8 @@ def _load_default_impl(api_type: Type[_T]) -> _T: return api_type() return result -def set_preferred_default_implementation(implementation_module: object) -> None: +def set_preferred_default_implementation( + implementation_module: _UntrustedImplFactory) -> None: """Sets a module that may be queried for a default implementation. See the module docs for more details.""" - _preferred_implementation[None] = implementation_module + _CALLBACKS_BY_TYPE[None] = implementation_module diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index ba18cef4777..ea05358cea7 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -64,7 +64,7 @@ from contextlib import contextmanager import typing - +from opentelemetry import loader class Span: """A span represents a single operation within a trace.""" @@ -119,8 +119,6 @@ def __init__(self, state: 'TraceState') -> None: pass -from opentelemetry import _implementation_loader - class Tracer: """Handles span creation and in-process context propagation. @@ -261,20 +259,26 @@ def tracer() -> Tracer: global _TRACER #pylint:disable=global-statement if _TRACER is None: - _TRACER = _implementation_loader.load_default_impl(Tracer) + #pylint:disable=protected-access + _TRACER = loader._load_default_impl(Tracer) -def set_preferred_tracer_implementation(impl_mod: object) -> None: - """Sets a module from which to load the tracer implementation. + return _TRACER + +def set_preferred_tracer_implementation( + factory: typing.Callable[[typing.Type[Tracer]], Tracer]) -> None: + """Sets a callback which to query for the tracer implementation. See :mod:`opentelemetry.loader` for details. + This function may not be called after a tracer is already loaded. + Args: - impl_mod: A module that implements a callback to create the - tracer. + factory: A function that, when called with the :class:`Tracer` type + as an argument, returns an instance of :class:`Tracer`. """ if _TRACER: raise RuntimeError("Tracer already loaded.") #pylint:disable=protected-access - _implementation_loader._preferred_module[Tracer] = impl_mod + loader._CALLBACKS_BY_TYPE[Tracer] = factory #pylint:enable=protected-access diff --git a/opentelemetry-api/tests/test_backend.py b/opentelemetry-api/tests/test_backend.py deleted file mode 100644 index 06499ea6639..00000000000 --- a/opentelemetry-api/tests/test_backend.py +++ /dev/null @@ -1,66 +0,0 @@ -from importlib import reload -import sys - -import pytest - -from opentelemetry import backend -from opentelemetry.trace import Tracer - -class DummyTracer(Tracer): - pass - -DUMMY_TRACER = None - -def get_opentelemetry_backend_impl(type_): - global DUMMY_TRACER #pylint:disable=global-statement - assert type_ is Tracer - DUMMY_TRACER = DummyTracer() - return DUMMY_TRACER - -#pylint:disable=redefined-outer-name,protected-access,unidiomatic-typecheck - -def test_get_default(backend=backend): - backend = reload(backend) - tracer = backend.tracer() - assert type(tracer) is Tracer - -def test_get_set(backend=backend): - backend = reload(backend) - set_tracer = DummyTracer() - backend.set_tracer(set_tracer) - tracer = backend.tracer() - assert tracer is set_tracer - - # Set again - set_tracer = DummyTracer() - backend.set_tracer(set_tracer) - tracer = backend.tracer() - assert tracer is set_tracer - -def test_get_set_import_from(backend=backend): - backend = reload(backend) - get_tracer = backend.tracer # Simulate `import tracer from backend as get_tracer` - set_tracer = DummyTracer() - backend.set_tracer(set_tracer) - tracer = get_tracer() - assert tracer is set_tracer - - # Set again - set_tracer = DummyTracer() - backend.set_tracer(set_tracer) - tracer = get_tracer() - assert tracer is set_tracer - -@pytest.mark.parametrize('envvar_suffix', ['TRACER', 'DEFAULT']) -def test_get_envvar(envvar_suffix, monkeypatch, backend=backend): - global DUMMY_TRACER #pylint:disable=global-statement - - backend = reload(backend) - assert not sys.flags.ignore_environment # Test is not runnable with this! - monkeypatch.setenv('OPENTELEMETRY_PYTHON_BACKEND_' + envvar_suffix, __name__) - try: - tracer = backend.tracer() - assert tracer is DUMMY_TRACER - finally: - DUMMY_TRACER = None - assert type(tracer) is DummyTracer diff --git a/opentelemetry-api/tests/test_loader.py b/opentelemetry-api/tests/test_loader.py new file mode 100644 index 00000000000..a1d386a59b6 --- /dev/null +++ b/opentelemetry-api/tests/test_loader.py @@ -0,0 +1,66 @@ +from importlib import reload +import sys + +import pytest + +from opentelemetry import loader +from opentelemetry import trace + +DUMMY_TRACER = None + +class DummyTracer(trace.Tracer): + pass + +@pytest.fixture(autouse=True) +def reload_mods(): + reload(loader) + reload(trace) + + # Need to reload self, otherwise DummyTracer will have the wrong base class + # after reloading `trace`. + reload(sys.modules[__name__]) + +def get_opentelemetry_implementation(type_): + global DUMMY_TRACER #pylint:disable=global-statement + assert type_ is trace.Tracer + DUMMY_TRACER = DummyTracer() + return DUMMY_TRACER + +#pylint:disable=redefined-outer-name,protected-access,unidiomatic-typecheck + +def test_get_default(): + tracer = trace.tracer() + assert type(tracer) is trace.Tracer + +def test_preferred_impl(): + trace.set_preferred_tracer_implementation(get_opentelemetry_implementation) + tracer = trace.tracer() + assert tracer is DUMMY_TRACER + +@pytest.mark.parametrize('setter', [ + trace.set_preferred_tracer_implementation, + loader.set_preferred_default_implementation]) +def test_preferred_impl_default(setter): + setter(get_opentelemetry_implementation) + tracer = trace.tracer() + assert tracer is DUMMY_TRACER + +def test_try_set_again(): + assert trace.tracer() + # Set again + with pytest.raises(RuntimeError) as excinfo: + trace.set_preferred_tracer_implementation(get_opentelemetry_implementation) + assert "already loaded" in str(excinfo.value) + +@pytest.mark.parametrize('envvar_suffix', ['TRACER', 'DEFAULT']) +def test_get_envvar(envvar_suffix, monkeypatch): + global DUMMY_TRACER #pylint:disable=global-statement + + assert not sys.flags.ignore_environment # Test is not runnable with this! + monkeypatch.setenv('OPENTELEMETRY_PYTHON_IMPLEMENTATION_' + envvar_suffix, __name__) + try: + tracer = trace.tracer() + assert tracer is DUMMY_TRACER + finally: + DUMMY_TRACER = None + assert type(tracer) is DummyTracer From dc08af9dedfcd014edd9fb1e2c2303290a098aeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Fri, 28 Jun 2019 15:44:19 +0200 Subject: [PATCH 12/20] Ditch 'default' in `_load_default_impl`, it's just wrong now. --- opentelemetry-api/opentelemetry/loader.py | 2 +- opentelemetry-api/src/opentelemetry/trace/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/opentelemetry-api/opentelemetry/loader.py b/opentelemetry-api/opentelemetry/loader.py index e9558490b97..c68476c79de 100644 --- a/opentelemetry-api/opentelemetry/loader.py +++ b/opentelemetry-api/opentelemetry/loader.py @@ -130,7 +130,7 @@ def _try_load_configured_impl(api_type: Type[_T]) -> Optional[_T]: return None # Public to other opentelemetry-api modules -def _load_default_impl(api_type: Type[_T]) -> _T: +def _load_impl(api_type: Type[_T]) -> _T: """Tries to load a configured implementation, if unsuccessful, returns a fast no-op implemenation that is always available. """ diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index ea05358cea7..50b44e0c1dc 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -260,7 +260,7 @@ def tracer() -> Tracer: if _TRACER is None: #pylint:disable=protected-access - _TRACER = loader._load_default_impl(Tracer) + _TRACER = loader._load_impl(Tracer) return _TRACER From b98346687bcc89a1ae1b7e40657f12ec0e7f0c0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Fri, 28 Jun 2019 16:01:25 +0200 Subject: [PATCH 13/20] Fix docs. --- docs/index.rst | 2 +- docs/opentelemetry.backend.rst | 4 ---- docs/opentelemetry.loader.rst | 4 ++++ opentelemetry-api/opentelemetry/loader.py | 14 ++++++-------- .../src/opentelemetry/trace/__init__.py | 2 +- opentelemetry-api/tests/__init__.py | 0 6 files changed, 12 insertions(+), 14 deletions(-) delete mode 100644 docs/opentelemetry.backend.rst create mode 100644 docs/opentelemetry.loader.rst create mode 100644 opentelemetry-api/tests/__init__.py diff --git a/docs/index.rst b/docs/index.rst index 159565bfb50..90385dba4a9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,7 +16,7 @@ abstract types for OpenTelemetry implementations. :caption: Contents: opentelemetry.trace - opentelemetry.backend + opentelemetry.loader Indices and tables diff --git a/docs/opentelemetry.backend.rst b/docs/opentelemetry.backend.rst deleted file mode 100644 index e37ce551fd7..00000000000 --- a/docs/opentelemetry.backend.rst +++ /dev/null @@ -1,4 +0,0 @@ -opentelemetry.backend module -============================ - -.. automodule:: opentelemetry.backend diff --git a/docs/opentelemetry.loader.rst b/docs/opentelemetry.loader.rst new file mode 100644 index 00000000000..bd6dd698f8b --- /dev/null +++ b/docs/opentelemetry.loader.rst @@ -0,0 +1,4 @@ +opentelemetry.loader module +=========================== + +.. automodule:: opentelemetry.loader diff --git a/opentelemetry-api/opentelemetry/loader.py b/opentelemetry-api/opentelemetry/loader.py index c68476c79de..852b79f825b 100644 --- a/opentelemetry-api/opentelemetry/loader.py +++ b/opentelemetry-api/opentelemetry/loader.py @@ -17,9 +17,7 @@ The OpenTelemetry loader module is mainly used internally to load the implementation for global objects like :func:`opentelemetry.trace.tracer`. -By default, if you call a getter function (e.g., :func:`tracer`) and the -corresponding setter (e.g., :func:`set_tracer`) wasn't called, you will get a -default implementation, which is selected as follows: +When loading an implementation, the following algorithm is used: 1. If the environment variable :samp:`OPENTELEMETRY_PYTHON_IMPLEMENTATION_{getter-name}` (e.g., @@ -27,16 +25,16 @@ value, an attempt is made to import a module with that name and call a function ``get_opentelemetry_implementation`` in it. The function receives the API type that is expected as an argument and should return - an instance of it (e.g., the argument is + an instance of it or ``None`` (e.g., the argument is :class:`opentelemetry.trace.Tracer` and the function should return an instance of a :class:`~opentelemetry.trace.Tracer` (probably of a derived type). 2. Otherwise, ``OPENTELEMETRY_PYTHON_IMPLEMENTATION_DEFAULT`` is tried instead. 3. Otherwise, if a :samp:`set_preferred_{}_implementation` was - called, the module set there is used. + called, the callback set there is used. 4. Otherwise, if :func:`set_preferred_default_implementation` was called, - the module set there is used. + the callback set there is used. 5. Otherwise, an attempt is made to import and use the OpenTelemetry SDK. 6. Otherwise the default implementation that ships with the API distribution (a fast no-op implementation) is used. @@ -142,6 +140,6 @@ def _load_impl(api_type: Type[_T]) -> _T: def set_preferred_default_implementation( implementation_module: _UntrustedImplFactory) -> None: - """Sets a module that may be queried for a default implementation. See the - module docs for more details.""" + """Sets a callback that may be queried for any implementation object. See + the module docs for more details.""" _CALLBACKS_BY_TYPE[None] = implementation_module diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 50b44e0c1dc..ef48be4e859 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -274,7 +274,7 @@ def set_preferred_tracer_implementation( Args: factory: A function that, when called with the :class:`Tracer` type - as an argument, returns an instance of :class:`Tracer`. + as an argument, returns an instance of :class:`Tracer`. """ if _TRACER: raise RuntimeError("Tracer already loaded.") diff --git a/opentelemetry-api/tests/__init__.py b/opentelemetry-api/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d From e0f6ca0f1d651e39e957b68f541d71151b9e86e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Mon, 8 Jul 2019 13:18:58 +0200 Subject: [PATCH 14/20] Apply new package structure. --- opentelemetry-api/{ => src}/opentelemetry/loader.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename opentelemetry-api/{ => src}/opentelemetry/loader.py (100%) diff --git a/opentelemetry-api/opentelemetry/loader.py b/opentelemetry-api/src/opentelemetry/loader.py similarity index 100% rename from opentelemetry-api/opentelemetry/loader.py rename to opentelemetry-api/src/opentelemetry/loader.py From 8ebf94a155d8d6cc4f8fc37940fb2f8418a28019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Mon, 8 Jul 2019 15:00:45 +0200 Subject: [PATCH 15/20] Remove unit tests (for now). --- opentelemetry-api/tests/__init__.py | 0 opentelemetry-api/tests/test_loader.py | 66 -------------------------- 2 files changed, 66 deletions(-) delete mode 100644 opentelemetry-api/tests/__init__.py delete mode 100644 opentelemetry-api/tests/test_loader.py diff --git a/opentelemetry-api/tests/__init__.py b/opentelemetry-api/tests/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/opentelemetry-api/tests/test_loader.py b/opentelemetry-api/tests/test_loader.py deleted file mode 100644 index a1d386a59b6..00000000000 --- a/opentelemetry-api/tests/test_loader.py +++ /dev/null @@ -1,66 +0,0 @@ -from importlib import reload -import sys - -import pytest - -from opentelemetry import loader -from opentelemetry import trace - -DUMMY_TRACER = None - -class DummyTracer(trace.Tracer): - pass - -@pytest.fixture(autouse=True) -def reload_mods(): - reload(loader) - reload(trace) - - # Need to reload self, otherwise DummyTracer will have the wrong base class - # after reloading `trace`. - reload(sys.modules[__name__]) - -def get_opentelemetry_implementation(type_): - global DUMMY_TRACER #pylint:disable=global-statement - assert type_ is trace.Tracer - DUMMY_TRACER = DummyTracer() - return DUMMY_TRACER - -#pylint:disable=redefined-outer-name,protected-access,unidiomatic-typecheck - -def test_get_default(): - tracer = trace.tracer() - assert type(tracer) is trace.Tracer - -def test_preferred_impl(): - trace.set_preferred_tracer_implementation(get_opentelemetry_implementation) - tracer = trace.tracer() - assert tracer is DUMMY_TRACER - -@pytest.mark.parametrize('setter', [ - trace.set_preferred_tracer_implementation, - loader.set_preferred_default_implementation]) -def test_preferred_impl_default(setter): - setter(get_opentelemetry_implementation) - tracer = trace.tracer() - assert tracer is DUMMY_TRACER - -def test_try_set_again(): - assert trace.tracer() - # Set again - with pytest.raises(RuntimeError) as excinfo: - trace.set_preferred_tracer_implementation(get_opentelemetry_implementation) - assert "already loaded" in str(excinfo.value) - -@pytest.mark.parametrize('envvar_suffix', ['TRACER', 'DEFAULT']) -def test_get_envvar(envvar_suffix, monkeypatch): - global DUMMY_TRACER #pylint:disable=global-statement - - assert not sys.flags.ignore_environment # Test is not runnable with this! - monkeypatch.setenv('OPENTELEMETRY_PYTHON_IMPLEMENTATION_' + envvar_suffix, __name__) - try: - tracer = trace.tracer() - assert tracer is DUMMY_TRACER - finally: - DUMMY_TRACER = None - assert type(tracer) is DummyTracer From c5dbd038a493fedbfe07a179e56c44849ca07833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Mon, 8 Jul 2019 15:27:13 +0200 Subject: [PATCH 16/20] Document the factory type aliases. --- opentelemetry-api/src/opentelemetry/loader.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/opentelemetry-api/src/opentelemetry/loader.py b/opentelemetry-api/src/opentelemetry/loader.py index 852b79f825b..58503cc12b7 100644 --- a/opentelemetry-api/src/opentelemetry/loader.py +++ b/opentelemetry-api/src/opentelemetry/loader.py @@ -51,8 +51,16 @@ _T = TypeVar('_T') +# "Untrusted" because this is usually user-provided and we don't trust the user +# to really return a _T: by using object, mypy forces us to check/cast +# explicitly. _UntrustedImplFactory = Callable[[Type[_T]], object] + +# This would be the normal ImplementationFactory which would be used to +# annotate setters, were it not for https://github.com/python/mypy/issues/7092 +# Once that bug is resolved, setters should use this instead of duplicating the +# code. #ImplementationFactory = Callable[[Type[_T]], _T] _DEFAULT_IMPLEMENTATION_MODNAME = ( From e4a1dd3a89b2cad073fe7d65f19f990c4498e20c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Mon, 8 Jul 2019 15:28:17 +0200 Subject: [PATCH 17/20] Store factory functions in respective modules. Gets rid of the dictionary in loader.py. --- opentelemetry-api/src/opentelemetry/loader.py | 29 ++++++++++--------- .../src/opentelemetry/trace/__init__.py | 16 ++++++---- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/loader.py b/opentelemetry-api/src/opentelemetry/loader.py index 58503cc12b7..0be911fd276 100644 --- a/opentelemetry-api/src/opentelemetry/loader.py +++ b/opentelemetry-api/src/opentelemetry/loader.py @@ -44,7 +44,7 @@ the search immediatelly skips to the last step. """ -from typing import Type, TypeVar, Optional, Callable, Dict +from typing import Type, TypeVar, Optional, Callable import importlib import os import sys @@ -66,7 +66,7 @@ _DEFAULT_IMPLEMENTATION_MODNAME = ( 'opentelemetry.sdk.internal.implementation_impl') -_CALLBACKS_BY_TYPE: Dict[Optional[type], _UntrustedImplFactory] = dict() +_DEFAULT_FACTORY: Optional[_UntrustedImplFactory] = None def _try_load_impl_from_modname( implementation_modname: str, api_type: Type[_T]) -> Optional[_T]: @@ -111,11 +111,12 @@ def _try_load_impl_from_callback( return result -def _try_load_configured_impl(api_type: Type[_T]) -> Optional[_T]: +def _try_load_configured_impl( + api_type: Type[_T], factory: Optional[_UntrustedImplFactory[_T]] + ) -> Optional[_T]: """Attempts to find any specially configured implementation. If none is configured, or a load error occurs, returns `None` """ - implementation_modname = None if sys.flags.ignore_environment: return None @@ -127,21 +128,20 @@ def _try_load_configured_impl(api_type: Type[_T]) -> Optional[_T]: 'OPENTELEMETRY_PYTHON_IMPLEMENTATION_DEFAULT') if implementation_modname: return _try_load_impl_from_modname(implementation_modname, api_type) - callback = _CALLBACKS_BY_TYPE.get(api_type) - if callback is not None: - return _try_load_impl_from_callback(callback, api_type) - callback = _CALLBACKS_BY_TYPE.get(None) - if callback is not None: - return _try_load_impl_from_callback(callback, api_type) + if factory is not None: + return _try_load_impl_from_callback(factory, api_type) + if _DEFAULT_FACTORY is not None: + return _try_load_impl_from_callback(_DEFAULT_FACTORY, api_type) return None - # Public to other opentelemetry-api modules -def _load_impl(api_type: Type[_T]) -> _T: +# Public to other opentelemetry-api modules +def _load_impl( + api_type: Type[_T], factory: Optional[Callable[[Type[_T]], _T]]) -> _T: """Tries to load a configured implementation, if unsuccessful, returns a fast no-op implemenation that is always available. """ - result = _try_load_configured_impl(api_type) + result = _try_load_configured_impl(api_type, factory) if result is None: return api_type() return result @@ -150,4 +150,5 @@ def set_preferred_default_implementation( implementation_module: _UntrustedImplFactory) -> None: """Sets a callback that may be queried for any implementation object. See the module docs for more details.""" - _CALLBACKS_BY_TYPE[None] = implementation_module + global _DEFAULT_FACTORY #pylint:disable=global-statement + _DEFAULT_FACTORY = implementation_module diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index ef48be4e859..0a2a3822366 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -250,17 +250,20 @@ class TraceState(typing.Dict[str, str]): pass -_TRACER = None +_TRACER: typing.Optional[Tracer] = None +_TRACER_FACTORY: \ + typing.Optional[typing.Callable[[typing.Type[Tracer]], Tracer]] = None def tracer() -> Tracer: """Gets the current global :class:`~.Tracer` object. If there isn't one set yet, a default will be loaded.""" - global _TRACER #pylint:disable=global-statement + global _TRACER, _TRACER_FACTORY #pylint:disable=global-statement if _TRACER is None: #pylint:disable=protected-access - _TRACER = loader._load_impl(Tracer) + _TRACER = loader._load_impl(Tracer, _TRACER_FACTORY) + del _TRACER_FACTORY return _TRACER @@ -276,9 +279,10 @@ def set_preferred_tracer_implementation( factory: A function that, when called with the :class:`Tracer` type as an argument, returns an instance of :class:`Tracer`. """ + + global _TRACER_FACTORY #pylint:disable=global-statement + if _TRACER: raise RuntimeError("Tracer already loaded.") - #pylint:disable=protected-access - loader._CALLBACKS_BY_TYPE[Tracer] = factory - #pylint:enable=protected-access + _TRACER_FACTORY = factory From 3f9a6cd0ae815799458bb6297d449b46a369e5c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Mon, 8 Jul 2019 16:21:20 +0200 Subject: [PATCH 18/20] Fix factory return type (Optional) & improve docs. --- opentelemetry-api/src/opentelemetry/loader.py | 55 +++++++++++++------ .../src/opentelemetry/trace/__init__.py | 14 +++-- 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/loader.py b/opentelemetry-api/src/opentelemetry/loader.py index 0be911fd276..e1a37f8c881 100644 --- a/opentelemetry-api/src/opentelemetry/loader.py +++ b/opentelemetry-api/src/opentelemetry/loader.py @@ -17,22 +17,35 @@ The OpenTelemetry loader module is mainly used internally to load the implementation for global objects like :func:`opentelemetry.trace.tracer`. -When loading an implementation, the following algorithm is used: +.. _loader-factory: + +An instance of a global object of type ``T`` is always created with a factory +function with the following signature:: + + def my_factory_for_t(api_type: typing.Type[T]) -> typing.Optional[T]: + # ... + +That function is called with e.g., the type of the global object it should +create as an argument (e.g. the type object +:class:`opentelemetry.trace.Tracer`) and should return an instance of that type +(such that ``instanceof(my_factory_for_t(T), T)`` is true). Alternatively, it +may return ``None`` to indicate that the no-op default should be used. + +When loading an implementation, the following algorithm is used to find a +factory function or other means to create the global object: 1. If the environment variable :samp:`OPENTELEMETRY_PYTHON_IMPLEMENTATION_{getter-name}` (e.g., ``OPENTELEMETRY_PYTHON_IMPLEMENTATION_TRACER``) is set to an nonempty - value, an attempt is made to import a module with that name and call a - function ``get_opentelemetry_implementation`` in it. The function - receives the API type that is expected as an argument and should return - an instance of it or ``None`` (e.g., the argument is - :class:`opentelemetry.trace.Tracer` and the function should return an - instance of a :class:`~opentelemetry.trace.Tracer` (probably of a - derived type). - 2. Otherwise, ``OPENTELEMETRY_PYTHON_IMPLEMENTATION_DEFAULT`` is tried - instead. + value, an attempt is made to import a module with that name and use a + factory function named ``get_opentelemetry_implementation`` in it. + 2. Otherwise, the same is tried with the environment + variable ``OPENTELEMETRY_PYTHON_IMPLEMENTATION_DEFAULT``. 3. Otherwise, if a :samp:`set_preferred_{}_implementation` was - called, the callback set there is used. + called (e.g. + :func:`opentelemetry.trace.set_preferred_tracer_implementation`), the + callback set there is used (that is, the environment variables override + the callback set in code). 4. Otherwise, if :func:`set_preferred_default_implementation` was called, the callback set there is used. 5. Otherwise, an attempt is made to import and use the OpenTelemetry SDK. @@ -42,6 +55,10 @@ If any of the above steps fails (e.g., a module is loaded but does not define the required function or a module name is set but the module fails to load), the search immediatelly skips to the last step. + +Note that the first two steps (those that query environment variables) are +skipped if :data:`sys.flags` has ``ignore_environment`` set (which usually +means that the Python interpreter was invoked with the ``-E`` or ``-I`` flag). """ from typing import Type, TypeVar, Optional, Callable @@ -54,14 +71,14 @@ # "Untrusted" because this is usually user-provided and we don't trust the user # to really return a _T: by using object, mypy forces us to check/cast # explicitly. -_UntrustedImplFactory = Callable[[Type[_T]], object] +_UntrustedImplFactory = Callable[[Type[_T]], Optional[object]] # This would be the normal ImplementationFactory which would be used to # annotate setters, were it not for https://github.com/python/mypy/issues/7092 # Once that bug is resolved, setters should use this instead of duplicating the # code. -#ImplementationFactory = Callable[[Type[_T]], _T] +#ImplementationFactory = Callable[[Type[_T]], Optional[_T]] _DEFAULT_IMPLEMENTATION_MODNAME = ( 'opentelemetry.sdk.internal.implementation_impl') @@ -136,7 +153,9 @@ def _try_load_configured_impl( # Public to other opentelemetry-api modules def _load_impl( - api_type: Type[_T], factory: Optional[Callable[[Type[_T]], _T]]) -> _T: + api_type: Type[_T], + factory: Optional[Callable[[Type[_T]], Optional[_T]]] + ) -> _T: """Tries to load a configured implementation, if unsuccessful, returns a fast no-op implemenation that is always available. """ @@ -147,8 +166,8 @@ def _load_impl( return result def set_preferred_default_implementation( - implementation_module: _UntrustedImplFactory) -> None: - """Sets a callback that may be queried for any implementation object. See - the module docs for more details.""" + implementation_factory: _UntrustedImplFactory) -> None: + """Sets a factory function that may be called for any implementation + object. See the :ref:`module docs ` for more details.""" global _DEFAULT_FACTORY #pylint:disable=global-statement - _DEFAULT_FACTORY = implementation_module + _DEFAULT_FACTORY = implementation_factory diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 0a2a3822366..af3948cbf56 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -251,8 +251,8 @@ class TraceState(typing.Dict[str, str]): _TRACER: typing.Optional[Tracer] = None -_TRACER_FACTORY: \ - typing.Optional[typing.Callable[[typing.Type[Tracer]], Tracer]] = None +_TRACER_FACTORY: typing.Optional[ + typing.Callable[[typing.Type[Tracer]], typing.Optional[Tracer]]] = None def tracer() -> Tracer: """Gets the current global :class:`~.Tracer` object. @@ -268,16 +268,18 @@ def tracer() -> Tracer: return _TRACER def set_preferred_tracer_implementation( - factory: typing.Callable[[typing.Type[Tracer]], Tracer]) -> None: - """Sets a callback which to query for the tracer implementation. + factory: typing.Callable[ + [typing.Type[Tracer]], typing.Optional[Tracer]] + ) -> None: + """Sets a factory function which may be used to create the tracer + implementation. See :mod:`opentelemetry.loader` for details. This function may not be called after a tracer is already loaded. Args: - factory: A function that, when called with the :class:`Tracer` type - as an argument, returns an instance of :class:`Tracer`. + factory: Callback that should create a new :class:`Tracer` instance. """ global _TRACER_FACTORY #pylint:disable=global-statement From a8c1da1cafa4c8f08c89511c0e9b6d8abfd1dddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Mon, 8 Jul 2019 16:31:10 +0200 Subject: [PATCH 19/20] Revert accidental changes to setup.py. --- opentelemetry-api/setup.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/opentelemetry-api/setup.py b/opentelemetry-api/setup.py index 73af43f8d6a..de97ec88c14 100644 --- a/opentelemetry-api/setup.py +++ b/opentelemetry-api/setup.py @@ -18,7 +18,7 @@ base_dir = os.path.dirname(__file__) package_info = {} -with open(os.path.join(base_dir, "opentelemetry", "internal", "version.py")) as f: +with open(os.path.join(base_dir, "src", "opentelemetry", "internal", "version.py")) as f: exec(f.read(), package_info) setuptools.setup( @@ -41,10 +41,12 @@ include_package_data=True, long_description=open("README.rst").read(), install_requires=[ + "typing; python_version<'3.5'", ], extras_require={}, license="Apache-2.0", - packages=setuptools.find_namespace_packages(include=["opentelemetry", "opentelemetry.*"]), + package_dir={"": "src"}, + packages=setuptools.find_namespace_packages(where="src"), url="https://github.com/open-telemetry/opentelemetry-python/tree/master/opentelemetry-api", zip_safe=False, ) From 0fb5d52d51956628d5d0b4f69083d41a0ae512a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Wed, 10 Jul 2019 11:07:24 +0200 Subject: [PATCH 20/20] Remove unused global. --- opentelemetry-api/src/opentelemetry/loader.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/loader.py b/opentelemetry-api/src/opentelemetry/loader.py index e1a37f8c881..dc3a4e078e9 100644 --- a/opentelemetry-api/src/opentelemetry/loader.py +++ b/opentelemetry-api/src/opentelemetry/loader.py @@ -80,9 +80,6 @@ def my_factory_for_t(api_type: typing.Type[T]) -> typing.Optional[T]: # code. #ImplementationFactory = Callable[[Type[_T]], Optional[_T]] -_DEFAULT_IMPLEMENTATION_MODNAME = ( - 'opentelemetry.sdk.internal.implementation_impl') - _DEFAULT_FACTORY: Optional[_UntrustedImplFactory] = None def _try_load_impl_from_modname(