mermaid-trace 0.3.1__py3-none-any.whl → 0.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mermaid_trace/__init__.py +83 -20
- mermaid_trace/cli.py +144 -34
- mermaid_trace/core/context.py +102 -41
- mermaid_trace/core/decorators.py +322 -104
- mermaid_trace/core/events.py +160 -45
- mermaid_trace/core/formatter.py +107 -28
- mermaid_trace/handlers/async_handler.py +105 -0
- mermaid_trace/handlers/mermaid_handler.py +84 -51
- mermaid_trace/integrations/fastapi.py +94 -50
- {mermaid_trace-0.3.1.dist-info → mermaid_trace-0.4.1.dist-info}/METADATA +25 -8
- mermaid_trace-0.4.1.dist-info/RECORD +16 -0
- mermaid_trace-0.3.1.dist-info/RECORD +0 -15
- {mermaid_trace-0.3.1.dist-info → mermaid_trace-0.4.1.dist-info}/WHEEL +0 -0
- {mermaid_trace-0.3.1.dist-info → mermaid_trace-0.4.1.dist-info}/entry_points.txt +0 -0
- {mermaid_trace-0.3.1.dist-info → mermaid_trace-0.4.1.dist-info}/licenses/LICENSE +0 -0
mermaid_trace/core/context.py
CHANGED
|
@@ -1,50 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Log Context Management Module
|
|
3
|
+
|
|
4
|
+
This module provides a thread-safe, async-friendly context management system
|
|
5
|
+
for tracking execution context across the application. It uses Python's ContextVar
|
|
6
|
+
mechanism to ensure proper context propagation in both synchronous and asynchronous
|
|
7
|
+
environments.
|
|
8
|
+
"""
|
|
9
|
+
|
|
1
10
|
from contextvars import ContextVar, Token
|
|
2
11
|
from contextlib import asynccontextmanager, contextmanager
|
|
3
12
|
from typing import Any, AsyncIterator, Dict, Iterator
|
|
4
13
|
import uuid
|
|
5
14
|
|
|
15
|
+
|
|
6
16
|
class LogContext:
|
|
7
17
|
"""
|
|
8
18
|
Manages global context information for logging (e.g., request_id, user_id, current_participant).
|
|
9
|
-
|
|
10
|
-
This class utilizes `contextvars.ContextVar` to ensure thread-safety and
|
|
11
|
-
correct context propagation in asynchronous (asyncio) environments.
|
|
12
|
-
|
|
13
|
-
event loop, ensuring that context is preserved across `await` points but isolated
|
|
19
|
+
|
|
20
|
+
This class utilizes `contextvars.ContextVar` to ensure thread-safety and
|
|
21
|
+
correct context propagation in asynchronous (asyncio) environments. Unlike
|
|
22
|
+
`threading.local()`, `ContextVar` works natively with Python's async/await
|
|
23
|
+
event loop, ensuring that context is preserved across `await` points but isolated
|
|
14
24
|
between different concurrent tasks.
|
|
15
25
|
"""
|
|
16
|
-
|
|
17
|
-
# ContextVar
|
|
18
|
-
#
|
|
19
|
-
# "log_context" is the name of the variable, useful for debugging.
|
|
20
|
-
# The default value is implicitly an empty state if not set (handled in _get_store).
|
|
26
|
+
|
|
27
|
+
# ContextVar stores a dictionary unique to the current execution context (Task/Thread)
|
|
28
|
+
# The name "log_context" is used for debugging purposes
|
|
21
29
|
_context_store: ContextVar[Dict[str, Any]] = ContextVar("log_context")
|
|
22
30
|
|
|
23
31
|
@classmethod
|
|
24
32
|
def _get_store(cls) -> Dict[str, Any]:
|
|
25
33
|
"""
|
|
26
34
|
Retrieves the current context dictionary.
|
|
27
|
-
|
|
28
|
-
If the context variable has not been set in the current context,
|
|
29
|
-
it
|
|
30
|
-
and
|
|
35
|
+
|
|
36
|
+
If the context variable has not been set in the current context,
|
|
37
|
+
it creates a fresh empty dictionary, sets it to the contextvar,
|
|
38
|
+
and returns it. This prevents LookupError and ensures there's
|
|
39
|
+
always a valid dictionary to work with.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Dict[str, Any]: Current context dictionary for the execution flow
|
|
31
43
|
"""
|
|
32
44
|
try:
|
|
33
45
|
return cls._context_store.get()
|
|
34
46
|
except LookupError:
|
|
35
|
-
|
|
47
|
+
empty_dict: Dict[str, Any] = {}
|
|
48
|
+
cls._context_store.set(empty_dict)
|
|
49
|
+
return empty_dict
|
|
36
50
|
|
|
37
51
|
@classmethod
|
|
38
52
|
def set(cls, key: str, value: Any) -> None:
|
|
39
53
|
"""
|
|
40
54
|
Sets a specific key-value pair in the current context.
|
|
41
|
-
|
|
42
|
-
Important: ContextVars are immutable collections. To modify the context,
|
|
55
|
+
|
|
56
|
+
Important: ContextVars are immutable collections. To modify the context,
|
|
43
57
|
we must:
|
|
44
|
-
1. Retrieve the current dictionary
|
|
45
|
-
2. Create a shallow copy
|
|
46
|
-
3. Update the copy
|
|
47
|
-
4. Re-set the ContextVar with the new dictionary
|
|
58
|
+
1. Retrieve the current dictionary using _get_store()
|
|
59
|
+
2. Create a shallow copy to avoid affecting parent contexts
|
|
60
|
+
3. Update the copy with the new key-value pair
|
|
61
|
+
4. Re-set the ContextVar with the new dictionary
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
key (str): Name of the context variable to set
|
|
65
|
+
value (Any): Value to associate with the key
|
|
48
66
|
"""
|
|
49
67
|
ctx = cls._get_store().copy()
|
|
50
68
|
ctx[key] = value
|
|
@@ -54,9 +72,12 @@ class LogContext:
|
|
|
54
72
|
def update(cls, data: Dict[str, Any]) -> None:
|
|
55
73
|
"""
|
|
56
74
|
Updates multiple keys in the current context at once.
|
|
57
|
-
|
|
58
|
-
This follows the same Copy-Update-Set pattern as `set()` to maintain
|
|
59
|
-
context isolation.
|
|
75
|
+
|
|
76
|
+
This follows the same Copy-Update-Set pattern as `set()` to maintain
|
|
77
|
+
context isolation between different execution flows.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
data (Dict[str, Any]): Dictionary of key-value pairs to update in context
|
|
60
81
|
"""
|
|
61
82
|
if not data:
|
|
62
83
|
return
|
|
@@ -68,6 +89,13 @@ class LogContext:
|
|
|
68
89
|
def get(cls, key: str, default: Any = None) -> Any:
|
|
69
90
|
"""
|
|
70
91
|
Retrieves a value from the current context safely.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
key (str): Name of the context variable to retrieve
|
|
95
|
+
default (Any, optional): Default value if key doesn't exist. Defaults to None.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Any: Value associated with the key, or default if key not found
|
|
71
99
|
"""
|
|
72
100
|
return cls._get_store().get(key, default)
|
|
73
101
|
|
|
@@ -75,6 +103,9 @@ class LogContext:
|
|
|
75
103
|
def get_all(cls) -> Dict[str, Any]:
|
|
76
104
|
"""
|
|
77
105
|
Returns a copy of the entire context dictionary.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Dict[str, Any]: Complete copy of the current context
|
|
78
109
|
"""
|
|
79
110
|
return cls._get_store().copy()
|
|
80
111
|
|
|
@@ -83,19 +114,25 @@ class LogContext:
|
|
|
83
114
|
def scope(cls, data: Dict[str, Any]) -> Iterator[None]:
|
|
84
115
|
"""
|
|
85
116
|
Synchronous context manager for temporary context updates.
|
|
86
|
-
|
|
117
|
+
|
|
87
118
|
Usage:
|
|
88
119
|
with LogContext.scope({"user_id": 123}):
|
|
89
120
|
# user_id is 123 here
|
|
90
121
|
some_function()
|
|
91
122
|
# user_id reverts to previous value (or disappears) here
|
|
92
|
-
|
|
123
|
+
|
|
93
124
|
Mechanism:
|
|
94
|
-
1. Copies current context and updates it with new data
|
|
95
|
-
2. Sets the ContextVar to this new state, receiving a `Token
|
|
96
|
-
3. Yields control to the block
|
|
97
|
-
4. Finally, uses the `Token` to reset the ContextVar to its exact state
|
|
98
|
-
before the block entered
|
|
125
|
+
1. Copies current context and updates it with new data
|
|
126
|
+
2. Sets the ContextVar to this new state, receiving a `Token`
|
|
127
|
+
3. Yields control to the block
|
|
128
|
+
4. Finally, uses the `Token` to reset the ContextVar to its exact state
|
|
129
|
+
before the block entered
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
data (Dict[str, Any]): Dictionary of context values to set within the scope
|
|
133
|
+
|
|
134
|
+
Yields:
|
|
135
|
+
None: Control to the block using this context manager
|
|
99
136
|
"""
|
|
100
137
|
current_ctx = cls._get_store().copy()
|
|
101
138
|
current_ctx.update(data)
|
|
@@ -103,7 +140,7 @@ class LogContext:
|
|
|
103
140
|
try:
|
|
104
141
|
yield
|
|
105
142
|
finally:
|
|
106
|
-
#
|
|
143
|
+
# Reset restores context to state before .set() was called
|
|
107
144
|
cls._context_store.reset(token)
|
|
108
145
|
|
|
109
146
|
@classmethod
|
|
@@ -111,14 +148,20 @@ class LogContext:
|
|
|
111
148
|
async def ascope(cls, data: Dict[str, Any]) -> AsyncIterator[None]:
|
|
112
149
|
"""
|
|
113
150
|
Async context manager for temporary context updates in coroutines.
|
|
114
|
-
|
|
151
|
+
|
|
115
152
|
Usage:
|
|
116
153
|
async with LogContext.ascope({"request_id": "abc"}):
|
|
117
154
|
await some_async_function()
|
|
118
|
-
|
|
155
|
+
|
|
119
156
|
This is functionally identical to `scope` but designed for `async with` blocks.
|
|
120
157
|
It ensures that even if the code inside `yield` suspends execution (await),
|
|
121
158
|
the context remains valid for that task.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
data (Dict[str, Any]): Dictionary of context values to set within the scope
|
|
162
|
+
|
|
163
|
+
Yields:
|
|
164
|
+
None: Control to the async block using this context manager
|
|
122
165
|
"""
|
|
123
166
|
current_ctx = cls._get_store().copy()
|
|
124
167
|
current_ctx.update(data)
|
|
@@ -136,6 +179,12 @@ class LogContext:
|
|
|
136
179
|
"""
|
|
137
180
|
Replaces the entire context with the provided data.
|
|
138
181
|
Returns a Token that can be used to manually reset the context later.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
data (Dict[str, Any]): New context dictionary to replace the current one
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Token[Dict[str, Any]]: Token for resetting context to previous state
|
|
139
188
|
"""
|
|
140
189
|
return cls._context_store.set(data.copy())
|
|
141
190
|
|
|
@@ -143,21 +192,30 @@ class LogContext:
|
|
|
143
192
|
def reset(cls, token: Token[Dict[str, Any]]) -> None:
|
|
144
193
|
"""
|
|
145
194
|
Manually resets the context using a Token obtained from `set` or `set_all`.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
token (Token[Dict[str, Any]]): Token returned by set_all() method
|
|
146
198
|
"""
|
|
147
199
|
cls._context_store.reset(token)
|
|
148
200
|
|
|
149
201
|
@classmethod
|
|
150
202
|
def current_participant(cls) -> str:
|
|
151
203
|
"""
|
|
152
|
-
Helper to get the 'participant' field, representing the current active object/module.
|
|
204
|
+
Helper method to get the 'participant' field, representing the current active object/module.
|
|
153
205
|
Defaults to 'Unknown' if not set.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
str: Name of the current participant in the trace flow
|
|
154
209
|
"""
|
|
155
210
|
return str(cls.get("participant", "Unknown"))
|
|
156
211
|
|
|
157
212
|
@classmethod
|
|
158
213
|
def set_participant(cls, name: str) -> None:
|
|
159
214
|
"""
|
|
160
|
-
Helper to set the 'participant' field.
|
|
215
|
+
Helper method to set the 'participant' field.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
name (str): Name of the participant to set
|
|
161
219
|
"""
|
|
162
220
|
cls.set("participant", name)
|
|
163
221
|
|
|
@@ -165,13 +223,16 @@ class LogContext:
|
|
|
165
223
|
def current_trace_id(cls) -> str:
|
|
166
224
|
"""
|
|
167
225
|
Retrieves the current trace ID for correlating events in a single flow.
|
|
168
|
-
|
|
226
|
+
|
|
169
227
|
Lazy Initialization Logic:
|
|
170
|
-
If no trace_id exists in the current context, it generates a new UUIDv4
|
|
228
|
+
If no trace_id exists in the current context, it generates a new UUIDv4
|
|
171
229
|
and sets it immediately. This ensures that:
|
|
172
|
-
1. A trace ID is always available when asked for
|
|
173
|
-
2. Once generated, the same ID persists for the duration of the context
|
|
174
|
-
(unless manually changed), linking all subsequent logs together
|
|
230
|
+
1. A trace ID is always available when asked for
|
|
231
|
+
2. Once generated, the same ID persists for the duration of the context
|
|
232
|
+
(unless manually changed), linking all subsequent logs together
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
str: Unique trace ID for the current execution flow
|
|
175
236
|
"""
|
|
176
237
|
tid = cls.get("trace_id")
|
|
177
238
|
if not tid:
|