mermaid-trace 0.4.0__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 +4 -2
- mermaid_trace/cli.py +88 -46
- mermaid_trace/core/context.py +83 -23
- mermaid_trace/core/decorators.py +112 -22
- mermaid_trace/core/events.py +134 -21
- mermaid_trace/core/formatter.py +92 -22
- mermaid_trace/handlers/async_handler.py +70 -17
- mermaid_trace/handlers/mermaid_handler.py +59 -39
- mermaid_trace/integrations/fastapi.py +54 -33
- {mermaid_trace-0.4.0.dist-info → mermaid_trace-0.4.1.dist-info}/METADATA +23 -8
- mermaid_trace-0.4.1.dist-info/RECORD +16 -0
- mermaid_trace-0.4.0.dist-info/RECORD +0 -16
- {mermaid_trace-0.4.0.dist-info → mermaid_trace-0.4.1.dist-info}/WHEEL +0 -0
- {mermaid_trace-0.4.0.dist-info → mermaid_trace-0.4.1.dist-info}/entry_points.txt +0 -0
- {mermaid_trace-0.4.0.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,6 +19,7 @@ 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
|
|
@@ -14,7 +27,11 @@ F = TypeVar("F", bound=Callable[..., Any])
|
|
|
14
27
|
|
|
15
28
|
|
|
16
29
|
def get_flow_logger() -> logging.Logger:
|
|
17
|
-
"""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
|
+
"""
|
|
18
35
|
return logging.getLogger(FLOW_LOGGER_NAME)
|
|
19
36
|
|
|
20
37
|
|
|
@@ -24,6 +41,14 @@ def _safe_repr(obj: Any, max_len: int = 50, max_depth: int = 1) -> str:
|
|
|
24
41
|
|
|
25
42
|
Prevents massive log files by truncating long strings/objects
|
|
26
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
|
|
27
52
|
"""
|
|
28
53
|
try:
|
|
29
54
|
# Create a custom repr object to control depth and length
|
|
@@ -37,6 +62,7 @@ def _safe_repr(obj: Any, max_len: int = 50, max_depth: int = 1) -> str:
|
|
|
37
62
|
return r[:max_len] + "..."
|
|
38
63
|
return r
|
|
39
64
|
except Exception:
|
|
65
|
+
# Fallback if repr() fails for any reason
|
|
40
66
|
return "<unrepresentable>"
|
|
41
67
|
|
|
42
68
|
|
|
@@ -50,14 +76,27 @@ def _format_args(
|
|
|
50
76
|
"""
|
|
51
77
|
Formats function arguments into a single string "arg1, arg2, k=v".
|
|
52
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
|
|
53
89
|
"""
|
|
54
90
|
if not capture_args:
|
|
55
91
|
return ""
|
|
56
92
|
|
|
57
|
-
parts = []
|
|
93
|
+
parts: list[str] = []
|
|
94
|
+
|
|
95
|
+
# Format positional arguments
|
|
58
96
|
for arg in args:
|
|
59
97
|
parts.append(_safe_repr(arg, max_len=max_arg_length, max_depth=max_arg_depth))
|
|
60
98
|
|
|
99
|
+
# Format keyword arguments
|
|
61
100
|
for k, v in kwargs.items():
|
|
62
101
|
val_str = _safe_repr(v, max_len=max_arg_length, max_depth=max_arg_depth)
|
|
63
102
|
parts.append(f"{k}={val_str}")
|
|
@@ -78,16 +117,22 @@ def _resolve_target(
|
|
|
78
117
|
3. **Class Method**: If the first arg is a type (cls), use the class name.
|
|
79
118
|
4. **Module Function**: Fallback to the name of the module containing the function.
|
|
80
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
|
|
81
128
|
"""
|
|
82
129
|
if target_override:
|
|
83
130
|
return target_override
|
|
84
131
|
|
|
85
|
-
# Heuristic: If it's a method call, args[0] is usually 'self'
|
|
132
|
+
# Heuristic: If it's a method call, args[0] is usually 'self' or 'cls'
|
|
86
133
|
if args:
|
|
87
134
|
first_arg = args[0]
|
|
88
|
-
# Check if it looks like a class instance
|
|
89
|
-
# We check hasattr(__class__) to distinguish objects from primitives/containers broadly,
|
|
90
|
-
# ensuring we don't mislabel a plain list passed as first arg to a function as a "List" participant.
|
|
135
|
+
# Check if it looks like a class instance (not a primitive or container)
|
|
91
136
|
if hasattr(first_arg, "__class__") and not isinstance(
|
|
92
137
|
first_arg, (str, int, float, bool, list, dict, type)
|
|
93
138
|
):
|
|
@@ -113,7 +158,15 @@ def _log_interaction(
|
|
|
113
158
|
) -> None:
|
|
114
159
|
"""
|
|
115
160
|
Logs the 'Call' event (Start of function).
|
|
116
|
-
|
|
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
|
|
117
170
|
"""
|
|
118
171
|
req_event = FlowEvent(
|
|
119
172
|
source=source,
|
|
@@ -144,6 +197,17 @@ def _log_return(
|
|
|
144
197
|
|
|
145
198
|
Note: 'source' here is the original caller, 'target' is the callee.
|
|
146
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
|
|
147
211
|
"""
|
|
148
212
|
result_str = ""
|
|
149
213
|
if capture_args:
|
|
@@ -172,6 +236,14 @@ def _log_error(
|
|
|
172
236
|
"""
|
|
173
237
|
Logs an 'Error' event if the function raises an exception.
|
|
174
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
|
|
175
247
|
"""
|
|
176
248
|
err_event = FlowEvent(
|
|
177
249
|
source=target,
|
|
@@ -217,25 +289,32 @@ def trace_interaction(
|
|
|
217
289
|
"""
|
|
218
290
|
Main Decorator for tracing function execution in Mermaid diagrams.
|
|
219
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
|
+
|
|
220
296
|
It supports two modes of operation:
|
|
221
|
-
1. **Simple**: `@trace` (No arguments)
|
|
222
|
-
2. **Configured**: `@trace(action="Login", target="AuthService")`
|
|
297
|
+
1. **Simple**: `@trace` (No arguments) - uses default settings
|
|
298
|
+
2. **Configured**: `@trace(action="Login", target="AuthService")` - customizes behavior
|
|
223
299
|
|
|
224
300
|
Args:
|
|
225
301
|
func: The function being decorated (automatically passed in simple mode).
|
|
226
302
|
source: Explicit name of the caller participant (rarely used, usually inferred from Context).
|
|
227
303
|
target: Explicit name of the callee participant (overrides automatic resolution).
|
|
228
304
|
name: Alias for 'target' (for clearer API usage).
|
|
229
|
-
action: Label for the arrow (defaults to function name).
|
|
305
|
+
action: Label for the arrow (defaults to function name in Title Case).
|
|
230
306
|
capture_args: Whether to include arguments and return values in the log. Default True.
|
|
231
307
|
max_arg_length: Maximum string length for argument/result representation. Default 50.
|
|
232
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)
|
|
233
312
|
"""
|
|
234
313
|
|
|
235
|
-
# Handle alias
|
|
314
|
+
# Handle alias - 'name' is an alternative name for 'target'
|
|
236
315
|
final_target = target or name
|
|
237
316
|
|
|
238
|
-
# Mode 1: @trace used without parentheses
|
|
317
|
+
# Mode 1: @trace used without parentheses - directly decorate the function
|
|
239
318
|
if func is not None and callable(func):
|
|
240
319
|
return _create_decorator(
|
|
241
320
|
func,
|
|
@@ -247,7 +326,7 @@ def trace_interaction(
|
|
|
247
326
|
max_arg_depth,
|
|
248
327
|
)
|
|
249
328
|
|
|
250
|
-
# Mode 2: @trace(...) used with arguments -> returns a factory
|
|
329
|
+
# Mode 2: @trace(...) used with arguments -> returns a factory that will decorate the function
|
|
251
330
|
def factory(f: F) -> F:
|
|
252
331
|
return _create_decorator(
|
|
253
332
|
f, source, final_target, action, capture_args, max_arg_length, max_arg_depth
|
|
@@ -266,11 +345,23 @@ def _create_decorator(
|
|
|
266
345
|
max_arg_depth: int,
|
|
267
346
|
) -> F:
|
|
268
347
|
"""
|
|
269
|
-
Constructs the actual wrapper function.
|
|
270
|
-
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.
|
|
271
350
|
|
|
272
351
|
This function separates the wrapper creation logic from the argument parsing logic
|
|
273
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
|
|
274
365
|
"""
|
|
275
366
|
|
|
276
367
|
# Pre-calculate static metadata to save time at runtime
|
|
@@ -280,10 +371,9 @@ def _create_decorator(
|
|
|
280
371
|
|
|
281
372
|
@functools.wraps(func)
|
|
282
373
|
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
283
|
-
"""
|
|
374
|
+
"""Synchronous function wrapper that adds tracing logic."""
|
|
284
375
|
# 1. Resolve Context
|
|
285
376
|
# 'source' is who called us (from Context). 'target' is who we are (resolved from self/cls).
|
|
286
|
-
# If 'source' is not explicitly provided, we look up the 'participant' set by the caller.
|
|
287
377
|
current_source = source or LogContext.current_participant()
|
|
288
378
|
trace_id = LogContext.current_trace_id()
|
|
289
379
|
current_target = _resolve_target(func, args, target)
|
|
@@ -294,7 +384,7 @@ def _create_decorator(
|
|
|
294
384
|
args, kwargs, capture_args, max_arg_length, max_arg_depth
|
|
295
385
|
)
|
|
296
386
|
|
|
297
|
-
# 2. Log Request (Start of
|
|
387
|
+
# 2. Log Request (Start of function)
|
|
298
388
|
# Logs the initial "Call" arrow (Source -> Target)
|
|
299
389
|
_log_interaction(
|
|
300
390
|
logger, current_source, current_target, action, params_str, trace_id
|
|
@@ -328,7 +418,7 @@ def _create_decorator(
|
|
|
328
418
|
|
|
329
419
|
@functools.wraps(func)
|
|
330
420
|
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
331
|
-
"""
|
|
421
|
+
"""Asynchronous function wrapper that adds tracing logic."""
|
|
332
422
|
# 1. Resolve Context (Same as sync)
|
|
333
423
|
current_source = source or LogContext.current_participant()
|
|
334
424
|
trace_id = LogContext.current_trace_id()
|
|
@@ -372,9 +462,9 @@ def _create_decorator(
|
|
|
372
462
|
|
|
373
463
|
# Detect if the wrapped function is a coroutine to choose the right wrapper
|
|
374
464
|
if inspect.iscoroutinefunction(func):
|
|
375
|
-
return cast(F, async_wrapper)
|
|
376
|
-
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
|
|
377
467
|
|
|
378
468
|
|
|
379
|
-
# Alias for easy import
|
|
469
|
+
# Alias for easy import - 'trace' is the primary name users should use
|
|
380
470
|
trace = trace_interaction
|
mermaid_trace/core/events.py
CHANGED
|
@@ -1,28 +1,107 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Event Definition Module
|
|
3
|
+
|
|
4
|
+
This module defines the event structure for the tracing system. It provides an
|
|
5
|
+
abstract Event base class and a concrete FlowEvent implementation that represents
|
|
6
|
+
individual interactions in the execution flow.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
1
10
|
from dataclasses import dataclass, field
|
|
2
11
|
import time
|
|
3
12
|
from typing import Optional
|
|
4
13
|
|
|
5
14
|
|
|
15
|
+
class Event(ABC):
|
|
16
|
+
"""
|
|
17
|
+
Abstract base class for all event types.
|
|
18
|
+
|
|
19
|
+
This provides a common interface for different types of events, allowing
|
|
20
|
+
for extensibility and supporting multiple output formats. Concrete event
|
|
21
|
+
classes must implement all abstract methods.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def get_source(self) -> str:
|
|
26
|
+
"""
|
|
27
|
+
Get the source of the event.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
str: Name of the participant that generated the event
|
|
31
|
+
"""
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def get_target(self) -> str:
|
|
36
|
+
"""
|
|
37
|
+
Get the target of the event.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
str: Name of the participant that received the event
|
|
41
|
+
"""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def get_action(self) -> str:
|
|
46
|
+
"""
|
|
47
|
+
Get the action name of the event.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
str: Short name describing the action performed
|
|
51
|
+
"""
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def get_message(self) -> str:
|
|
56
|
+
"""
|
|
57
|
+
Get the message text of the event.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
str: Detailed message describing the event
|
|
61
|
+
"""
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
@abstractmethod
|
|
65
|
+
def get_timestamp(self) -> float:
|
|
66
|
+
"""
|
|
67
|
+
Get the timestamp of the event.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
float: Unix timestamp (seconds) when the event occurred
|
|
71
|
+
"""
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
def get_trace_id(self) -> str:
|
|
76
|
+
"""
|
|
77
|
+
Get the trace ID of the event.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
str: Unique identifier for the trace session
|
|
81
|
+
"""
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
|
|
6
85
|
@dataclass
|
|
7
|
-
class FlowEvent:
|
|
86
|
+
class FlowEvent(Event):
|
|
8
87
|
"""
|
|
9
88
|
Represents a single interaction or step in the execution flow.
|
|
10
89
|
|
|
11
90
|
This data structure acts as the intermediate representation (IR) between
|
|
12
|
-
runtime code execution and the final
|
|
91
|
+
runtime code execution and the final diagram output. Each instance
|
|
13
92
|
corresponds directly to one arrow or note in the sequence diagram.
|
|
14
93
|
|
|
15
|
-
The fields map to
|
|
94
|
+
The fields map to diagram syntax components as follows:
|
|
16
95
|
`source` -> `target`: `message`
|
|
17
96
|
|
|
18
97
|
Attributes:
|
|
19
98
|
source (str):
|
|
20
99
|
The name of the participant initiating the action (the "Caller").
|
|
21
|
-
In
|
|
100
|
+
In sequence diagrams: The participant on the LEFT side of the arrow.
|
|
22
101
|
|
|
23
102
|
target (str):
|
|
24
103
|
The name of the participant receiving the action (the "Callee").
|
|
25
|
-
In
|
|
104
|
+
In sequence diagrams: The participant on the RIGHT side of the arrow.
|
|
26
105
|
|
|
27
106
|
action (str):
|
|
28
107
|
A short, human-readable name for the operation (e.g., function name).
|
|
@@ -34,8 +113,8 @@ class FlowEvent:
|
|
|
34
113
|
|
|
35
114
|
timestamp (float):
|
|
36
115
|
Unix timestamp (seconds) of when the event occurred.
|
|
37
|
-
Used for ordering events if logs are processed asynchronously
|
|
38
|
-
|
|
116
|
+
Used for ordering events if logs are processed asynchronously.
|
|
117
|
+
Defaults to current time when event is created.
|
|
39
118
|
|
|
40
119
|
trace_id (str):
|
|
41
120
|
Unique identifier for the trace session.
|
|
@@ -44,34 +123,68 @@ class FlowEvent:
|
|
|
44
123
|
|
|
45
124
|
is_return (bool):
|
|
46
125
|
Flag indicating if this is a response arrow.
|
|
47
|
-
If True, the arrow is drawn as a dotted line
|
|
48
|
-
If False, it is a solid line
|
|
126
|
+
If True, the arrow is drawn as a dotted line in sequence diagrams.
|
|
127
|
+
If False, it is a solid line representing a call.
|
|
128
|
+
Defaults to False.
|
|
49
129
|
|
|
50
130
|
is_error (bool):
|
|
51
131
|
Flag indicating if an exception occurred.
|
|
52
|
-
If True, the arrow might be styled differently
|
|
132
|
+
If True, the arrow might be styled differently to show failure.
|
|
133
|
+
Defaults to False.
|
|
53
134
|
|
|
54
135
|
error_message (Optional[str]):
|
|
55
136
|
Detailed error text if `is_error` is True.
|
|
56
137
|
Can be added as a note or included in the arrow label.
|
|
138
|
+
Defaults to None.
|
|
57
139
|
|
|
58
140
|
params (Optional[str]):
|
|
59
141
|
Stringified representation of function arguments.
|
|
60
142
|
Captured only for request events (call start).
|
|
143
|
+
Defaults to None.
|
|
61
144
|
|
|
62
145
|
result (Optional[str]):
|
|
63
146
|
Stringified representation of the return value.
|
|
64
147
|
Captured only for return events (call end).
|
|
148
|
+
Defaults to None.
|
|
65
149
|
"""
|
|
66
150
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
151
|
+
# Required fields for every event
|
|
152
|
+
source: str # Participant who initiated the action
|
|
153
|
+
target: str # Participant who received the action
|
|
154
|
+
action: str # Short name for the operation
|
|
155
|
+
message: str # Detailed message for the diagram arrow
|
|
156
|
+
trace_id: str # Unique identifier for the trace session
|
|
157
|
+
|
|
158
|
+
# Optional fields with defaults
|
|
159
|
+
timestamp: float = field(
|
|
160
|
+
default_factory=time.time
|
|
161
|
+
) # Unix timestamp of event creation
|
|
162
|
+
is_return: bool = False # Whether this is a response arrow
|
|
163
|
+
is_error: bool = False # Whether an error occurred
|
|
164
|
+
error_message: Optional[str] = None # Detailed error message if is_error is True
|
|
165
|
+
params: Optional[str] = None # Stringified function arguments
|
|
166
|
+
result: Optional[str] = None # Stringified return value
|
|
167
|
+
|
|
168
|
+
def get_source(self) -> str:
|
|
169
|
+
"""Get the source of the event."""
|
|
170
|
+
return self.source
|
|
171
|
+
|
|
172
|
+
def get_target(self) -> str:
|
|
173
|
+
"""Get the target of the event."""
|
|
174
|
+
return self.target
|
|
175
|
+
|
|
176
|
+
def get_action(self) -> str:
|
|
177
|
+
"""Get the action name of the event."""
|
|
178
|
+
return self.action
|
|
179
|
+
|
|
180
|
+
def get_message(self) -> str:
|
|
181
|
+
"""Get the message text of the event."""
|
|
182
|
+
return self.message
|
|
183
|
+
|
|
184
|
+
def get_timestamp(self) -> float:
|
|
185
|
+
"""Get the timestamp of the event."""
|
|
186
|
+
return self.timestamp
|
|
187
|
+
|
|
188
|
+
def get_trace_id(self) -> str:
|
|
189
|
+
"""Get the trace ID of the event."""
|
|
190
|
+
return self.trace_id
|
mermaid_trace/core/formatter.py
CHANGED
|
@@ -1,81 +1,151 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Event Formatting Module
|
|
3
|
+
|
|
4
|
+
This module provides formatters to convert Event objects into various output formats.
|
|
5
|
+
Currently, it supports Mermaid sequence diagram syntax formatting, but can be extended
|
|
6
|
+
with additional formatters for other diagram types or logging formats.
|
|
7
|
+
"""
|
|
8
|
+
|
|
1
9
|
import logging
|
|
2
10
|
import re
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
3
12
|
from typing import Optional
|
|
4
|
-
from .events import FlowEvent
|
|
13
|
+
from .events import Event, FlowEvent
|
|
5
14
|
|
|
6
15
|
|
|
7
|
-
class
|
|
16
|
+
class BaseFormatter(ABC, logging.Formatter):
|
|
8
17
|
"""
|
|
9
|
-
|
|
18
|
+
Abstract base class for all event formatters.
|
|
19
|
+
|
|
20
|
+
This provides a common interface for different formatters, allowing
|
|
21
|
+
for extensibility and supporting multiple output formats.
|
|
22
|
+
|
|
23
|
+
Subclasses must implement the format_event method to convert Event objects
|
|
24
|
+
into the desired output string format.
|
|
10
25
|
"""
|
|
11
26
|
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def format_event(self, event: Event) -> str:
|
|
29
|
+
"""
|
|
30
|
+
Format an Event into the desired output string.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
event: The Event object to format
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
str: Formatted string representation of the event
|
|
37
|
+
"""
|
|
38
|
+
pass
|
|
39
|
+
|
|
12
40
|
def format(self, record: logging.LogRecord) -> str:
|
|
13
|
-
|
|
14
|
-
|
|
41
|
+
"""
|
|
42
|
+
Format a logging record containing an event.
|
|
43
|
+
|
|
44
|
+
This method overrides the standard logging.Formatter.format method to
|
|
45
|
+
extract and format Event objects from log records.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
record: The logging record to format. Must contain a 'flow_event' attribute
|
|
49
|
+
if it represents a tracing event.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
str: Formatted string representation of the record
|
|
53
|
+
"""
|
|
54
|
+
# Retrieve the Event object from the log record
|
|
55
|
+
event: Optional[Event] = getattr(record, "flow_event", None)
|
|
15
56
|
|
|
16
57
|
if not event:
|
|
17
58
|
# Fallback for standard logs if they accidentally reach this handler
|
|
18
59
|
return super().format(record)
|
|
19
60
|
|
|
20
|
-
#
|
|
21
|
-
return self.
|
|
61
|
+
# Convert event to the desired format using the subclass's format_event method
|
|
62
|
+
return self.format_event(event)
|
|
22
63
|
|
|
23
|
-
|
|
64
|
+
|
|
65
|
+
class MermaidFormatter(BaseFormatter):
|
|
66
|
+
"""
|
|
67
|
+
Custom formatter to convert Events into Mermaid sequence diagram syntax.
|
|
68
|
+
|
|
69
|
+
This formatter transforms FlowEvent objects into lines of Mermaid syntax that
|
|
70
|
+
can be directly written to a .mmd file. Each event becomes a single line in the
|
|
71
|
+
sequence diagram.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def format_event(self, event: Event) -> str:
|
|
24
75
|
"""
|
|
25
|
-
Converts
|
|
76
|
+
Converts an Event into a Mermaid syntax string.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
event: The Event object to format
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
str: Mermaid syntax string representation of the event
|
|
26
83
|
"""
|
|
84
|
+
if not isinstance(event, FlowEvent):
|
|
85
|
+
# Fallback format for non-FlowEvent types
|
|
86
|
+
return f"{event.get_source()}->>{event.get_target()}: {event.get_message()}"
|
|
87
|
+
|
|
27
88
|
# Sanitize participant names to avoid syntax errors in Mermaid
|
|
28
89
|
src = self._sanitize(event.source)
|
|
29
90
|
tgt = self._sanitize(event.target)
|
|
30
91
|
|
|
31
|
-
# Determine arrow type
|
|
92
|
+
# Determine arrow type based on event properties
|
|
32
93
|
# ->> : Solid line with arrowhead (synchronous call)
|
|
33
94
|
# -->> : Dotted line with arrowhead (return)
|
|
34
95
|
# --x : Dotted line with cross (error)
|
|
35
96
|
arrow = "-->>" if event.is_return else "->>"
|
|
36
97
|
|
|
98
|
+
# Construct message text based on event type
|
|
37
99
|
msg = ""
|
|
38
100
|
if event.is_error:
|
|
39
101
|
arrow = "--x"
|
|
40
102
|
msg = f"Error: {event.error_message}"
|
|
41
103
|
elif event.is_return:
|
|
42
|
-
# For
|
|
104
|
+
# For return events, show return value or just "Return"
|
|
43
105
|
msg = f"Return: {event.result}" if event.result else "Return"
|
|
44
106
|
else:
|
|
45
|
-
# For
|
|
107
|
+
# For call events, show Action(Params) or just Action
|
|
46
108
|
msg = f"{event.message}({event.params})" if event.params else event.message
|
|
47
109
|
|
|
48
|
-
#
|
|
49
|
-
# For now, we just output the interaction.
|
|
50
|
-
|
|
51
|
-
# Escape message for Mermaid safety (e.g. replacing newlines)
|
|
110
|
+
# Escape message for Mermaid safety (e.g., replacing newlines)
|
|
52
111
|
msg = self._escape_message(msg)
|
|
53
112
|
|
|
113
|
+
# Return the complete Mermaid syntax line
|
|
54
114
|
# Format: Source->>Target: Message
|
|
55
115
|
return f"{src}{arrow}{tgt}: {msg}"
|
|
56
116
|
|
|
57
117
|
def _sanitize(self, name: str) -> str:
|
|
58
118
|
"""
|
|
59
119
|
Sanitizes participant names to be valid Mermaid identifiers.
|
|
60
|
-
Allows alphanumeric and underscores. Replaces others.
|
|
61
120
|
|
|
62
121
|
Mermaid doesn't like spaces or special characters in participant aliases
|
|
63
122
|
unless they are quoted (which we are not doing here for simplicity),
|
|
64
123
|
so we replace them with underscores.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
name: Original participant name
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
str: Sanitized participant name
|
|
65
130
|
"""
|
|
66
131
|
# Replace any non-alphanumeric character (except underscore) with underscore
|
|
67
132
|
clean_name = re.sub(r"[^a-zA-Z0-9_]", "_", name)
|
|
68
|
-
# Ensure it doesn't start with a digit (Mermaid doesn't like that sometimes
|
|
133
|
+
# Ensure it doesn't start with a digit (Mermaid doesn't like that sometimes)
|
|
69
134
|
if clean_name and clean_name[0].isdigit():
|
|
70
135
|
clean_name = "_" + clean_name
|
|
71
136
|
return clean_name
|
|
72
137
|
|
|
73
138
|
def _escape_message(self, msg: str) -> str:
|
|
74
139
|
"""
|
|
75
|
-
Escapes special characters in the message text.
|
|
76
|
-
|
|
140
|
+
Escapes special characters in the message text for safe Mermaid rendering.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
msg: Original message text
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
str: Escaped message text
|
|
77
147
|
"""
|
|
78
|
-
# Replace newlines with <br/> for Mermaid
|
|
148
|
+
# Replace newlines with <br/> for proper display in Mermaid diagrams
|
|
79
149
|
msg = msg.replace("\n", "<br/>")
|
|
80
|
-
#
|
|
150
|
+
# Additional escaping could be added here if needed for other characters
|
|
81
151
|
return msg
|