Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 7c5e018

Browse files
authored
Initial implementation of context (open-telemetry#57)
1 parent f609450 commit 7c5e018

File tree

6 files changed

+345
-2
lines changed

6 files changed

+345
-2
lines changed

mypy-relaxed.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
disallow_any_unimported = True
55
; disallow_any_expr = True
66
disallow_any_decorated = True
7-
disallow_any_explicit = True
7+
; disallow_any_explicit = True
88
disallow_any_generics = True
99
disallow_subclassing_any = True
1010
disallow_untyped_calls = True

mypy.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
disallow_any_unimported = True
33
disallow_any_expr = True
44
disallow_any_decorated = True
5-
disallow_any_explicit = True
5+
; disallow_any_explicit = True
66
disallow_any_generics = True
77
disallow_subclassing_any = True
88
disallow_untyped_calls = True

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

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,144 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
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 `Context.with_current_context`
31+
can be used to carry the context across threads:
32+
33+
from threading import Thread
34+
from opentelemetry.context import Context
35+
36+
def work(name):
37+
print('Entering worker:', Context)
38+
Context.operation_id = name
39+
print('Exiting worker:', Context)
40+
41+
if __name__ == '__main__':
42+
print('Main thread:', Context)
43+
Context.operation_id = 'main'
44+
45+
print('Main thread:', Context)
46+
47+
# by default context is not propagated to worker thread
48+
thread = Thread(target=work, args=('foo',))
49+
thread.start()
50+
thread.join()
51+
52+
print('Main thread:', Context)
53+
54+
# user can propagate context explicitly
55+
thread = Thread(
56+
target=Context.with_current_context(work),
57+
args=('bar',),
58+
)
59+
thread.start()
60+
thread.join()
61+
62+
print('Main thread:', Context)
63+
64+
Here goes another example using thread pool:
65+
66+
import time
67+
import threading
68+
69+
from multiprocessing.dummy import Pool as ThreadPool
70+
from opentelemetry.context import Context
71+
72+
_console_lock = threading.Lock()
73+
74+
def println(msg):
75+
with _console_lock:
76+
print(msg)
77+
78+
def work(name):
79+
println('Entering worker[{}]: {}'.format(name, Context))
80+
Context.operation_id = name
81+
time.sleep(0.01)
82+
println('Exiting worker[{}]: {}'.format(name, Context))
83+
84+
if __name__ == "__main__":
85+
println('Main thread: {}'.format(Context))
86+
Context.operation_id = 'main'
87+
pool = ThreadPool(2) # create a thread pool with 2 threads
88+
pool.map(Context.with_current_context(work), [
89+
'bear',
90+
'cat',
91+
'dog',
92+
'horse',
93+
'rabbit',
94+
])
95+
pool.close()
96+
pool.join()
97+
println('Main thread: {}'.format(Context))
98+
99+
Here goes a simple demo of how async could work in Python 3.7+:
100+
101+
import asyncio
102+
103+
from opentelemetry.context import Context
104+
105+
class Span(object):
106+
def __init__(self, name):
107+
self.name = name
108+
self.parent = Context.current_span
109+
110+
def __repr__(self):
111+
return ('{}(name={}, parent={})'
112+
.format(
113+
type(self).__name__,
114+
self.name,
115+
self.parent,
116+
))
117+
118+
async def __aenter__(self):
119+
Context.current_span = self
120+
121+
async def __aexit__(self, exc_type, exc, tb):
122+
Context.current_span = self.parent
123+
124+
async def main():
125+
print(Context)
126+
async with Span('foo'):
127+
print(Context)
128+
await asyncio.sleep(0.1)
129+
async with Span('bar'):
130+
print(Context)
131+
await asyncio.sleep(0.1)
132+
print(Context)
133+
await asyncio.sleep(0.1)
134+
print(Context)
135+
136+
if __name__ == '__main__':
137+
asyncio.run(main())
138+
"""
139+
140+
import typing
141+
142+
from .base_context import BaseRuntimeContext
143+
144+
__all__ = ['Context']
145+
146+
147+
Context: typing.Optional[BaseRuntimeContext]
148+
149+
try:
150+
from .async_context import AsyncRuntimeContext
151+
Context = AsyncRuntimeContext() # pylint:disable=invalid-name
152+
except ImportError:
153+
from .thread_local_context import ThreadLocalRuntimeContext
154+
Context = ThreadLocalRuntimeContext() # pylint:disable=invalid-name
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
from contextvars import ContextVar
16+
import typing
17+
18+
from . import base_context
19+
20+
21+
class AsyncRuntimeContext(base_context.BaseRuntimeContext):
22+
class Slot(base_context.BaseRuntimeContext.Slot):
23+
def __init__(self, name: str, default: 'object'):
24+
# pylint: disable=super-init-not-called
25+
self.name = name
26+
self.contextvar: 'ContextVar[object]' = ContextVar(name)
27+
self.default: typing.Callable[..., object]
28+
self.default = base_context.wrap_callable(default)
29+
30+
def clear(self) -> None:
31+
self.contextvar.set(self.default())
32+
33+
def get(self) -> 'object':
34+
try:
35+
return self.contextvar.get()
36+
except LookupError:
37+
value = self.default()
38+
self.set(value)
39+
return value
40+
41+
def set(self, value: 'object') -> None:
42+
self.contextvar.set(value)
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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+
import threading
16+
import typing
17+
18+
19+
def wrap_callable(target: 'object') -> typing.Callable[[], object]:
20+
if callable(target):
21+
return target
22+
return lambda: target
23+
24+
25+
class BaseRuntimeContext:
26+
class Slot:
27+
def __init__(self, name: str, default: 'object'):
28+
raise NotImplementedError
29+
30+
def clear(self) -> None:
31+
raise NotImplementedError
32+
33+
def get(self) -> 'object':
34+
raise NotImplementedError
35+
36+
def set(self, value: 'object') -> None:
37+
raise NotImplementedError
38+
39+
_lock = threading.Lock()
40+
_slots: typing.Dict[str, Slot] = {}
41+
42+
@classmethod
43+
def clear(cls) -> None:
44+
"""Clear all slots to their default value."""
45+
keys = cls._slots.keys()
46+
for name in keys:
47+
slot = cls._slots[name]
48+
slot.clear()
49+
50+
@classmethod
51+
def register_slot(cls, name: str, default: 'object' = None) -> 'Slot':
52+
"""Register a context slot with an optional default value.
53+
54+
:type name: str
55+
:param name: The name of the context slot.
56+
57+
:type default: object
58+
:param name: The default value of the slot, can be a value or lambda.
59+
60+
:returns: The registered slot.
61+
"""
62+
with cls._lock:
63+
if name in cls._slots:
64+
raise ValueError('slot {} already registered'.format(name))
65+
slot = cls.Slot(name, default)
66+
cls._slots[name] = slot
67+
return slot
68+
69+
def apply(self, snapshot: typing.Dict[str, 'object']) -> None:
70+
"""Set the current context from a given snapshot dictionary"""
71+
72+
for name in snapshot:
73+
setattr(self, name, snapshot[name])
74+
75+
def snapshot(self) -> typing.Dict[str, 'object']:
76+
"""Return a dictionary of current slots by reference."""
77+
78+
keys = self._slots.keys()
79+
return dict((n, self._slots[n].get()) for n in keys)
80+
81+
def __repr__(self) -> str:
82+
return '{}({})'.format(type(self).__name__, self.snapshot())
83+
84+
def __getattr__(self, name: str) -> 'object':
85+
if name not in self._slots:
86+
self.register_slot(name, None)
87+
slot = self._slots[name]
88+
return slot.get()
89+
90+
def __setattr__(self, name: str, value: 'object') -> None:
91+
if name not in self._slots:
92+
self.register_slot(name, None)
93+
slot = self._slots[name]
94+
slot.set(value)
95+
96+
def with_current_context(
97+
self,
98+
func: typing.Callable[..., 'object'],
99+
) -> typing.Callable[..., 'object']:
100+
"""Capture the current context and apply it to the provided func.
101+
"""
102+
103+
caller_context = self.snapshot()
104+
105+
def call_with_current_context(
106+
*args: 'object',
107+
**kwargs: 'object',
108+
) -> 'object':
109+
try:
110+
backup_context = self.snapshot()
111+
self.apply(caller_context)
112+
return func(*args, **kwargs)
113+
finally:
114+
self.apply(backup_context)
115+
116+
return call_with_current_context
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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+
import threading
16+
import typing
17+
18+
from . import base_context
19+
20+
21+
class ThreadLocalRuntimeContext(base_context.BaseRuntimeContext):
22+
class Slot(base_context.BaseRuntimeContext.Slot):
23+
_thread_local = threading.local()
24+
25+
def __init__(self, name: str, default: 'object'):
26+
# pylint: disable=super-init-not-called
27+
self.name = name
28+
self.default: typing.Callable[..., object]
29+
self.default = base_context.wrap_callable(default)
30+
31+
def clear(self) -> None:
32+
setattr(self._thread_local, self.name, self.default())
33+
34+
def get(self) -> 'object':
35+
try:
36+
got: object = getattr(self._thread_local, self.name)
37+
return got
38+
except AttributeError:
39+
value = self.default()
40+
self.set(value)
41+
return value
42+
43+
def set(self, value: 'object') -> None:
44+
setattr(self._thread_local, self.name, value)

0 commit comments

Comments
 (0)