1414
1515import logging
1616import typing
17+ from functools import wraps
1718from os import environ
1819from sys import version_info
1920
2526_RUNTIME_CONTEXT = None # type: typing.Optional[RuntimeContext]
2627
2728
29+ _F = typing .TypeVar ("_F" , bound = typing .Callable [..., typing .Any ])
30+
31+
32+ def _load_runtime_context (func : _F ) -> _F :
33+ """A decorator used to initialize the global RuntimeContext
34+
35+ Returns:
36+ A wrapper of the decorated method.
37+ """
38+
39+ @wraps (func ) # type: ignore
40+ def wrapper (
41+ * args : typing .Tuple [typing .Any , typing .Any ],
42+ ** kwargs : typing .Dict [typing .Any , typing .Any ]
43+ ) -> typing .Optional [typing .Any ]:
44+ global _RUNTIME_CONTEXT # pylint: disable=global-statement
45+ if _RUNTIME_CONTEXT is None :
46+ # FIXME use a better implementation of a configuration manager to avoid having
47+ # to get configuration values straight from environment variables
48+ if version_info < (3 , 5 ):
49+ # contextvars are not supported in 3.4, use thread-local storage
50+ default_context = "threadlocal_context"
51+ else :
52+ default_context = "contextvars_context"
53+
54+ configured_context = environ .get (
55+ "OPENTELEMETRY_CONTEXT" , default_context
56+ ) # type: str
57+ try :
58+ _RUNTIME_CONTEXT = next (
59+ iter_entry_points (
60+ "opentelemetry_context" , configured_context
61+ )
62+ ).load ()()
63+ except Exception : # pylint: disable=broad-except
64+ logger .error ("Failed to load context: %s" , configured_context )
65+ return func (* args , ** kwargs ) # type: ignore
66+
67+ return wrapper # type:ignore
68+
69+
2870def get_value (key : str , context : typing .Optional [Context ] = None ) -> "object" :
2971 """To access the local state of a concern, the RuntimeContext API
3072 provides a function which takes a context and a key as input,
@@ -33,6 +75,9 @@ def get_value(key: str, context: typing.Optional[Context] = None) -> "object":
3375 Args:
3476 key: The key of the value to retrieve.
3577 context: The context from which to retrieve the value, if None, the current context is used.
78+
79+ Returns:
80+ The value associated with the key.
3681 """
3782 return context .get (key ) if context is not None else get_current ().get (key )
3883
@@ -46,91 +91,55 @@ def set_value(
4691 which contains the new value.
4792
4893 Args:
49- key: The key of the entry to set
50- value: The value of the entry to set
51- context: The context to copy, if None, the current context is used
52- """
53- if context is None :
54- context = get_current ()
55- new_values = context .copy ()
56- new_values [key ] = value
57- return Context (new_values )
94+ key: The key of the entry to set.
95+ value: The value of the entry to set.
96+ context: The context to copy, if None, the current context is used.
5897
59-
60- def remove_value (
61- key : str , context : typing .Optional [Context ] = None
62- ) -> Context :
63- """To remove a value, this method returns a new context with the key
64- cleared. Note that the removed value still remains present in the old
65- context.
66-
67- Args:
68- key: The key of the entry to remove
69- context: The context to copy, if None, the current context is used
98+ Returns:
99+ A new `Context` containing the value set.
70100 """
71101 if context is None :
72102 context = get_current ()
73103 new_values = context .copy ()
74- new_values . pop ( key , None )
104+ new_values [ key ] = value
75105 return Context (new_values )
76106
77107
108+ @_load_runtime_context # type: ignore
78109def get_current () -> Context :
79110 """To access the context associated with program execution,
80- the RuntimeContext API provides a function which takes no arguments
81- and returns a RuntimeContext.
82- """
83-
84- global _RUNTIME_CONTEXT # pylint: disable=global-statement
85- if _RUNTIME_CONTEXT is None :
86- # FIXME use a better implementation of a configuration manager to avoid having
87- # to get configuration values straight from environment variables
88- if version_info < (3 , 5 ):
89- # contextvars are not supported in 3.4, use thread-local storage
90- default_context = "threadlocal_context"
91- else :
92- default_context = "contextvars_context"
93-
94- configured_context = environ .get (
95- "OPENTELEMETRY_CONTEXT" , default_context
96- ) # type: str
97- try :
98- _RUNTIME_CONTEXT = next (
99- iter_entry_points ("opentelemetry_context" , configured_context )
100- ).load ()()
101- except Exception : # pylint: disable=broad-except
102- logger .error ("Failed to load context: %s" , configured_context )
111+ the Context API provides a function which takes no arguments
112+ and returns a Context.
103113
114+ Returns:
115+ The current `Context` object.
116+ """
104117 return _RUNTIME_CONTEXT .get_current () # type:ignore
105118
106119
107- def set_current (context : Context ) -> Context :
108- """To associate a context with program execution, the Context
109- API provides a function which takes a Context.
120+ @_load_runtime_context # type: ignore
121+ def attach (context : Context ) -> object :
122+ """Associates a Context with the caller's current execution unit. Returns
123+ a token that can be used to restore the previous Context.
110124
111125 Args:
112- context: The context to use as current.
113- """
114- old_context = get_current ()
115- _RUNTIME_CONTEXT .set_current (context ) # type:ignore
116- return old_context
117-
126+ context: The Context to set as current.
118127
119- def with_current_context (
120- func : typing . Callable [..., "object" ]
121- ) -> typing . Callable [..., "object" ]:
122- """Capture the current context and apply it to the provided func."""
128+ Returns:
129+ A token that can be used with `detach` to reset the context.
130+ """
131+ return _RUNTIME_CONTEXT . attach ( context ) # type:ignore
123132
124- caller_context = get_current ()
125133
126- def call_with_current_context (
127- * args : "object" , ** kwargs : "object"
128- ) -> "object" :
129- try :
130- backup = get_current ()
131- set_current (caller_context )
132- return func (* args , ** kwargs )
133- finally :
134- set_current (backup )
134+ @_load_runtime_context # type: ignore
135+ def detach (token : object ) -> None :
136+ """Resets the Context associated with the caller's current execution unit
137+ to the value it had before attaching a specified Context.
135138
136- return call_with_current_context
139+ Args:
140+ token: The Token that was returned by a previous call to attach a Context.
141+ """
142+ try :
143+ _RUNTIME_CONTEXT .detach (token ) # type: ignore
144+ except Exception : # pylint: disable=broad-except
145+ logger .error ("Failed to detach context" )
0 commit comments