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/events.py
CHANGED
|
@@ -1,75 +1,190 @@
|
|
|
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
|
|
|
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
|
+
|
|
5
85
|
@dataclass
|
|
6
|
-
class FlowEvent:
|
|
86
|
+
class FlowEvent(Event):
|
|
7
87
|
"""
|
|
8
88
|
Represents a single interaction or step in the execution flow.
|
|
9
|
-
|
|
89
|
+
|
|
10
90
|
This data structure acts as the intermediate representation (IR) between
|
|
11
|
-
runtime code execution and the final
|
|
91
|
+
runtime code execution and the final diagram output. Each instance
|
|
12
92
|
corresponds directly to one arrow or note in the sequence diagram.
|
|
13
|
-
|
|
14
|
-
The fields map to
|
|
93
|
+
|
|
94
|
+
The fields map to diagram syntax components as follows:
|
|
15
95
|
`source` -> `target`: `message`
|
|
16
|
-
|
|
96
|
+
|
|
17
97
|
Attributes:
|
|
18
|
-
source (str):
|
|
98
|
+
source (str):
|
|
19
99
|
The name of the participant initiating the action (the "Caller").
|
|
20
|
-
In
|
|
21
|
-
|
|
22
|
-
target (str):
|
|
100
|
+
In sequence diagrams: The participant on the LEFT side of the arrow.
|
|
101
|
+
|
|
102
|
+
target (str):
|
|
23
103
|
The name of the participant receiving the action (the "Callee").
|
|
24
|
-
In
|
|
25
|
-
|
|
26
|
-
action (str):
|
|
104
|
+
In sequence diagrams: The participant on the RIGHT side of the arrow.
|
|
105
|
+
|
|
106
|
+
action (str):
|
|
27
107
|
A short, human-readable name for the operation (e.g., function name).
|
|
28
108
|
Used for grouping or filtering logs, but often redundant with message.
|
|
29
|
-
|
|
30
|
-
message (str):
|
|
109
|
+
|
|
110
|
+
message (str):
|
|
31
111
|
The actual text label displayed on the diagram arrow.
|
|
32
112
|
Example: "getUser(id=1)" or "Return: User(name='Alice')".
|
|
33
|
-
|
|
34
|
-
timestamp (float):
|
|
113
|
+
|
|
114
|
+
timestamp (float):
|
|
35
115
|
Unix timestamp (seconds) of when the event occurred.
|
|
36
|
-
Used for ordering events if logs are processed asynchronously
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
trace_id (str):
|
|
116
|
+
Used for ordering events if logs are processed asynchronously.
|
|
117
|
+
Defaults to current time when event is created.
|
|
118
|
+
|
|
119
|
+
trace_id (str):
|
|
40
120
|
Unique identifier for the trace session.
|
|
41
121
|
Allows filtering multiple concurrent traces from a single log file
|
|
42
122
|
to generate separate diagrams for separate requests.
|
|
43
|
-
|
|
44
|
-
is_return (bool):
|
|
123
|
+
|
|
124
|
+
is_return (bool):
|
|
45
125
|
Flag indicating if this is a response arrow.
|
|
46
|
-
If True, the arrow is drawn as a dotted line
|
|
47
|
-
If False, it is a solid line
|
|
48
|
-
|
|
49
|
-
|
|
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):
|
|
50
131
|
Flag indicating if an exception occurred.
|
|
51
|
-
If True, the arrow might be styled differently
|
|
52
|
-
|
|
53
|
-
|
|
132
|
+
If True, the arrow might be styled differently to show failure.
|
|
133
|
+
Defaults to False.
|
|
134
|
+
|
|
135
|
+
error_message (Optional[str]):
|
|
54
136
|
Detailed error text if `is_error` is True.
|
|
55
137
|
Can be added as a note or included in the arrow label.
|
|
56
|
-
|
|
57
|
-
|
|
138
|
+
Defaults to None.
|
|
139
|
+
|
|
140
|
+
params (Optional[str]):
|
|
58
141
|
Stringified representation of function arguments.
|
|
59
142
|
Captured only for request events (call start).
|
|
60
|
-
|
|
61
|
-
|
|
143
|
+
Defaults to None.
|
|
144
|
+
|
|
145
|
+
result (Optional[str]):
|
|
62
146
|
Stringified representation of the return value.
|
|
63
147
|
Captured only for return events (call end).
|
|
148
|
+
Defaults to None.
|
|
64
149
|
"""
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
150
|
+
|
|
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,72 +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
|
|
14
|
+
|
|
5
15
|
|
|
6
|
-
class
|
|
16
|
+
class BaseFormatter(ABC, logging.Formatter):
|
|
7
17
|
"""
|
|
8
|
-
|
|
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.
|
|
9
25
|
"""
|
|
10
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
|
+
|
|
11
40
|
def format(self, record: logging.LogRecord) -> str:
|
|
12
|
-
|
|
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)
|
|
56
|
+
|
|
15
57
|
if not event:
|
|
16
58
|
# Fallback for standard logs if they accidentally reach this handler
|
|
17
59
|
return super().format(record)
|
|
18
60
|
|
|
19
|
-
#
|
|
20
|
-
return self.
|
|
61
|
+
# Convert event to the desired format using the subclass's format_event method
|
|
62
|
+
return self.format_event(event)
|
|
63
|
+
|
|
21
64
|
|
|
22
|
-
|
|
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:
|
|
23
75
|
"""
|
|
24
|
-
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
|
|
25
83
|
"""
|
|
26
|
-
|
|
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
|
+
|
|
88
|
+
# Sanitize participant names to avoid syntax errors in Mermaid
|
|
27
89
|
src = self._sanitize(event.source)
|
|
28
90
|
tgt = self._sanitize(event.target)
|
|
29
|
-
|
|
30
|
-
# Determine arrow type
|
|
91
|
+
|
|
92
|
+
# Determine arrow type based on event properties
|
|
31
93
|
# ->> : Solid line with arrowhead (synchronous call)
|
|
32
94
|
# -->> : Dotted line with arrowhead (return)
|
|
33
95
|
# --x : Dotted line with cross (error)
|
|
34
96
|
arrow = "-->>" if event.is_return else "->>"
|
|
35
|
-
|
|
97
|
+
|
|
98
|
+
# Construct message text based on event type
|
|
99
|
+
msg = ""
|
|
36
100
|
if event.is_error:
|
|
37
101
|
arrow = "--x"
|
|
38
102
|
msg = f"Error: {event.error_message}"
|
|
39
103
|
elif event.is_return:
|
|
104
|
+
# For return events, show return value or just "Return"
|
|
40
105
|
msg = f"Return: {event.result}" if event.result else "Return"
|
|
41
106
|
else:
|
|
107
|
+
# For call events, show Action(Params) or just Action
|
|
42
108
|
msg = f"{event.message}({event.params})" if event.params else event.message
|
|
43
|
-
|
|
44
|
-
#
|
|
45
|
-
# For now, we just output the interaction.
|
|
46
|
-
|
|
47
|
-
# Escape message for Mermaid safety
|
|
109
|
+
|
|
110
|
+
# Escape message for Mermaid safety (e.g., replacing newlines)
|
|
48
111
|
msg = self._escape_message(msg)
|
|
49
|
-
|
|
112
|
+
|
|
113
|
+
# Return the complete Mermaid syntax line
|
|
114
|
+
# Format: Source->>Target: Message
|
|
50
115
|
return f"{src}{arrow}{tgt}: {msg}"
|
|
51
116
|
|
|
52
117
|
def _sanitize(self, name: str) -> str:
|
|
53
118
|
"""
|
|
54
119
|
Sanitizes participant names to be valid Mermaid identifiers.
|
|
55
|
-
|
|
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.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
name: Original participant name
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
str: Sanitized participant name
|
|
56
130
|
"""
|
|
57
131
|
# Replace any non-alphanumeric character (except underscore) with underscore
|
|
58
|
-
clean_name = re.sub(r
|
|
59
|
-
# Ensure it doesn't start with a digit (Mermaid doesn't like that sometimes
|
|
132
|
+
clean_name = re.sub(r"[^a-zA-Z0-9_]", "_", name)
|
|
133
|
+
# Ensure it doesn't start with a digit (Mermaid doesn't like that sometimes)
|
|
60
134
|
if clean_name and clean_name[0].isdigit():
|
|
61
135
|
clean_name = "_" + clean_name
|
|
62
136
|
return clean_name
|
|
63
137
|
|
|
64
138
|
def _escape_message(self, msg: str) -> str:
|
|
65
139
|
"""
|
|
66
|
-
Escapes special characters in the message text.
|
|
67
|
-
|
|
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
|
|
68
147
|
"""
|
|
69
|
-
# Replace newlines with <br/> for Mermaid
|
|
70
|
-
msg = msg.replace(
|
|
71
|
-
#
|
|
148
|
+
# Replace newlines with <br/> for proper display in Mermaid diagrams
|
|
149
|
+
msg = msg.replace("\n", "<br/>")
|
|
150
|
+
# Additional escaping could be added here if needed for other characters
|
|
72
151
|
return msg
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Asynchronous Mermaid Handler Module
|
|
3
|
+
|
|
4
|
+
This module provides a non-blocking logging handler that uses a background thread
|
|
5
|
+
for writing logs. It's designed to improve performance in high-throughput applications
|
|
6
|
+
by decoupling the logging I/O from the main execution thread.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import logging.handlers
|
|
11
|
+
import queue
|
|
12
|
+
import atexit
|
|
13
|
+
from typing import List, Optional
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AsyncMermaidHandler(logging.handlers.QueueHandler):
|
|
17
|
+
"""
|
|
18
|
+
A non-blocking logging handler that uses a background thread to write logs.
|
|
19
|
+
|
|
20
|
+
This handler pushes log records to a queue, which are then picked up by a
|
|
21
|
+
QueueListener running in a separate thread and dispatched to the actual
|
|
22
|
+
handlers (e.g., MermaidFileHandler).
|
|
23
|
+
|
|
24
|
+
This architecture provides several benefits:
|
|
25
|
+
- Main thread doesn't block waiting for disk I/O
|
|
26
|
+
- Logs are processed in the background
|
|
27
|
+
- Better performance in high-throughput applications
|
|
28
|
+
- Smooth handling of burst traffic
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, handlers: List[logging.Handler], queue_size: int = 1000):
|
|
32
|
+
"""
|
|
33
|
+
Initialize the async handler.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
handlers: A list of handlers that should receive the logs from the queue.
|
|
37
|
+
These are typically MermaidFileHandler instances.
|
|
38
|
+
queue_size: The maximum size of the queue. Default is 1000.
|
|
39
|
+
If the queue fills up, new log records may be dropped.
|
|
40
|
+
"""
|
|
41
|
+
# Create a bounded queue with the specified size
|
|
42
|
+
self._log_queue: queue.Queue[logging.LogRecord] = queue.Queue(queue_size)
|
|
43
|
+
self._queue_size = queue_size
|
|
44
|
+
|
|
45
|
+
# Initialize parent QueueHandler with our queue
|
|
46
|
+
super().__init__(self._log_queue)
|
|
47
|
+
|
|
48
|
+
# Initialize QueueListener to process records from the queue
|
|
49
|
+
# It starts an internal thread to monitor the queue
|
|
50
|
+
# respect_handler_level=True ensures the target handlers' log levels are respected
|
|
51
|
+
self._listener: Optional[logging.handlers.QueueListener] = (
|
|
52
|
+
logging.handlers.QueueListener(
|
|
53
|
+
self._log_queue, *handlers, respect_handler_level=True
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Start the listener thread
|
|
58
|
+
self._listener.start()
|
|
59
|
+
|
|
60
|
+
# Register stop method to be called on program exit
|
|
61
|
+
# This ensures all pending logs are written to disk before termination
|
|
62
|
+
atexit.register(self.stop)
|
|
63
|
+
|
|
64
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Emit a log record to the queue with a timeout and drop policy.
|
|
67
|
+
|
|
68
|
+
If the queue is full, this method will attempt to put the record with
|
|
69
|
+
a short timeout. If that fails, it will drop the record and print a warning.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
record: The log record to emit
|
|
73
|
+
"""
|
|
74
|
+
from typing import cast
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
# Try to put the record in the queue with a short timeout (0.1 seconds)
|
|
78
|
+
# This prevents the main thread from blocking indefinitely if the queue is full
|
|
79
|
+
# Use cast to tell Mypy this is a queue.Queue instance
|
|
80
|
+
queue_instance = cast(queue.Queue[logging.LogRecord], self.queue)
|
|
81
|
+
queue_instance.put(record, block=True, timeout=0.1)
|
|
82
|
+
except queue.Full:
|
|
83
|
+
# If queue is full, log a warning and drop the record
|
|
84
|
+
if record.levelno >= logging.WARNING:
|
|
85
|
+
# Avoid infinite recursion by not using self.logger
|
|
86
|
+
print(
|
|
87
|
+
f"WARNING: AsyncMermaidHandler queue is full (size: {self._queue_size}), dropping log record: {record.msg}"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def stop(self) -> None:
|
|
91
|
+
"""
|
|
92
|
+
Stops the listener and flushes all pending logs from the queue.
|
|
93
|
+
|
|
94
|
+
This method is registered with `atexit` to ensure that all pending logs
|
|
95
|
+
are written to disk before the application terminates.
|
|
96
|
+
"""
|
|
97
|
+
if self._listener:
|
|
98
|
+
try:
|
|
99
|
+
# Stop the listener - this will process all remaining records in the queue
|
|
100
|
+
self._listener.stop()
|
|
101
|
+
self._listener = None
|
|
102
|
+
except queue.Full:
|
|
103
|
+
# Handle case where queue is full when trying to put sentinel value
|
|
104
|
+
# The listener thread may still be processing, but we can safely exit
|
|
105
|
+
pass
|