Skip to content

Commit 72862c9

Browse files
codebotenocelotlmauriciovasquezbernal
authored
Adding Context API (open-telemetry#395)
This change implements the Context API portion of OTEP open-telemetry#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. Co-authored-by: Diego Hurtado <ocelotl@users.noreply.github.com> Co-authored-by: Mauricio Vásquez <mauricio@kinvolk.io>
1 parent c50aab8 commit 72862c9

File tree

25 files changed

+879
-407
lines changed

25 files changed

+879
-407
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
opentelemetry.context.base\_context module
22
==========================================
33

4-
.. automodule:: opentelemetry.context.base_context
4+
.. automodule:: opentelemetry.context.context
55
:members:
66
:undoc-members:
77
:show-inheritance:

docs/opentelemetry.context.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Submodules
66

77
.. toctree::
88

9-
opentelemetry.context.base_context
9+
opentelemetry.context.context
1010

1111
Module contents
1212
---------------

ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@
2222

2323
from requests.sessions import Session
2424

25-
from opentelemetry import propagators
26-
from opentelemetry.context import Context
25+
from opentelemetry import context, propagators
2726
from opentelemetry.ext.http_requests.version import __version__
2827
from opentelemetry.trace import SpanKind
2928

@@ -54,7 +53,7 @@ def enable(tracer_source):
5453

5554
@functools.wraps(wrapped)
5655
def instrumented_request(self, method, url, *args, **kwargs):
57-
if Context.suppress_instrumentation:
56+
if context.get_value("suppress_instrumentation"):
5857
return wrapped(self, method, url, *args, **kwargs)
5958

6059
# See

opentelemetry-api/setup.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,11 @@
5656
"/tree/master/opentelemetry-api"
5757
),
5858
zip_safe=False,
59+
entry_points={
60+
"opentelemetry_context": [
61+
"default_context = "
62+
"opentelemetry.context.default_context:"
63+
"DefaultRuntimeContext",
64+
]
65+
},
5966
)

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

Lines changed: 116 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -12,141 +12,119 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
16-
"""
17-
The OpenTelemetry context module provides abstraction layer on top of
18-
thread-local storage and contextvars. The long term direction is to switch to
19-
contextvars provided by the Python runtime library.
20-
21-
A global object ``Context`` is provided to access all the context related
22-
functionalities::
23-
24-
>>> from opentelemetry.context import Context
25-
>>> Context.foo = 1
26-
>>> Context.foo = 2
27-
>>> Context.foo
28-
2
29-
30-
When explicit thread is used, a helper function
31-
``Context.with_current_context`` can be used to carry the context across
32-
threads::
33-
34-
from threading import Thread
35-
from opentelemetry.context import Context
36-
37-
def work(name):
38-
print('Entering worker:', Context)
39-
Context.operation_id = name
40-
print('Exiting worker:', Context)
41-
42-
if __name__ == '__main__':
43-
print('Main thread:', Context)
44-
Context.operation_id = 'main'
45-
46-
print('Main thread:', Context)
47-
48-
# by default context is not propagated to worker thread
49-
thread = Thread(target=work, args=('foo',))
50-
thread.start()
51-
thread.join()
52-
53-
print('Main thread:', Context)
54-
55-
# user can propagate context explicitly
56-
thread = Thread(
57-
target=Context.with_current_context(work),
58-
args=('bar',),
59-
)
60-
thread.start()
61-
thread.join()
62-
63-
print('Main thread:', Context)
64-
65-
Here goes another example using thread pool::
66-
67-
import time
68-
import threading
69-
70-
from multiprocessing.dummy import Pool as ThreadPool
71-
from opentelemetry.context import Context
72-
73-
_console_lock = threading.Lock()
74-
75-
def println(msg):
76-
with _console_lock:
77-
print(msg)
78-
79-
def work(name):
80-
println('Entering worker[{}]: {}'.format(name, Context))
81-
Context.operation_id = name
82-
time.sleep(0.01)
83-
println('Exiting worker[{}]: {}'.format(name, Context))
84-
85-
if __name__ == "__main__":
86-
println('Main thread: {}'.format(Context))
87-
Context.operation_id = 'main'
88-
pool = ThreadPool(2) # create a thread pool with 2 threads
89-
pool.map(Context.with_current_context(work), [
90-
'bear',
91-
'cat',
92-
'dog',
93-
'horse',
94-
'rabbit',
95-
])
96-
pool.close()
97-
pool.join()
98-
println('Main thread: {}'.format(Context))
99-
100-
Here goes a simple demo of how async could work in Python 3.7+::
101-
102-
import asyncio
103-
104-
from opentelemetry.context import Context
105-
106-
class Span(object):
107-
def __init__(self, name):
108-
self.name = name
109-
self.parent = Context.current_span
110-
111-
def __repr__(self):
112-
return ('{}(name={}, parent={})'
113-
.format(
114-
type(self).__name__,
115-
self.name,
116-
self.parent,
117-
))
118-
119-
async def __aenter__(self):
120-
Context.current_span = self
121-
122-
async def __aexit__(self, exc_type, exc, tb):
123-
Context.current_span = self.parent
124-
125-
async def main():
126-
print(Context)
127-
async with Span('foo'):
128-
print(Context)
129-
await asyncio.sleep(0.1)
130-
async with Span('bar'):
131-
print(Context)
132-
await asyncio.sleep(0.1)
133-
print(Context)
134-
await asyncio.sleep(0.1)
135-
print(Context)
136-
137-
if __name__ == '__main__':
138-
asyncio.run(main())
139-
"""
140-
141-
from .base_context import BaseRuntimeContext
142-
143-
__all__ = ["Context"]
144-
145-
try:
146-
from .async_context import AsyncRuntimeContext
147-
148-
Context = AsyncRuntimeContext() # type: BaseRuntimeContext
149-
except ImportError:
150-
from .thread_local_context import ThreadLocalRuntimeContext
151-
152-
Context = ThreadLocalRuntimeContext()
15+
import logging
16+
import typing
17+
from os import environ
18+
19+
from pkg_resources import iter_entry_points
20+
21+
from opentelemetry.context.context import Context, RuntimeContext
22+
23+
logger = logging.getLogger(__name__)
24+
_RUNTIME_CONTEXT = None # type: typing.Optional[RuntimeContext]
25+
26+
27+
def get_value(key: str, context: typing.Optional[Context] = None) -> "object":
28+
"""To access the local state of a concern, the RuntimeContext API
29+
provides a function which takes a context and a key as input,
30+
and returns a value.
31+
32+
Args:
33+
key: The key of the value to retrieve.
34+
context: The context from which to retrieve the value, if None, the current context is used.
35+
"""
36+
return context.get(key) if context is not None else get_current().get(key)
37+
38+
39+
def set_value(
40+
key: str, value: "object", context: typing.Optional[Context] = None
41+
) -> Context:
42+
"""To record the local state of a cross-cutting concern, the
43+
RuntimeContext API provides a function which takes a context, a
44+
key, and a value as input, and returns an updated context
45+
which contains the new value.
46+
47+
Args:
48+
key: The key of the entry to set
49+
value: The value of the entry to set
50+
context: The context to copy, if None, the current context is used
51+
"""
52+
if context is None:
53+
context = get_current()
54+
new_values = context.copy()
55+
new_values[key] = value
56+
return Context(new_values)
57+
58+
59+
def remove_value(
60+
key: str, context: typing.Optional[Context] = None
61+
) -> Context:
62+
"""To remove a value, this method returns a new context with the key
63+
cleared. Note that the removed value still remains present in the old
64+
context.
65+
66+
Args:
67+
key: The key of the entry to remove
68+
context: The context to copy, if None, the current context is used
69+
"""
70+
if context is None:
71+
context = get_current()
72+
new_values = context.copy()
73+
new_values.pop(key, None)
74+
return Context(new_values)
75+
76+
77+
def get_current() -> Context:
78+
"""To access the context associated with program execution,
79+
the RuntimeContext API provides a function which takes no arguments
80+
and returns a RuntimeContext.
81+
"""
82+
83+
global _RUNTIME_CONTEXT # pylint: disable=global-statement
84+
if _RUNTIME_CONTEXT is None:
85+
# FIXME use a better implementation of a configuration manager to avoid having
86+
# to get configuration values straight from environment variables
87+
88+
configured_context = environ.get(
89+
"OPENTELEMETRY_CONTEXT", "default_context"
90+
) # type: str
91+
try:
92+
_RUNTIME_CONTEXT = next(
93+
iter_entry_points("opentelemetry_context", configured_context)
94+
).load()()
95+
except Exception: # pylint: disable=broad-except
96+
logger.error("Failed to load context: %s", configured_context)
97+
98+
return _RUNTIME_CONTEXT.get_current() # type:ignore
99+
100+
101+
def set_current(context: Context) -> Context:
102+
"""To associate a context with program execution, the Context
103+
API provides a function which takes a Context.
104+
105+
Args:
106+
context: The context to use as current.
107+
"""
108+
old_context = get_current()
109+
_RUNTIME_CONTEXT.set_current(context) # type:ignore
110+
return old_context
111+
112+
113+
def with_current_context(
114+
func: typing.Callable[..., "object"]
115+
) -> typing.Callable[..., "object"]:
116+
"""Capture the current context and apply it to the provided func."""
117+
118+
caller_context = get_current()
119+
120+
def call_with_current_context(
121+
*args: "object", **kwargs: "object"
122+
) -> "object":
123+
try:
124+
backup = get_current()
125+
set_current(caller_context)
126+
return func(*args, **kwargs)
127+
finally:
128+
set_current(backup)
129+
130+
return call_with_current_context

opentelemetry-api/src/opentelemetry/context/async_context.py

Lines changed: 0 additions & 45 deletions
This file was deleted.

0 commit comments

Comments
 (0)