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/decorators.py
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Function Tracing Decorator Module
|
|
3
|
+
|
|
4
|
+
This module provides the core tracing functionality for MermaidTrace. It includes:
|
|
5
|
+
- A decorator (`trace`/`trace_interaction`) to instrument functions for tracing
|
|
6
|
+
- Helper functions for formatting and logging trace events
|
|
7
|
+
- Context management for tracking call chains
|
|
8
|
+
|
|
9
|
+
The decorator can be applied to both synchronous and asynchronous functions,
|
|
10
|
+
and automatically handles context propagation for nested calls.
|
|
11
|
+
"""
|
|
12
|
+
|
|
1
13
|
import functools
|
|
2
14
|
import logging
|
|
3
15
|
import inspect
|
|
@@ -7,70 +19,127 @@ from typing import Optional, Any, Callable, Tuple, Dict, Union, TypeVar, cast, o
|
|
|
7
19
|
from .events import FlowEvent
|
|
8
20
|
from .context import LogContext
|
|
9
21
|
|
|
22
|
+
# Logger name for flow events - used to isolate tracing logs from other application logs
|
|
10
23
|
FLOW_LOGGER_NAME = "mermaid_trace.flow"
|
|
11
24
|
|
|
12
25
|
# Define generic type variable for the decorated function
|
|
13
26
|
F = TypeVar("F", bound=Callable[..., Any])
|
|
14
27
|
|
|
28
|
+
|
|
15
29
|
def get_flow_logger() -> logging.Logger:
|
|
16
|
-
"""Returns the dedicated logger for flow events.
|
|
30
|
+
"""Returns the dedicated logger for flow events.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
logging.Logger: Logger instance configured for tracing events
|
|
34
|
+
"""
|
|
17
35
|
return logging.getLogger(FLOW_LOGGER_NAME)
|
|
18
36
|
|
|
19
|
-
|
|
37
|
+
|
|
38
|
+
def _safe_repr(obj: Any, max_len: int = 50, max_depth: int = 1) -> str:
|
|
20
39
|
"""
|
|
21
40
|
Safely creates a string representation of an object.
|
|
22
|
-
|
|
23
|
-
Prevents massive log files by truncating long strings/objects
|
|
41
|
+
|
|
42
|
+
Prevents massive log files by truncating long strings/objects
|
|
24
43
|
and handling exceptions during __repr__ calls (e.g. strict objects).
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
obj: The object to represent as a string
|
|
47
|
+
max_len: Maximum length of the resulting string
|
|
48
|
+
max_depth: Maximum recursion depth for nested objects
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
str: Safe, truncated representation of the object
|
|
25
52
|
"""
|
|
26
53
|
try:
|
|
27
|
-
|
|
54
|
+
# Create a custom repr object to control depth and length
|
|
55
|
+
a_repr = reprlib.Repr()
|
|
56
|
+
a_repr.maxstring = max_len
|
|
57
|
+
a_repr.maxother = max_len
|
|
58
|
+
a_repr.maxlevel = max_depth
|
|
59
|
+
|
|
60
|
+
r = a_repr.repr(obj)
|
|
28
61
|
if len(r) > max_len:
|
|
29
62
|
return r[:max_len] + "..."
|
|
30
63
|
return r
|
|
31
64
|
except Exception:
|
|
65
|
+
# Fallback if repr() fails for any reason
|
|
32
66
|
return "<unrepresentable>"
|
|
33
67
|
|
|
34
|
-
|
|
68
|
+
|
|
69
|
+
def _format_args(
|
|
70
|
+
args: Tuple[Any, ...],
|
|
71
|
+
kwargs: Dict[str, Any],
|
|
72
|
+
capture_args: bool = True,
|
|
73
|
+
max_arg_length: int = 50,
|
|
74
|
+
max_arg_depth: int = 1,
|
|
75
|
+
) -> str:
|
|
35
76
|
"""
|
|
36
77
|
Formats function arguments into a single string "arg1, arg2, k=v".
|
|
37
78
|
Used for the arrow label in the diagram.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
args: Positional arguments to format
|
|
82
|
+
kwargs: Keyword arguments to format
|
|
83
|
+
capture_args: Whether to capture and format arguments at all
|
|
84
|
+
max_arg_length: Maximum length of each argument representation
|
|
85
|
+
max_arg_depth: Maximum recursion depth for nested arguments
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
str: Formatted string of arguments, or empty string if capture_args is False
|
|
38
89
|
"""
|
|
39
|
-
|
|
90
|
+
if not capture_args:
|
|
91
|
+
return ""
|
|
92
|
+
|
|
93
|
+
parts: list[str] = []
|
|
94
|
+
|
|
95
|
+
# Format positional arguments
|
|
40
96
|
for arg in args:
|
|
41
|
-
parts.append(_safe_repr(arg))
|
|
42
|
-
|
|
97
|
+
parts.append(_safe_repr(arg, max_len=max_arg_length, max_depth=max_arg_depth))
|
|
98
|
+
|
|
99
|
+
# Format keyword arguments
|
|
43
100
|
for k, v in kwargs.items():
|
|
44
|
-
|
|
45
|
-
|
|
101
|
+
val_str = _safe_repr(v, max_len=max_arg_length, max_depth=max_arg_depth)
|
|
102
|
+
parts.append(f"{k}={val_str}")
|
|
103
|
+
|
|
46
104
|
return ", ".join(parts)
|
|
47
105
|
|
|
48
|
-
|
|
106
|
+
|
|
107
|
+
def _resolve_target(
|
|
108
|
+
func: Callable[..., Any], args: Tuple[Any, ...], target_override: Optional[str]
|
|
109
|
+
) -> str:
|
|
49
110
|
"""
|
|
50
111
|
Determines the name of the participant (Target) for the diagram.
|
|
51
|
-
|
|
112
|
+
|
|
52
113
|
Resolution Priority:
|
|
53
114
|
1. **Override**: If the user explicitly provided `target="Name"`, use it.
|
|
54
|
-
2. **Instance Method**: If the first arg looks like `self` (has __class__),
|
|
115
|
+
2. **Instance Method**: If the first arg looks like `self` (has __class__),
|
|
55
116
|
use the class name.
|
|
56
117
|
3. **Class Method**: If the first arg is a type (cls), use the class name.
|
|
57
118
|
4. **Module Function**: Fallback to the name of the module containing the function.
|
|
58
119
|
5. **Fallback**: "Unknown".
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
func: The function being called
|
|
123
|
+
args: Positional arguments passed to the function
|
|
124
|
+
target_override: Explicit target name provided by the user, if any
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
str: Resolved target name for the diagram
|
|
59
128
|
"""
|
|
60
129
|
if target_override:
|
|
61
130
|
return target_override
|
|
62
|
-
|
|
63
|
-
# Heuristic: If it's a method call, args[0] is usually 'self'
|
|
131
|
+
|
|
132
|
+
# Heuristic: If it's a method call, args[0] is usually 'self' or 'cls'
|
|
64
133
|
if args:
|
|
65
134
|
first_arg = args[0]
|
|
66
|
-
# Check if it looks like a class instance
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
135
|
+
# Check if it looks like a class instance (not a primitive or container)
|
|
136
|
+
if hasattr(first_arg, "__class__") and not isinstance(
|
|
137
|
+
first_arg, (str, int, float, bool, list, dict, type)
|
|
138
|
+
):
|
|
139
|
+
return str(first_arg.__class__.__name__)
|
|
71
140
|
# Check if it looks like a class (cls) - e.g. @classmethod
|
|
72
141
|
if isinstance(first_arg, type):
|
|
73
|
-
|
|
142
|
+
return first_arg.__name__
|
|
74
143
|
|
|
75
144
|
# Fallback to module name for standalone functions
|
|
76
145
|
module = inspect.getmodule(func)
|
|
@@ -78,175 +147,324 @@ def _resolve_target(func: Callable[..., Any], args: Tuple[Any, ...], target_over
|
|
|
78
147
|
return module.__name__.split(".")[-1]
|
|
79
148
|
return "Unknown"
|
|
80
149
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
150
|
+
|
|
151
|
+
def _log_interaction(
|
|
152
|
+
logger: logging.Logger,
|
|
153
|
+
source: str,
|
|
154
|
+
target: str,
|
|
155
|
+
action: str,
|
|
156
|
+
params: str,
|
|
157
|
+
trace_id: str,
|
|
158
|
+
) -> None:
|
|
87
159
|
"""
|
|
88
160
|
Logs the 'Call' event (Start of function).
|
|
89
|
-
|
|
161
|
+
Generates a FlowEvent and logs it with the appropriate context.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
logger: Logger instance to use for logging
|
|
165
|
+
source: Name of the source participant (caller)
|
|
166
|
+
target: Name of the target participant (callee)
|
|
167
|
+
action: Name of the action being performed
|
|
168
|
+
params: Formatted string of function arguments
|
|
169
|
+
trace_id: Unique trace identifier for correlation
|
|
90
170
|
"""
|
|
91
171
|
req_event = FlowEvent(
|
|
92
|
-
source=source,
|
|
93
|
-
|
|
94
|
-
|
|
172
|
+
source=source,
|
|
173
|
+
target=target,
|
|
174
|
+
action=action,
|
|
175
|
+
message=action,
|
|
176
|
+
params=params,
|
|
177
|
+
trace_id=trace_id,
|
|
95
178
|
)
|
|
96
179
|
# The 'extra' dict is critical: the Handler will pick this up to format the Mermaid line
|
|
97
180
|
logger.info(f"{source}->{target}: {action}", extra={"flow_event": req_event})
|
|
98
181
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
182
|
+
|
|
183
|
+
def _log_return(
|
|
184
|
+
logger: logging.Logger,
|
|
185
|
+
source: str,
|
|
186
|
+
target: str,
|
|
187
|
+
action: str,
|
|
188
|
+
result: Any,
|
|
189
|
+
trace_id: str,
|
|
190
|
+
capture_args: bool = True,
|
|
191
|
+
max_arg_length: int = 50,
|
|
192
|
+
max_arg_depth: int = 1,
|
|
193
|
+
) -> None:
|
|
105
194
|
"""
|
|
106
195
|
Logs the 'Return' event (End of function).
|
|
107
196
|
Arrow: target --> source (Dotted line return)
|
|
108
|
-
|
|
197
|
+
|
|
109
198
|
Note: 'source' here is the original caller, 'target' is the callee.
|
|
110
199
|
So the return arrow goes from target back to source.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
logger: Logger instance to use for logging
|
|
203
|
+
source: Name of the original caller
|
|
204
|
+
target: Name of the callee that's returning
|
|
205
|
+
action: Name of the action being returned from
|
|
206
|
+
result: Return value of the function
|
|
207
|
+
trace_id: Unique trace identifier for correlation
|
|
208
|
+
capture_args: Whether to include the return value in the log
|
|
209
|
+
max_arg_length: Maximum length of the return value representation
|
|
210
|
+
max_arg_depth: Maximum recursion depth for nested return values
|
|
111
211
|
"""
|
|
112
|
-
result_str =
|
|
212
|
+
result_str = ""
|
|
213
|
+
if capture_args:
|
|
214
|
+
result_str = _safe_repr(result, max_len=max_arg_length, max_depth=max_arg_depth)
|
|
215
|
+
|
|
113
216
|
resp_event = FlowEvent(
|
|
114
|
-
source=target,
|
|
115
|
-
|
|
116
|
-
|
|
217
|
+
source=target,
|
|
218
|
+
target=source,
|
|
219
|
+
action=action,
|
|
220
|
+
message="Return",
|
|
221
|
+
is_return=True,
|
|
222
|
+
result=result_str,
|
|
223
|
+
trace_id=trace_id,
|
|
117
224
|
)
|
|
118
225
|
logger.info(f"{target}->{source}: Return", extra={"flow_event": resp_event})
|
|
119
226
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
227
|
+
|
|
228
|
+
def _log_error(
|
|
229
|
+
logger: logging.Logger,
|
|
230
|
+
source: str,
|
|
231
|
+
target: str,
|
|
232
|
+
action: str,
|
|
233
|
+
error: Exception,
|
|
234
|
+
trace_id: str,
|
|
235
|
+
) -> None:
|
|
126
236
|
"""
|
|
127
237
|
Logs an 'Error' event if the function raises an exception.
|
|
128
238
|
Arrow: target -x source (Error return)
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
logger: Logger instance to use for logging
|
|
242
|
+
source: Name of the original caller
|
|
243
|
+
target: Name of the callee that encountered an error
|
|
244
|
+
action: Name of the action that failed
|
|
245
|
+
error: Exception that was raised
|
|
246
|
+
trace_id: Unique trace identifier for correlation
|
|
129
247
|
"""
|
|
130
248
|
err_event = FlowEvent(
|
|
131
|
-
source=target,
|
|
132
|
-
|
|
133
|
-
|
|
249
|
+
source=target,
|
|
250
|
+
target=source,
|
|
251
|
+
action=action,
|
|
252
|
+
message=str(error),
|
|
253
|
+
is_return=True,
|
|
254
|
+
is_error=True,
|
|
255
|
+
error_message=str(error),
|
|
256
|
+
trace_id=trace_id,
|
|
134
257
|
)
|
|
135
258
|
logger.error(f"{target}-x{source}: Error", extra={"flow_event": err_event})
|
|
136
259
|
|
|
260
|
+
|
|
137
261
|
@overload
|
|
138
|
-
def trace_interaction(func: F) -> F:
|
|
139
|
-
|
|
262
|
+
def trace_interaction(func: F) -> F: ...
|
|
263
|
+
|
|
140
264
|
|
|
141
265
|
@overload
|
|
142
266
|
def trace_interaction(
|
|
143
|
-
*,
|
|
144
|
-
source: Optional[str] = None,
|
|
145
|
-
target: Optional[str] = None,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
267
|
+
*,
|
|
268
|
+
source: Optional[str] = None,
|
|
269
|
+
target: Optional[str] = None,
|
|
270
|
+
name: Optional[str] = None,
|
|
271
|
+
action: Optional[str] = None,
|
|
272
|
+
capture_args: bool = True,
|
|
273
|
+
max_arg_length: int = 50,
|
|
274
|
+
max_arg_depth: int = 1,
|
|
275
|
+
) -> Callable[[F], F]: ...
|
|
276
|
+
|
|
149
277
|
|
|
150
278
|
def trace_interaction(
|
|
151
|
-
func: Optional[F] = None,
|
|
152
|
-
*,
|
|
153
|
-
source: Optional[str] = None,
|
|
154
|
-
target: Optional[str] = None,
|
|
155
|
-
|
|
279
|
+
func: Optional[F] = None,
|
|
280
|
+
*,
|
|
281
|
+
source: Optional[str] = None,
|
|
282
|
+
target: Optional[str] = None,
|
|
283
|
+
name: Optional[str] = None,
|
|
284
|
+
action: Optional[str] = None,
|
|
285
|
+
capture_args: bool = True,
|
|
286
|
+
max_arg_length: int = 50,
|
|
287
|
+
max_arg_depth: int = 1,
|
|
156
288
|
) -> Union[F, Callable[[F], F]]:
|
|
157
289
|
"""
|
|
158
290
|
Main Decorator for tracing function execution in Mermaid diagrams.
|
|
159
|
-
|
|
291
|
+
|
|
292
|
+
This decorator instruments functions to log their execution flow as Mermaid
|
|
293
|
+
sequence diagram events. It supports both synchronous and asynchronous functions,
|
|
294
|
+
and automatically handles context propagation for nested calls.
|
|
295
|
+
|
|
160
296
|
It supports two modes of operation:
|
|
161
|
-
1. **Simple**: `@trace` (No arguments)
|
|
162
|
-
2. **Configured**: `@trace(action="Login", target="AuthService")`
|
|
163
|
-
|
|
297
|
+
1. **Simple**: `@trace` (No arguments) - uses default settings
|
|
298
|
+
2. **Configured**: `@trace(action="Login", target="AuthService")` - customizes behavior
|
|
299
|
+
|
|
164
300
|
Args:
|
|
165
301
|
func: The function being decorated (automatically passed in simple mode).
|
|
166
302
|
source: Explicit name of the caller participant (rarely used, usually inferred from Context).
|
|
167
303
|
target: Explicit name of the callee participant (overrides automatic resolution).
|
|
168
|
-
|
|
304
|
+
name: Alias for 'target' (for clearer API usage).
|
|
305
|
+
action: Label for the arrow (defaults to function name in Title Case).
|
|
306
|
+
capture_args: Whether to include arguments and return values in the log. Default True.
|
|
307
|
+
max_arg_length: Maximum string length for argument/result representation. Default 50.
|
|
308
|
+
max_arg_depth: Maximum recursion depth for argument/result representation. Default 1.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Callable: Either the decorated function (simple mode) or a decorator factory (configured mode)
|
|
169
312
|
"""
|
|
170
|
-
|
|
171
|
-
#
|
|
313
|
+
|
|
314
|
+
# Handle alias - 'name' is an alternative name for 'target'
|
|
315
|
+
final_target = target or name
|
|
316
|
+
|
|
317
|
+
# Mode 1: @trace used without parentheses - directly decorate the function
|
|
172
318
|
if func is not None and callable(func):
|
|
173
|
-
return _create_decorator(
|
|
174
|
-
|
|
175
|
-
|
|
319
|
+
return _create_decorator(
|
|
320
|
+
func,
|
|
321
|
+
source,
|
|
322
|
+
final_target,
|
|
323
|
+
action,
|
|
324
|
+
capture_args,
|
|
325
|
+
max_arg_length,
|
|
326
|
+
max_arg_depth,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Mode 2: @trace(...) used with arguments -> returns a factory that will decorate the function
|
|
176
330
|
def factory(f: F) -> F:
|
|
177
|
-
return _create_decorator(
|
|
331
|
+
return _create_decorator(
|
|
332
|
+
f, source, final_target, action, capture_args, max_arg_length, max_arg_depth
|
|
333
|
+
)
|
|
334
|
+
|
|
178
335
|
return factory
|
|
179
336
|
|
|
337
|
+
|
|
180
338
|
def _create_decorator(
|
|
181
|
-
func: F,
|
|
182
|
-
source: Optional[str],
|
|
183
|
-
target: Optional[str],
|
|
184
|
-
action: Optional[str]
|
|
339
|
+
func: F,
|
|
340
|
+
source: Optional[str],
|
|
341
|
+
target: Optional[str],
|
|
342
|
+
action: Optional[str],
|
|
343
|
+
capture_args: bool,
|
|
344
|
+
max_arg_length: int,
|
|
345
|
+
max_arg_depth: int,
|
|
185
346
|
) -> F:
|
|
186
347
|
"""
|
|
187
|
-
Constructs the actual wrapper function.
|
|
188
|
-
Handles both synchronous and asynchronous functions.
|
|
348
|
+
Constructs the actual wrapper function for the decorated function.
|
|
349
|
+
Handles both synchronous and asynchronous functions by creating the appropriate wrapper.
|
|
350
|
+
|
|
351
|
+
This function separates the wrapper creation logic from the argument parsing logic
|
|
352
|
+
in `trace_interaction`, making the code cleaner and easier to test.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
func: The function to decorate
|
|
356
|
+
source: Explicit source name, if any
|
|
357
|
+
target: Explicit target name, if any
|
|
358
|
+
action: Explicit action name, if any
|
|
359
|
+
capture_args: Whether to capture arguments and return values
|
|
360
|
+
max_arg_length: Maximum length for argument/return value representations
|
|
361
|
+
max_arg_depth: Maximum recursion depth for nested objects
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
Callable: Decorated function with tracing logic
|
|
189
365
|
"""
|
|
190
|
-
|
|
366
|
+
|
|
191
367
|
# Pre-calculate static metadata to save time at runtime
|
|
192
368
|
if action is None:
|
|
369
|
+
# Default action name is the function name, converted to Title Case
|
|
193
370
|
action = func.__name__.replace("_", " ").title()
|
|
194
371
|
|
|
195
372
|
@functools.wraps(func)
|
|
196
373
|
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
197
|
-
"""
|
|
374
|
+
"""Synchronous function wrapper that adds tracing logic."""
|
|
198
375
|
# 1. Resolve Context
|
|
199
376
|
# 'source' is who called us (from Context). 'target' is who we are (resolved from self/cls).
|
|
200
377
|
current_source = source or LogContext.current_participant()
|
|
201
378
|
trace_id = LogContext.current_trace_id()
|
|
202
379
|
current_target = _resolve_target(func, args, target)
|
|
203
|
-
|
|
380
|
+
|
|
204
381
|
logger = get_flow_logger()
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
382
|
+
# Format arguments for the diagram arrow label
|
|
383
|
+
params_str = _format_args(
|
|
384
|
+
args, kwargs, capture_args, max_arg_length, max_arg_depth
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# 2. Log Request (Start of function)
|
|
388
|
+
# Logs the initial "Call" arrow (Source -> Target)
|
|
389
|
+
_log_interaction(
|
|
390
|
+
logger, current_source, current_target, action, params_str, trace_id
|
|
391
|
+
)
|
|
392
|
+
|
|
210
393
|
# 3. Execute with New Context
|
|
211
394
|
# We push 'current_target' as the NEW 'participant' (source) for any internal calls.
|
|
395
|
+
# This builds the chain: A -> B, then inside B, B becomes the source for C (B -> C).
|
|
212
396
|
with LogContext.scope({"participant": current_target, "trace_id": trace_id}):
|
|
213
397
|
try:
|
|
214
398
|
result = func(*args, **kwargs)
|
|
215
399
|
# 4. Log Success Return
|
|
216
|
-
|
|
400
|
+
# Logs the "Return" arrow (Target --> Source)
|
|
401
|
+
_log_return(
|
|
402
|
+
logger,
|
|
403
|
+
current_source,
|
|
404
|
+
current_target,
|
|
405
|
+
action,
|
|
406
|
+
result,
|
|
407
|
+
trace_id,
|
|
408
|
+
capture_args,
|
|
409
|
+
max_arg_length,
|
|
410
|
+
max_arg_depth,
|
|
411
|
+
)
|
|
217
412
|
return result
|
|
218
413
|
except Exception as e:
|
|
219
414
|
# 5. Log Error Return
|
|
415
|
+
# Logs the "Error" arrow (Target --x Source)
|
|
220
416
|
_log_error(logger, current_source, current_target, action, e, trace_id)
|
|
221
417
|
raise
|
|
222
418
|
|
|
223
419
|
@functools.wraps(func)
|
|
224
420
|
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
225
|
-
"""
|
|
421
|
+
"""Asynchronous function wrapper that adds tracing logic."""
|
|
422
|
+
# 1. Resolve Context (Same as sync)
|
|
226
423
|
current_source = source or LogContext.current_participant()
|
|
227
424
|
trace_id = LogContext.current_trace_id()
|
|
228
425
|
current_target = _resolve_target(func, args, target)
|
|
229
|
-
|
|
426
|
+
|
|
230
427
|
logger = get_flow_logger()
|
|
231
|
-
params_str = _format_args(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
428
|
+
params_str = _format_args(
|
|
429
|
+
args, kwargs, capture_args, max_arg_length, max_arg_depth
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
# 2. Log Request
|
|
433
|
+
_log_interaction(
|
|
434
|
+
logger, current_source, current_target, action, params_str, trace_id
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
# 3. Execute with New Context using 'ascope'
|
|
438
|
+
# Use async context manager (ascope) to ensure context propagates correctly across awaits.
|
|
439
|
+
# This is critical for asyncio: context must be preserved even if the task yields control.
|
|
440
|
+
async with LogContext.ascope(
|
|
441
|
+
{"participant": current_target, "trace_id": trace_id}
|
|
442
|
+
):
|
|
238
443
|
try:
|
|
239
444
|
result = await func(*args, **kwargs)
|
|
240
|
-
|
|
445
|
+
# 4. Log Success Return
|
|
446
|
+
_log_return(
|
|
447
|
+
logger,
|
|
448
|
+
current_source,
|
|
449
|
+
current_target,
|
|
450
|
+
action,
|
|
451
|
+
result,
|
|
452
|
+
trace_id,
|
|
453
|
+
capture_args,
|
|
454
|
+
max_arg_length,
|
|
455
|
+
max_arg_depth,
|
|
456
|
+
)
|
|
241
457
|
return result
|
|
242
458
|
except Exception as e:
|
|
459
|
+
# 5. Log Error Return
|
|
243
460
|
_log_error(logger, current_source, current_target, action, e, trace_id)
|
|
244
461
|
raise
|
|
245
462
|
|
|
246
463
|
# Detect if the wrapped function is a coroutine to choose the right wrapper
|
|
247
464
|
if inspect.iscoroutinefunction(func):
|
|
248
|
-
return cast(F, async_wrapper)
|
|
249
|
-
return cast(F, wrapper)
|
|
465
|
+
return cast(F, async_wrapper) # Return async wrapper for coroutines
|
|
466
|
+
return cast(F, wrapper) # Return sync wrapper for regular functions
|
|
467
|
+
|
|
250
468
|
|
|
251
|
-
# Alias for easy import
|
|
469
|
+
# Alias for easy import - 'trace' is the primary name users should use
|
|
252
470
|
trace = trace_interaction
|