mermaid-trace 0.4.1__py3-none-any.whl → 0.6.0.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 +53 -21
- mermaid_trace/cli.py +168 -86
- mermaid_trace/core/config.py +55 -0
- mermaid_trace/core/decorators.py +401 -196
- mermaid_trace/core/events.py +13 -143
- mermaid_trace/core/formatter.py +176 -18
- mermaid_trace/core/utils.py +96 -0
- mermaid_trace/handlers/async_handler.py +123 -47
- mermaid_trace/handlers/mermaid_handler.py +152 -86
- mermaid_trace/integrations/__init__.py +9 -0
- mermaid_trace/integrations/fastapi.py +101 -46
- mermaid_trace/integrations/langchain.py +312 -0
- mermaid_trace/server.py +406 -0
- mermaid_trace-0.6.0.post0.dist-info/METADATA +272 -0
- mermaid_trace-0.6.0.post0.dist-info/RECORD +21 -0
- mermaid_trace-0.4.1.dist-info/METADATA +0 -180
- mermaid_trace-0.4.1.dist-info/RECORD +0 -16
- {mermaid_trace-0.4.1.dist-info → mermaid_trace-0.6.0.post0.dist-info}/WHEEL +0 -0
- {mermaid_trace-0.4.1.dist-info → mermaid_trace-0.6.0.post0.dist-info}/entry_points.txt +0 -0
- {mermaid_trace-0.4.1.dist-info → mermaid_trace-0.6.0.post0.dist-info}/licenses/LICENSE +0 -0
mermaid_trace/core/events.py
CHANGED
|
@@ -6,7 +6,7 @@ abstract Event base class and a concrete FlowEvent implementation that represent
|
|
|
6
6
|
individual interactions in the execution flow.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
from abc import ABC
|
|
9
|
+
from abc import ABC
|
|
10
10
|
from dataclasses import dataclass, field
|
|
11
11
|
import time
|
|
12
12
|
from typing import Optional
|
|
@@ -21,65 +21,13 @@ class Event(ABC):
|
|
|
21
21
|
classes must implement all abstract methods.
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
24
|
+
# Common attributes that should be present in all events
|
|
25
|
+
source: str
|
|
26
|
+
target: str
|
|
27
|
+
action: str
|
|
28
|
+
message: str
|
|
29
|
+
timestamp: float
|
|
30
|
+
trace_id: str
|
|
83
31
|
|
|
84
32
|
|
|
85
33
|
@dataclass
|
|
@@ -87,65 +35,7 @@ class FlowEvent(Event):
|
|
|
87
35
|
"""
|
|
88
36
|
Represents a single interaction or step in the execution flow.
|
|
89
37
|
|
|
90
|
-
|
|
91
|
-
runtime code execution and the final diagram output. Each instance
|
|
92
|
-
corresponds directly to one arrow or note in the sequence diagram.
|
|
93
|
-
|
|
94
|
-
The fields map to diagram syntax components as follows:
|
|
95
|
-
`source` -> `target`: `message`
|
|
96
|
-
|
|
97
|
-
Attributes:
|
|
98
|
-
source (str):
|
|
99
|
-
The name of the participant initiating the action (the "Caller").
|
|
100
|
-
In sequence diagrams: The participant on the LEFT side of the arrow.
|
|
101
|
-
|
|
102
|
-
target (str):
|
|
103
|
-
The name of the participant receiving the action (the "Callee").
|
|
104
|
-
In sequence diagrams: The participant on the RIGHT side of the arrow.
|
|
105
|
-
|
|
106
|
-
action (str):
|
|
107
|
-
A short, human-readable name for the operation (e.g., function name).
|
|
108
|
-
Used for grouping or filtering logs, but often redundant with message.
|
|
109
|
-
|
|
110
|
-
message (str):
|
|
111
|
-
The actual text label displayed on the diagram arrow.
|
|
112
|
-
Example: "getUser(id=1)" or "Return: User(name='Alice')".
|
|
113
|
-
|
|
114
|
-
timestamp (float):
|
|
115
|
-
Unix timestamp (seconds) of when the event occurred.
|
|
116
|
-
Used for ordering events if logs are processed asynchronously.
|
|
117
|
-
Defaults to current time when event is created.
|
|
118
|
-
|
|
119
|
-
trace_id (str):
|
|
120
|
-
Unique identifier for the trace session.
|
|
121
|
-
Allows filtering multiple concurrent traces from a single log file
|
|
122
|
-
to generate separate diagrams for separate requests.
|
|
123
|
-
|
|
124
|
-
is_return (bool):
|
|
125
|
-
Flag indicating if this is a response arrow.
|
|
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.
|
|
129
|
-
|
|
130
|
-
is_error (bool):
|
|
131
|
-
Flag indicating if an exception occurred.
|
|
132
|
-
If True, the arrow might be styled differently to show failure.
|
|
133
|
-
Defaults to False.
|
|
134
|
-
|
|
135
|
-
error_message (Optional[str]):
|
|
136
|
-
Detailed error text if `is_error` is True.
|
|
137
|
-
Can be added as a note or included in the arrow label.
|
|
138
|
-
Defaults to None.
|
|
139
|
-
|
|
140
|
-
params (Optional[str]):
|
|
141
|
-
Stringified representation of function arguments.
|
|
142
|
-
Captured only for request events (call start).
|
|
143
|
-
Defaults to None.
|
|
144
|
-
|
|
145
|
-
result (Optional[str]):
|
|
146
|
-
Stringified representation of the return value.
|
|
147
|
-
Captured only for return events (call end).
|
|
148
|
-
Defaults to None.
|
|
38
|
+
(Existing docstring omitted for brevity)
|
|
149
39
|
"""
|
|
150
40
|
|
|
151
41
|
# Required fields for every event
|
|
@@ -162,29 +52,9 @@ class FlowEvent(Event):
|
|
|
162
52
|
is_return: bool = False # Whether this is a response arrow
|
|
163
53
|
is_error: bool = False # Whether an error occurred
|
|
164
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
|
|
165
56
|
params: Optional[str] = None # Stringified function arguments
|
|
166
57
|
result: Optional[str] = None # Stringified return value
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
58
|
+
collapsed: bool = (
|
|
59
|
+
False # Whether this interaction should be visually collapsed (loop/folding)
|
|
60
|
+
)
|
mermaid_trace/core/formatter.py
CHANGED
|
@@ -9,7 +9,7 @@ with additional formatters for other diagram types or logging formats.
|
|
|
9
9
|
import logging
|
|
10
10
|
import re
|
|
11
11
|
from abc import ABC, abstractmethod
|
|
12
|
-
from typing import Optional
|
|
12
|
+
from typing import Optional, Dict, Set, Any, List, Tuple
|
|
13
13
|
from .events import Event, FlowEvent
|
|
14
14
|
|
|
15
15
|
|
|
@@ -37,6 +37,19 @@ class BaseFormatter(ABC, logging.Formatter):
|
|
|
37
37
|
"""
|
|
38
38
|
pass
|
|
39
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
|
+
|
|
40
53
|
def format(self, record: logging.LogRecord) -> str:
|
|
41
54
|
"""
|
|
42
55
|
Format a logging record containing an event.
|
|
@@ -71,68 +84,213 @@ class MermaidFormatter(BaseFormatter):
|
|
|
71
84
|
sequence diagram.
|
|
72
85
|
"""
|
|
73
86
|
|
|
74
|
-
def
|
|
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)
|
|
199
|
+
|
|
200
|
+
def get_header(self, title: str = "Log Flow") -> str:
|
|
201
|
+
"""
|
|
202
|
+
Returns the Mermaid sequence diagram header.
|
|
203
|
+
"""
|
|
204
|
+
return f"sequenceDiagram\n title {title}\n autonumber\n\n"
|
|
205
|
+
|
|
206
|
+
def format_event(self, event: Event, count: int = 1) -> str:
|
|
75
207
|
"""
|
|
76
208
|
Converts an Event into a Mermaid syntax string.
|
|
77
209
|
|
|
78
210
|
Args:
|
|
79
211
|
event: The Event object to format
|
|
212
|
+
count: Number of times this event was repeated (for collapsing)
|
|
80
213
|
|
|
81
214
|
Returns:
|
|
82
215
|
str: Mermaid syntax string representation of the event
|
|
83
216
|
"""
|
|
84
217
|
if not isinstance(event, FlowEvent):
|
|
85
218
|
# Fallback format for non-FlowEvent types
|
|
86
|
-
return f"{event.
|
|
219
|
+
return f"{event.source}->>{event.target}: {event.message}"
|
|
87
220
|
|
|
88
221
|
# Sanitize participant names to avoid syntax errors in Mermaid
|
|
89
222
|
src = self._sanitize(event.source)
|
|
90
223
|
tgt = self._sanitize(event.target)
|
|
91
224
|
|
|
92
|
-
# Determine arrow type
|
|
93
|
-
# ->> : Solid line with arrowhead (synchronous call)
|
|
94
|
-
# -->> : Dotted line with arrowhead (return)
|
|
95
|
-
# --x : Dotted line with cross (error)
|
|
225
|
+
# Determine arrow type
|
|
96
226
|
arrow = "-->>" if event.is_return else "->>"
|
|
97
227
|
|
|
98
|
-
# Construct message text
|
|
228
|
+
# Construct message text
|
|
99
229
|
msg = ""
|
|
100
230
|
if event.is_error:
|
|
101
231
|
arrow = "--x"
|
|
102
232
|
msg = f"Error: {event.error_message}"
|
|
103
233
|
elif event.is_return:
|
|
104
|
-
# For return events, show return value or just "Return"
|
|
105
234
|
msg = f"Return: {event.result}" if event.result else "Return"
|
|
106
235
|
else:
|
|
107
|
-
# For call events, show Action(Params) or just Action
|
|
108
236
|
msg = f"{event.message}({event.params})" if event.params else event.message
|
|
109
237
|
|
|
110
|
-
#
|
|
238
|
+
# Append count if collapsed
|
|
239
|
+
if count > 1:
|
|
240
|
+
msg += f" (x{count})"
|
|
241
|
+
|
|
242
|
+
# Escape message for Mermaid safety
|
|
111
243
|
msg = self._escape_message(msg)
|
|
112
244
|
|
|
113
245
|
# Return the complete Mermaid syntax line
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
116
260
|
|
|
117
261
|
def _sanitize(self, name: str) -> str:
|
|
118
262
|
"""
|
|
119
263
|
Sanitizes participant names to be valid Mermaid identifiers.
|
|
120
|
-
|
|
121
|
-
Mermaid doesn't like spaces or special characters in participant aliases
|
|
122
|
-
unless they are quoted (which we are not doing here for simplicity),
|
|
123
|
-
so we replace them with underscores.
|
|
264
|
+
Handles naming collisions by ensuring unique IDs.
|
|
124
265
|
|
|
125
266
|
Args:
|
|
126
267
|
name: Original participant name
|
|
127
268
|
|
|
128
269
|
Returns:
|
|
129
|
-
str: Sanitized participant name
|
|
270
|
+
str: Sanitized participant name (unique)
|
|
130
271
|
"""
|
|
272
|
+
if name in self._participant_map:
|
|
273
|
+
return self._participant_map[name]
|
|
274
|
+
|
|
131
275
|
# Replace any non-alphanumeric character (except underscore) with underscore
|
|
132
276
|
clean_name = re.sub(r"[^a-zA-Z0-9_]", "_", name)
|
|
133
277
|
# Ensure it doesn't start with a digit (Mermaid doesn't like that sometimes)
|
|
134
278
|
if clean_name and clean_name[0].isdigit():
|
|
135
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)
|
|
136
294
|
return clean_name
|
|
137
295
|
|
|
138
296
|
def _escape_message(self, msg: str) -> str:
|
|
@@ -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)
|