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.
@@ -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 Mermaid diagram output. Each instance
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 Mermaid syntax components as follows:
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 Mermaid: The participant on the LEFT side of the arrow.
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 Mermaid: The participant on the RIGHT side of the arrow.
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
- though Mermaid sequence diagrams primarily rely on line order.
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 (`-->`) in Mermaid.
47
- If False, it is a solid line (`->`) representing a call.
48
-
49
- is_error (bool):
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 (e.g., `-x`) to show failure.
52
-
53
- error_message (Optional[str]):
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
- params (Optional[str]):
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
- result (Optional[str]):
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
- source: str
66
- target: str
67
- action: str
68
- message: str
69
- trace_id: str
70
- timestamp: float = field(default_factory=time.time)
71
- is_return: bool = False
72
- is_error: bool = False
73
- error_message: Optional[str] = None
74
- params: Optional[str] = None
75
- result: Optional[str] = None
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
@@ -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 MermaidFormatter(logging.Formatter):
16
+ class BaseFormatter(ABC, logging.Formatter):
7
17
  """
8
- Custom formatter to convert FlowEvents into Mermaid sequence diagram syntax.
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
- # 1. Retrieve the FlowEvent
13
- event: Optional[FlowEvent] = getattr(record, 'flow_event', None)
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
- # 2. Convert event to Mermaid line
20
- return self._to_mermaid_line(event)
61
+ # Convert event to the desired format using the subclass's format_event method
62
+ return self.format_event(event)
63
+
21
64
 
22
- def _to_mermaid_line(self, event: FlowEvent) -> str:
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 a FlowEvent into a Mermaid syntax string.
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
- # Sanitize participant names
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
- # Optional: Add note or group if trace_id changes (not implemented in single line format)
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
- Allows alphanumeric and underscores. Replaces others.
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'[^a-zA-Z0-9_]', '_', name)
59
- # Ensure it doesn't start with a digit (Mermaid doesn't like that sometimes, though often okay)
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
- Mermaid messages can contain most chars, but : and newlines can be tricky.
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 display
70
- msg = msg.replace('\n', '<br/>')
71
- # We might want to escape other chars if needed, but usually text after : is forgiving.
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