Skip to content

Commit 9f0527d

Browse files
authored
Add "global tracer"/backend objects (#15) (#29)
* Global tracer registry: First somewhat satisfying iteration. * Add API docs for backend module * Fix documentation (and overlong lines within). * Sort imports. * Remove _UNIT_TEST_IGNORE_ENV. We should be able to ensure clean envvars for the test run. * Misc cleanup. * Fix backend.py not being included in wheel. * Rewrite backend/loader. * Rewrite backend/loader more, fix pylint, mypy. * Ditch 'default' in `_load_default_impl`, it's just wrong now. * Fix docs. * Apply new package structure. * Remove unit tests (for now). * Document the factory type aliases. * Store factory functions in respective modules. Gets rid of the dictionary in loader.py. * Fix factory return type (Optional) & improve docs. * Revert accidental changes to setup.py. * Remove unused global.
1 parent d78ea98 commit 9f0527d

File tree

4 files changed

+216
-2
lines changed

4 files changed

+216
-2
lines changed

docs/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ abstract types for OpenTelemetry implementations.
1616
:caption: Contents:
1717

1818
opentelemetry.trace
19+
opentelemetry.loader
1920

2021

2122
Indices and tables

docs/opentelemetry.loader.rst

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
opentelemetry.loader module
2+
===========================
3+
4+
.. automodule:: opentelemetry.loader
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# Copyright 2019, OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
"""
17+
The OpenTelemetry loader module is mainly used internally to load the
18+
implementation for global objects like :func:`opentelemetry.trace.tracer`.
19+
20+
.. _loader-factory:
21+
22+
An instance of a global object of type ``T`` is always created with a factory
23+
function with the following signature::
24+
25+
def my_factory_for_t(api_type: typing.Type[T]) -> typing.Optional[T]:
26+
# ...
27+
28+
That function is called with e.g., the type of the global object it should
29+
create as an argument (e.g. the type object
30+
:class:`opentelemetry.trace.Tracer`) and should return an instance of that type
31+
(such that ``instanceof(my_factory_for_t(T), T)`` is true). Alternatively, it
32+
may return ``None`` to indicate that the no-op default should be used.
33+
34+
When loading an implementation, the following algorithm is used to find a
35+
factory function or other means to create the global object:
36+
37+
1. If the environment variable
38+
:samp:`OPENTELEMETRY_PYTHON_IMPLEMENTATION_{getter-name}` (e.g.,
39+
``OPENTELEMETRY_PYTHON_IMPLEMENTATION_TRACER``) is set to an nonempty
40+
value, an attempt is made to import a module with that name and use a
41+
factory function named ``get_opentelemetry_implementation`` in it.
42+
2. Otherwise, the same is tried with the environment
43+
variable ``OPENTELEMETRY_PYTHON_IMPLEMENTATION_DEFAULT``.
44+
3. Otherwise, if a :samp:`set_preferred_{<type>}_implementation` was
45+
called (e.g.
46+
:func:`opentelemetry.trace.set_preferred_tracer_implementation`), the
47+
callback set there is used (that is, the environment variables override
48+
the callback set in code).
49+
4. Otherwise, if :func:`set_preferred_default_implementation` was called,
50+
the callback set there is used.
51+
5. Otherwise, an attempt is made to import and use the OpenTelemetry SDK.
52+
6. Otherwise the default implementation that ships with the API
53+
distribution (a fast no-op implementation) is used.
54+
55+
If any of the above steps fails (e.g., a module is loaded but does not define
56+
the required function or a module name is set but the module fails to load),
57+
the search immediatelly skips to the last step.
58+
59+
Note that the first two steps (those that query environment variables) are
60+
skipped if :data:`sys.flags` has ``ignore_environment`` set (which usually
61+
means that the Python interpreter was invoked with the ``-E`` or ``-I`` flag).
62+
"""
63+
64+
from typing import Type, TypeVar, Optional, Callable
65+
import importlib
66+
import os
67+
import sys
68+
69+
_T = TypeVar('_T')
70+
71+
# "Untrusted" because this is usually user-provided and we don't trust the user
72+
# to really return a _T: by using object, mypy forces us to check/cast
73+
# explicitly.
74+
_UntrustedImplFactory = Callable[[Type[_T]], Optional[object]]
75+
76+
77+
# This would be the normal ImplementationFactory which would be used to
78+
# annotate setters, were it not for https://github.com/python/mypy/issues/7092
79+
# Once that bug is resolved, setters should use this instead of duplicating the
80+
# code.
81+
#ImplementationFactory = Callable[[Type[_T]], Optional[_T]]
82+
83+
_DEFAULT_FACTORY: Optional[_UntrustedImplFactory] = None
84+
85+
def _try_load_impl_from_modname(
86+
implementation_modname: str, api_type: Type[_T]) -> Optional[_T]:
87+
try:
88+
implementation_mod = importlib.import_module(implementation_modname)
89+
except (ImportError, SyntaxError):
90+
# TODO Log/warn
91+
return None
92+
93+
return _try_load_impl_from_mod(implementation_mod, api_type)
94+
95+
def _try_load_impl_from_mod(
96+
implementation_mod: object, api_type: Type[_T]) -> Optional[_T]:
97+
98+
try:
99+
# Note: We use such a long name to avoid calling a function that is not
100+
# intended for this API.
101+
102+
implementation_fn = getattr(
103+
implementation_mod,
104+
'get_opentelemetry_implementation') # type: _UntrustedImplFactory
105+
except AttributeError:
106+
# TODO Log/warn
107+
return None
108+
109+
return _try_load_impl_from_callback(implementation_fn, api_type)
110+
111+
def _try_load_impl_from_callback(
112+
implementation_fn: _UntrustedImplFactory,
113+
api_type: Type[_T]
114+
) -> Optional[_T]:
115+
result = implementation_fn(api_type)
116+
if result is None:
117+
return None
118+
if not isinstance(result, api_type):
119+
# TODO Warn if wrong type is returned
120+
return None
121+
122+
# TODO: Warn if implementation_fn returns api_type(): It should return None
123+
# to indicate using the default.
124+
125+
return result
126+
127+
128+
def _try_load_configured_impl(
129+
api_type: Type[_T], factory: Optional[_UntrustedImplFactory[_T]]
130+
) -> Optional[_T]:
131+
"""Attempts to find any specially configured implementation. If none is
132+
configured, or a load error occurs, returns `None`
133+
"""
134+
implementation_modname = None
135+
if sys.flags.ignore_environment:
136+
return None
137+
implementation_modname = os.getenv(
138+
'OPENTELEMETRY_PYTHON_IMPLEMENTATION_' + api_type.__name__.upper())
139+
if implementation_modname:
140+
return _try_load_impl_from_modname(implementation_modname, api_type)
141+
implementation_modname = os.getenv(
142+
'OPENTELEMETRY_PYTHON_IMPLEMENTATION_DEFAULT')
143+
if implementation_modname:
144+
return _try_load_impl_from_modname(implementation_modname, api_type)
145+
if factory is not None:
146+
return _try_load_impl_from_callback(factory, api_type)
147+
if _DEFAULT_FACTORY is not None:
148+
return _try_load_impl_from_callback(_DEFAULT_FACTORY, api_type)
149+
return None
150+
151+
# Public to other opentelemetry-api modules
152+
def _load_impl(
153+
api_type: Type[_T],
154+
factory: Optional[Callable[[Type[_T]], Optional[_T]]]
155+
) -> _T:
156+
"""Tries to load a configured implementation, if unsuccessful, returns a
157+
fast no-op implemenation that is always available.
158+
"""
159+
160+
result = _try_load_configured_impl(api_type, factory)
161+
if result is None:
162+
return api_type()
163+
return result
164+
165+
def set_preferred_default_implementation(
166+
implementation_factory: _UntrustedImplFactory) -> None:
167+
"""Sets a factory function that may be called for any implementation
168+
object. See the :ref:`module docs <loader-factory>` for more details."""
169+
global _DEFAULT_FACTORY #pylint:disable=global-statement
170+
_DEFAULT_FACTORY = implementation_factory

opentelemetry-api/src/opentelemetry/trace/__init__.py

+41-2
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464

6565
from contextlib import contextmanager
6666
import typing
67-
67+
from opentelemetry import loader
6868

6969
class Span:
7070
"""A span represents a single operation within a trace."""
@@ -119,7 +119,6 @@ def __init__(self,
119119
state: 'TraceState') -> None:
120120
pass
121121

122-
123122
class Tracer:
124123
"""Handles span creation and in-process context propagation.
125124
@@ -248,3 +247,43 @@ class TraceOptions(int):
248247
# TODO
249248
class TraceState(typing.Dict[str, str]):
250249
pass
250+
251+
252+
_TRACER: typing.Optional[Tracer] = None
253+
_TRACER_FACTORY: typing.Optional[
254+
typing.Callable[[typing.Type[Tracer]], typing.Optional[Tracer]]] = None
255+
256+
def tracer() -> Tracer:
257+
"""Gets the current global :class:`~.Tracer` object.
258+
If there isn't one set yet, a default will be loaded."""
259+
260+
global _TRACER, _TRACER_FACTORY #pylint:disable=global-statement
261+
262+
if _TRACER is None:
263+
#pylint:disable=protected-access
264+
_TRACER = loader._load_impl(Tracer, _TRACER_FACTORY)
265+
del _TRACER_FACTORY
266+
267+
return _TRACER
268+
269+
def set_preferred_tracer_implementation(
270+
factory: typing.Callable[
271+
[typing.Type[Tracer]], typing.Optional[Tracer]]
272+
) -> None:
273+
"""Sets a factory function which may be used to create the tracer
274+
implementation.
275+
276+
See :mod:`opentelemetry.loader` for details.
277+
278+
This function may not be called after a tracer is already loaded.
279+
280+
Args:
281+
factory: Callback that should create a new :class:`Tracer` instance.
282+
"""
283+
284+
global _TRACER_FACTORY #pylint:disable=global-statement
285+
286+
if _TRACER:
287+
raise RuntimeError("Tracer already loaded.")
288+
289+
_TRACER_FACTORY = factory

0 commit comments

Comments
 (0)