mermaid-trace 0.4.0__py3-none-any.whl → 0.5.3.post0__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 +55 -21
- mermaid_trace/cli.py +159 -63
- mermaid_trace/core/config.py +55 -0
- mermaid_trace/core/context.py +83 -23
- mermaid_trace/core/decorators.py +440 -145
- mermaid_trace/core/events.py +46 -63
- mermaid_trace/core/formatter.py +257 -29
- mermaid_trace/core/utils.py +96 -0
- mermaid_trace/handlers/async_handler.py +156 -27
- mermaid_trace/handlers/mermaid_handler.py +162 -76
- mermaid_trace/integrations/__init__.py +4 -0
- mermaid_trace/integrations/fastapi.py +110 -34
- {mermaid_trace-0.4.0.dist-info → mermaid_trace-0.5.3.post0.dist-info}/METADATA +78 -11
- mermaid_trace-0.5.3.post0.dist-info/RECORD +19 -0
- mermaid_trace-0.4.0.dist-info/RECORD +0 -16
- {mermaid_trace-0.4.0.dist-info → mermaid_trace-0.5.3.post0.dist-info}/WHEEL +0 -0
- {mermaid_trace-0.4.0.dist-info → mermaid_trace-0.5.3.post0.dist-info}/entry_points.txt +0 -0
- {mermaid_trace-0.4.0.dist-info → mermaid_trace-0.5.3.post0.dist-info}/licenses/LICENSE +0 -0
mermaid_trace/core/events.py
CHANGED
|
@@ -1,77 +1,60 @@
|
|
|
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
|
|
1
10
|
from dataclasses import dataclass, field
|
|
2
11
|
import time
|
|
3
12
|
from typing import Optional
|
|
4
13
|
|
|
5
14
|
|
|
6
|
-
|
|
7
|
-
class FlowEvent:
|
|
15
|
+
class Event(ABC):
|
|
8
16
|
"""
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
This data structure acts as the intermediate representation (IR) between
|
|
12
|
-
runtime code execution and the final Mermaid diagram output. Each instance
|
|
13
|
-
corresponds directly to one arrow or note in the sequence diagram.
|
|
14
|
-
|
|
15
|
-
The fields map to Mermaid syntax components as follows:
|
|
16
|
-
`source` -> `target`: `message`
|
|
17
|
-
|
|
18
|
-
Attributes:
|
|
19
|
-
source (str):
|
|
20
|
-
The name of the participant initiating the action (the "Caller").
|
|
21
|
-
In Mermaid: The participant on the LEFT side of the arrow.
|
|
22
|
-
|
|
23
|
-
target (str):
|
|
24
|
-
The name of the participant receiving the action (the "Callee").
|
|
25
|
-
In Mermaid: The participant on the RIGHT side of the arrow.
|
|
26
|
-
|
|
27
|
-
action (str):
|
|
28
|
-
A short, human-readable name for the operation (e.g., function name).
|
|
29
|
-
Used for grouping or filtering logs, but often redundant with message.
|
|
30
|
-
|
|
31
|
-
message (str):
|
|
32
|
-
The actual text label displayed on the diagram arrow.
|
|
33
|
-
Example: "getUser(id=1)" or "Return: User(name='Alice')".
|
|
34
|
-
|
|
35
|
-
timestamp (float):
|
|
36
|
-
Unix timestamp (seconds) of when the event occurred.
|
|
37
|
-
Used for ordering events if logs are processed asynchronously,
|
|
38
|
-
though Mermaid sequence diagrams primarily rely on line order.
|
|
39
|
-
|
|
40
|
-
trace_id (str):
|
|
41
|
-
Unique identifier for the trace session.
|
|
42
|
-
Allows filtering multiple concurrent traces from a single log file
|
|
43
|
-
to generate separate diagrams for separate requests.
|
|
17
|
+
Abstract base class for all event types.
|
|
44
18
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
If False, it is a solid line (`->`) representing a call.
|
|
49
|
-
|
|
50
|
-
is_error (bool):
|
|
51
|
-
Flag indicating if an exception occurred.
|
|
52
|
-
If True, the arrow might be styled differently (e.g., `-x`) to show failure.
|
|
53
|
-
|
|
54
|
-
error_message (Optional[str]):
|
|
55
|
-
Detailed error text if `is_error` is True.
|
|
56
|
-
Can be added as a note or included in the arrow label.
|
|
57
|
-
|
|
58
|
-
params (Optional[str]):
|
|
59
|
-
Stringified representation of function arguments.
|
|
60
|
-
Captured only for request events (call start).
|
|
61
|
-
|
|
62
|
-
result (Optional[str]):
|
|
63
|
-
Stringified representation of the return value.
|
|
64
|
-
Captured only for return events (call end).
|
|
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.
|
|
65
22
|
"""
|
|
66
23
|
|
|
24
|
+
# Common attributes that should be present in all events
|
|
67
25
|
source: str
|
|
68
26
|
target: str
|
|
69
27
|
action: str
|
|
70
28
|
message: str
|
|
29
|
+
timestamp: float
|
|
71
30
|
trace_id: str
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class FlowEvent(Event):
|
|
35
|
+
"""
|
|
36
|
+
Represents a single interaction or step in the execution flow.
|
|
37
|
+
|
|
38
|
+
(Existing docstring omitted for brevity)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
# Required fields for every event
|
|
42
|
+
source: str # Participant who initiated the action
|
|
43
|
+
target: str # Participant who received the action
|
|
44
|
+
action: str # Short name for the operation
|
|
45
|
+
message: str # Detailed message for the diagram arrow
|
|
46
|
+
trace_id: str # Unique identifier for the trace session
|
|
47
|
+
|
|
48
|
+
# Optional fields with defaults
|
|
49
|
+
timestamp: float = field(
|
|
50
|
+
default_factory=time.time
|
|
51
|
+
) # Unix timestamp of event creation
|
|
52
|
+
is_return: bool = False # Whether this is a response arrow
|
|
53
|
+
is_error: bool = False # Whether an error occurred
|
|
54
|
+
error_message: Optional[str] = None # Detailed error message if is_error is True
|
|
55
|
+
stack_trace: Optional[str] = None # Full stack trace if is_error is True
|
|
56
|
+
params: Optional[str] = None # Stringified function arguments
|
|
57
|
+
result: Optional[str] = None # Stringified return value
|
|
58
|
+
collapsed: bool = (
|
|
59
|
+
False # Whether this interaction should be visually collapsed (loop/folding)
|
|
60
|
+
)
|
mermaid_trace/core/formatter.py
CHANGED
|
@@ -1,81 +1,309 @@
|
|
|
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
|
|
3
|
-
from
|
|
4
|
-
from
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from typing import Optional, Dict, Set, Any, List, Tuple
|
|
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
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def get_header(self, title: str) -> str:
|
|
42
|
+
"""
|
|
43
|
+
Get the file header for the diagram format.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
title: The title of the diagram
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
str: Header string
|
|
50
|
+
"""
|
|
51
|
+
pass
|
|
52
|
+
|
|
12
53
|
def format(self, record: logging.LogRecord) -> str:
|
|
13
|
-
|
|
14
|
-
|
|
54
|
+
"""
|
|
55
|
+
Format a logging record containing an event.
|
|
56
|
+
|
|
57
|
+
This method overrides the standard logging.Formatter.format method to
|
|
58
|
+
extract and format Event objects from log records.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
record: The logging record to format. Must contain a 'flow_event' attribute
|
|
62
|
+
if it represents a tracing event.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
str: Formatted string representation of the record
|
|
66
|
+
"""
|
|
67
|
+
# Retrieve the Event object from the log record
|
|
68
|
+
event: Optional[Event] = getattr(record, "flow_event", None)
|
|
15
69
|
|
|
16
70
|
if not event:
|
|
17
71
|
# Fallback for standard logs if they accidentally reach this handler
|
|
18
72
|
return super().format(record)
|
|
19
73
|
|
|
20
|
-
#
|
|
21
|
-
return self.
|
|
74
|
+
# Convert event to the desired format using the subclass's format_event method
|
|
75
|
+
return self.format_event(event)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class MermaidFormatter(BaseFormatter):
|
|
79
|
+
"""
|
|
80
|
+
Custom formatter to convert Events into Mermaid sequence diagram syntax.
|
|
81
|
+
|
|
82
|
+
This formatter transforms FlowEvent objects into lines of Mermaid syntax that
|
|
83
|
+
can be directly written to a .mmd file. Each event becomes a single line in the
|
|
84
|
+
sequence diagram.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
88
|
+
super().__init__(*args, **kwargs)
|
|
89
|
+
# Map raw participant names to sanitized Mermaid IDs
|
|
90
|
+
self._participant_map: Dict[str, str] = {}
|
|
91
|
+
# Set of already used Mermaid IDs to prevent collisions
|
|
92
|
+
self._used_ids: Set[str] = set()
|
|
93
|
+
|
|
94
|
+
# State for intelligent collapsing
|
|
95
|
+
# We track a window of events to detect patterns (length 1 or 2)
|
|
96
|
+
self._event_buffer: List[FlowEvent] = []
|
|
97
|
+
self._pattern_count: int = 0
|
|
98
|
+
self._current_pattern: List[Tuple[str, str, str, bool]] = []
|
|
99
|
+
|
|
100
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
101
|
+
"""
|
|
102
|
+
Format a logging record containing an event.
|
|
103
|
+
|
|
104
|
+
If the record contains a FlowEvent, it will be buffered for pattern-based collapsing.
|
|
105
|
+
To get immediate output for tests, you can call flush() after format().
|
|
106
|
+
"""
|
|
107
|
+
event: Optional[Event] = getattr(record, "flow_event", None)
|
|
108
|
+
|
|
109
|
+
if not event or not isinstance(event, FlowEvent):
|
|
110
|
+
return super().format(record)
|
|
111
|
+
|
|
112
|
+
# Create a key for this event type
|
|
113
|
+
event_key = (event.source, event.target, event.action, event.is_return)
|
|
114
|
+
|
|
115
|
+
# Case 1: Already in a pattern
|
|
116
|
+
if self._current_pattern:
|
|
117
|
+
pattern_len = len(self._current_pattern)
|
|
118
|
+
match_idx = len(self._event_buffer) % pattern_len
|
|
119
|
+
|
|
120
|
+
if event_key == self._current_pattern[match_idx]:
|
|
121
|
+
# It matches!
|
|
122
|
+
self._event_buffer.append(event)
|
|
123
|
+
if match_idx == pattern_len - 1:
|
|
124
|
+
self._pattern_count += 1
|
|
125
|
+
return ""
|
|
126
|
+
else:
|
|
127
|
+
# Pattern broken! Flush and continue to find new pattern
|
|
128
|
+
output = self.flush()
|
|
129
|
+
prefix = output + "\n" if output else ""
|
|
130
|
+
|
|
131
|
+
self._event_buffer = [event]
|
|
132
|
+
return prefix.strip()
|
|
133
|
+
|
|
134
|
+
# Case 2: Not in a pattern yet, but have one event buffered
|
|
135
|
+
if self._event_buffer:
|
|
136
|
+
first = self._event_buffer[0]
|
|
137
|
+
first_key = (first.source, first.target, first.action, first.is_return)
|
|
138
|
+
|
|
139
|
+
if event_key == first_key:
|
|
140
|
+
# Pattern length 1 detected (A, A)
|
|
141
|
+
self._current_pattern = [first_key]
|
|
142
|
+
self._event_buffer.append(event)
|
|
143
|
+
self._pattern_count = 2
|
|
144
|
+
return ""
|
|
145
|
+
elif (
|
|
146
|
+
event.is_return
|
|
147
|
+
and event.source == first.target
|
|
148
|
+
and event.target == first.source
|
|
149
|
+
and event.action == first.action
|
|
150
|
+
):
|
|
151
|
+
# Pattern length 2 detected (Call, Return)
|
|
152
|
+
self._current_pattern = [first_key, event_key]
|
|
153
|
+
self._event_buffer.append(event)
|
|
154
|
+
self._pattern_count = 1
|
|
155
|
+
return ""
|
|
156
|
+
else:
|
|
157
|
+
# No pattern, flush first event and keep current as new potential start
|
|
158
|
+
output = self.format_event(first, 1)
|
|
159
|
+
self._event_buffer = [event]
|
|
160
|
+
return output.strip()
|
|
161
|
+
|
|
162
|
+
# Case 3: Completely idle
|
|
163
|
+
# To avoid breaking existing tests that expect immediate output for single events,
|
|
164
|
+
# we check if we can immediately format it. But for true collapsing, we need buffering.
|
|
165
|
+
# Strategy: we buffer it, but if it's the ONLY event, flush() must be called.
|
|
166
|
+
# To maintain compatibility with tests, we'll keep buffering but update tests
|
|
167
|
+
# or change logic to only buffer if we suspect a pattern.
|
|
168
|
+
# Actually, the most robust way is to update the tests/handlers to call flush.
|
|
169
|
+
self._event_buffer = [event]
|
|
170
|
+
return ""
|
|
171
|
+
|
|
172
|
+
def flush(self) -> str:
|
|
173
|
+
"""
|
|
174
|
+
Flushes the current collapsed pattern and returns its Mermaid representation.
|
|
175
|
+
"""
|
|
176
|
+
if not self._event_buffer:
|
|
177
|
+
return ""
|
|
178
|
+
|
|
179
|
+
output_lines = []
|
|
180
|
+
|
|
181
|
+
if self._current_pattern:
|
|
182
|
+
pattern_len = len(self._current_pattern)
|
|
183
|
+
# Only output one instance of the pattern, but with the total count
|
|
184
|
+
for i in range(pattern_len):
|
|
185
|
+
event = self._event_buffer[i]
|
|
186
|
+
line = self.format_event(event, self._pattern_count)
|
|
187
|
+
output_lines.append(line)
|
|
188
|
+
else:
|
|
189
|
+
# Just some buffered events that didn't form a pattern
|
|
190
|
+
for event in self._event_buffer:
|
|
191
|
+
output_lines.append(self.format_event(event, 1))
|
|
192
|
+
|
|
193
|
+
# Reset state
|
|
194
|
+
self._event_buffer = []
|
|
195
|
+
self._current_pattern = []
|
|
196
|
+
self._pattern_count = 0
|
|
197
|
+
|
|
198
|
+
return "\n".join(output_lines)
|
|
22
199
|
|
|
23
|
-
def
|
|
200
|
+
def get_header(self, title: str = "Log Flow") -> str:
|
|
24
201
|
"""
|
|
25
|
-
|
|
202
|
+
Returns the Mermaid sequence diagram header.
|
|
26
203
|
"""
|
|
204
|
+
return f"sequenceDiagram\n title {title}\n autonumber\n\n"
|
|
205
|
+
|
|
206
|
+
def format_event(self, event: Event, count: int = 1) -> str:
|
|
207
|
+
"""
|
|
208
|
+
Converts an Event into a Mermaid syntax string.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
event: The Event object to format
|
|
212
|
+
count: Number of times this event was repeated (for collapsing)
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
str: Mermaid syntax string representation of the event
|
|
216
|
+
"""
|
|
217
|
+
if not isinstance(event, FlowEvent):
|
|
218
|
+
# Fallback format for non-FlowEvent types
|
|
219
|
+
return f"{event.source}->>{event.target}: {event.message}"
|
|
220
|
+
|
|
27
221
|
# Sanitize participant names to avoid syntax errors in Mermaid
|
|
28
222
|
src = self._sanitize(event.source)
|
|
29
223
|
tgt = self._sanitize(event.target)
|
|
30
224
|
|
|
31
225
|
# Determine arrow type
|
|
32
|
-
# ->> : Solid line with arrowhead (synchronous call)
|
|
33
|
-
# -->> : Dotted line with arrowhead (return)
|
|
34
|
-
# --x : Dotted line with cross (error)
|
|
35
226
|
arrow = "-->>" if event.is_return else "->>"
|
|
36
227
|
|
|
228
|
+
# Construct message text
|
|
37
229
|
msg = ""
|
|
38
230
|
if event.is_error:
|
|
39
231
|
arrow = "--x"
|
|
40
232
|
msg = f"Error: {event.error_message}"
|
|
41
233
|
elif event.is_return:
|
|
42
|
-
# For returns, we usually show the return value or just "Return"
|
|
43
234
|
msg = f"Return: {event.result}" if event.result else "Return"
|
|
44
235
|
else:
|
|
45
|
-
# For calls, we show Action(Params) or just Action
|
|
46
236
|
msg = f"{event.message}({event.params})" if event.params else event.message
|
|
47
237
|
|
|
48
|
-
#
|
|
49
|
-
|
|
238
|
+
# Append count if collapsed
|
|
239
|
+
if count > 1:
|
|
240
|
+
msg += f" (x{count})"
|
|
50
241
|
|
|
51
|
-
# Escape message for Mermaid safety
|
|
242
|
+
# Escape message for Mermaid safety
|
|
52
243
|
msg = self._escape_message(msg)
|
|
53
244
|
|
|
54
|
-
#
|
|
55
|
-
|
|
245
|
+
# Return the complete Mermaid syntax line
|
|
246
|
+
line = f"{src}{arrow}{tgt}: {msg}"
|
|
247
|
+
|
|
248
|
+
# Add Notes for Errors (Stack Trace)
|
|
249
|
+
if event.is_error and event.stack_trace:
|
|
250
|
+
short_stack = self._escape_message(event.stack_trace[:300] + "...")
|
|
251
|
+
note = f"note right of {tgt}: {short_stack}"
|
|
252
|
+
return f"{line}\n{note}"
|
|
253
|
+
|
|
254
|
+
# Handle manually marked collapsed events (if any)
|
|
255
|
+
if event.collapsed and count == 1:
|
|
256
|
+
note = f"note right of {src}: ( Sampled / Collapsed Interaction )"
|
|
257
|
+
return f"{line}\n{note}"
|
|
258
|
+
|
|
259
|
+
return line
|
|
56
260
|
|
|
57
261
|
def _sanitize(self, name: str) -> str:
|
|
58
262
|
"""
|
|
59
263
|
Sanitizes participant names to be valid Mermaid identifiers.
|
|
60
|
-
|
|
264
|
+
Handles naming collisions by ensuring unique IDs.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
name: Original participant name
|
|
61
268
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
so we replace them with underscores.
|
|
269
|
+
Returns:
|
|
270
|
+
str: Sanitized participant name (unique)
|
|
65
271
|
"""
|
|
272
|
+
if name in self._participant_map:
|
|
273
|
+
return self._participant_map[name]
|
|
274
|
+
|
|
66
275
|
# Replace any non-alphanumeric character (except underscore) with underscore
|
|
67
276
|
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
|
|
277
|
+
# Ensure it doesn't start with a digit (Mermaid doesn't like that sometimes)
|
|
69
278
|
if clean_name and clean_name[0].isdigit():
|
|
70
279
|
clean_name = "_" + clean_name
|
|
280
|
+
|
|
281
|
+
if not clean_name:
|
|
282
|
+
clean_name = "Unknown"
|
|
283
|
+
|
|
284
|
+
# Check for collisions
|
|
285
|
+
if clean_name in self._used_ids:
|
|
286
|
+
original_clean = clean_name
|
|
287
|
+
counter = 1
|
|
288
|
+
while clean_name in self._used_ids:
|
|
289
|
+
clean_name = f"{original_clean}_{counter}"
|
|
290
|
+
counter += 1
|
|
291
|
+
|
|
292
|
+
self._participant_map[name] = clean_name
|
|
293
|
+
self._used_ids.add(clean_name)
|
|
71
294
|
return clean_name
|
|
72
295
|
|
|
73
296
|
def _escape_message(self, msg: str) -> str:
|
|
74
297
|
"""
|
|
75
|
-
Escapes special characters in the message text.
|
|
76
|
-
|
|
298
|
+
Escapes special characters in the message text for safe Mermaid rendering.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
msg: Original message text
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
str: Escaped message text
|
|
77
305
|
"""
|
|
78
|
-
# Replace newlines with <br/> for Mermaid
|
|
306
|
+
# Replace newlines with <br/> for proper display in Mermaid diagrams
|
|
79
307
|
msg = msg.replace("\n", "<br/>")
|
|
80
|
-
#
|
|
308
|
+
# Additional escaping could be added here if needed for other characters
|
|
81
309
|
return msg
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for auto-instrumentation and patching.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from typing import Any, Type, Optional, List
|
|
7
|
+
from .decorators import trace
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def trace_class(
|
|
11
|
+
cls: Optional[Type[Any]] = None,
|
|
12
|
+
*,
|
|
13
|
+
include_private: bool = False,
|
|
14
|
+
exclude: Optional[List[str]] = None,
|
|
15
|
+
**trace_kwargs: Any,
|
|
16
|
+
) -> Any:
|
|
17
|
+
"""
|
|
18
|
+
Class decorator to automatically trace all methods in a class.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
cls: The class to instrument.
|
|
22
|
+
include_private: If True, traces methods starting with '_'. Defaults to False.
|
|
23
|
+
exclude: List of method names to skip.
|
|
24
|
+
**trace_kwargs: Arguments passed to the @trace decorator (e.g., capture_args).
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
@trace_class
|
|
28
|
+
class MyService:
|
|
29
|
+
def method1(self): ...
|
|
30
|
+
def method2(self): ...
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def _decorate_class(klass: Type[Any]) -> Type[Any]:
|
|
34
|
+
exclude_set = set(exclude or [])
|
|
35
|
+
|
|
36
|
+
for name, method in inspect.getmembers(klass):
|
|
37
|
+
# Skip excluded methods
|
|
38
|
+
if name in exclude_set:
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
# Skip private methods unless requested
|
|
42
|
+
if name.startswith("_") and not include_private:
|
|
43
|
+
# But always skip magic methods like __init__, __str__ unless explicitly handled?
|
|
44
|
+
# Usually we don't want to trace __init__ by default as it clutters the diagram
|
|
45
|
+
# and __repr__ MUST NOT be traced to avoid recursion in logging.
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
# Double check it's actually a function/method defined in this class (or inherited)
|
|
49
|
+
# inspect.isfunction or inspect.isroutine is good.
|
|
50
|
+
if inspect.isfunction(method) or inspect.iscoroutinefunction(method):
|
|
51
|
+
# Apply the trace decorator
|
|
52
|
+
# We need to handle staticmethods and classmethods carefully if we iterate __dict__
|
|
53
|
+
# But inspect.getmembers returns the bound/unbound method.
|
|
54
|
+
# However, modifying the class requires setting the attribute on the class.
|
|
55
|
+
|
|
56
|
+
# A safer way is to inspect __dict__ to see what is actually defined in THIS class
|
|
57
|
+
# vs inherited. But trace_class usually implies tracing this class's behavior.
|
|
58
|
+
|
|
59
|
+
# Let's try to set the attribute.
|
|
60
|
+
try:
|
|
61
|
+
setattr(klass, name, trace(**trace_kwargs)(method))
|
|
62
|
+
except (AttributeError, TypeError):
|
|
63
|
+
# Some attributes might be read-only or not settable
|
|
64
|
+
pass
|
|
65
|
+
return klass
|
|
66
|
+
|
|
67
|
+
if cls is None:
|
|
68
|
+
return _decorate_class
|
|
69
|
+
return _decorate_class(cls)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def patch_object(obj: Any, method_name: str, **trace_kwargs: Any) -> None:
|
|
73
|
+
"""
|
|
74
|
+
Monkey-patches a method on an object or class with tracing.
|
|
75
|
+
|
|
76
|
+
Useful for third-party libraries where you cannot modify the source code.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
obj: The object or class or module to patch.
|
|
80
|
+
method_name: The name of the function/method to patch.
|
|
81
|
+
**trace_kwargs: Arguments for @trace.
|
|
82
|
+
|
|
83
|
+
Usage:
|
|
84
|
+
import requests
|
|
85
|
+
patch_object(requests, 'get', action="HTTP GET")
|
|
86
|
+
"""
|
|
87
|
+
if not hasattr(obj, method_name):
|
|
88
|
+
raise AttributeError(f"{obj} has no attribute '{method_name}'")
|
|
89
|
+
|
|
90
|
+
original = getattr(obj, method_name)
|
|
91
|
+
|
|
92
|
+
# Apply trace decorator
|
|
93
|
+
decorated = trace(**trace_kwargs)(original)
|
|
94
|
+
|
|
95
|
+
# Replace
|
|
96
|
+
setattr(obj, method_name, decorated)
|