traceact 0.1.0__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.
traceact/sinks.py ADDED
@@ -0,0 +1,177 @@
1
+ # sinks.py
2
+ #
3
+ # Defines the sink system — where finished traces are written.
4
+ #
5
+ # A sink is any object that accepts a trace record (a plain Python dict) and
6
+ # stores or displays it. TraceAct ships two sinks for v1:
7
+ # JsonlSink — appends records to a .jsonl file, one JSON object per line.
8
+ # ConsoleSink — prints records to stdout, formatted for readability.
9
+ #
10
+ # How sink_mode is handled:
11
+ # The sink objects themselves are simple: they just implement write(record).
12
+ # The sink_mode setting (from TraceConfig) controls *when* write() is called:
13
+ # "blocking" — write() is called immediately when a trace finishes.
14
+ # "buffered" — the record is held in memory; write() is called on flush().
15
+ # "disabled" — write() is never called.
16
+ #
17
+ # The actual sink_mode logic lives in trace.py (_write_to_sinks). Sinks do not
18
+ # need to know which mode is active — they just write when asked.
19
+ #
20
+ # Future sinks (SqliteSink, HttpSink, OpenTelemetrySink) will follow the same
21
+ # interface: implement write(record: dict) and that is all TraceAct requires.
22
+
23
+ import atexit
24
+ import json
25
+ import os
26
+ from typing import Any, Dict, List
27
+
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Global buffer for buffered sink mode
31
+ # ---------------------------------------------------------------------------
32
+ #
33
+ # When sink_mode="buffered", finished trace records are appended here instead
34
+ # of being written to sinks immediately. They are flushed either by an explicit
35
+ # flush_buffer() call or automatically when the Python interpreter exits.
36
+ #
37
+ # Why a module-level list?
38
+ # The buffer needs to be shared across all traces in the same process, regardless
39
+ # of which sink or configuration object is in scope at the time of the flush.
40
+ # A module-level list is the simplest way to achieve this.
41
+
42
+ _buffer: List[Dict[str, Any]] = []
43
+ _flush_registered: bool = False # True once we have registered the atexit handler
44
+
45
+
46
+ def _flush_on_exit() -> None:
47
+ """
48
+ Called automatically by the Python interpreter on normal program exit.
49
+ Flushes any buffered traces to the configured sinks.
50
+
51
+ This ensures that short-lived scripts (which never call flush_buffer()
52
+ explicitly) still produce complete trace output.
53
+ """
54
+ from traceact.config import get_package_sinks
55
+ flush_buffer(get_package_sinks())
56
+
57
+
58
+ def _ensure_flush_registered() -> None:
59
+ """
60
+ Register the atexit flush handler the first time a buffered trace is
61
+ recorded. We register lazily so the handler is only installed when
62
+ actually needed.
63
+ """
64
+ global _flush_registered
65
+ if not _flush_registered:
66
+ atexit.register(_flush_on_exit)
67
+ _flush_registered = True
68
+
69
+
70
+ def buffer_record(record: Dict[str, Any]) -> None:
71
+ """
72
+ Add a finished trace record to the in-memory buffer.
73
+ Also ensures the atexit flush handler is registered.
74
+ """
75
+ _ensure_flush_registered()
76
+ _buffer.append(record)
77
+
78
+
79
+ def flush_buffer(sinks: List[Any]) -> None:
80
+ """
81
+ Write all buffered records to each sink, then clear the buffer.
82
+
83
+ Args:
84
+ sinks: The list of sink objects to write to.
85
+
86
+ This is safe to call multiple times. After flushing, the buffer is empty.
87
+ Calling flush when the buffer is already empty does nothing.
88
+ """
89
+ if not _buffer:
90
+ return
91
+ for record in _buffer:
92
+ for sink in sinks:
93
+ try:
94
+ sink.write(record)
95
+ except Exception:
96
+ # Sink failures never crash the application. Tracing is
97
+ # observability tooling; it must never become a point of
98
+ # failure for the code it is observing.
99
+ pass
100
+ _buffer.clear()
101
+
102
+
103
+ # ---------------------------------------------------------------------------
104
+ # JsonlSink
105
+ # ---------------------------------------------------------------------------
106
+
107
+ class JsonlSink:
108
+ """
109
+ Writes trace records to a newline-delimited JSON (JSONL) file.
110
+
111
+ Each finished trace is appended as a single line of JSON, followed by a
112
+ newline. This format is easy to append to, easy to read line by line, and
113
+ compatible with tools like jq, grep, and most log aggregators.
114
+
115
+ Args:
116
+ path: Path to the output file. The file is created if it does not exist
117
+ and appended to if it does. Parent directories must exist.
118
+
119
+ Example:
120
+ sinks=[JsonlSink("data/traces/traces.jsonl")]
121
+ """
122
+
123
+ def __init__(self, path: str) -> None:
124
+ self.path = path
125
+ # Ensure the parent directory exists so writes don't fail silently.
126
+ parent = os.path.dirname(os.path.abspath(path))
127
+ os.makedirs(parent, exist_ok=True)
128
+
129
+ def write(self, record: Dict[str, Any]) -> None:
130
+ """
131
+ Append one trace record to the JSONL file.
132
+
133
+ Args:
134
+ record: A plain dict representing the finished trace. Must be
135
+ JSON-serialisable; non-serialisable values should have
136
+ been sanitised before reaching the sink.
137
+ """
138
+ with open(self.path, "a", encoding="utf-8") as f:
139
+ f.write(json.dumps(record, default=str) + "\n")
140
+
141
+
142
+ # ---------------------------------------------------------------------------
143
+ # ConsoleSink
144
+ # ---------------------------------------------------------------------------
145
+
146
+ class ConsoleSink:
147
+ """
148
+ Prints trace records to stdout.
149
+
150
+ In pretty mode (the default), the record is printed as indented JSON so it
151
+ is readable in a terminal. In compact mode, it is printed as a single line
152
+ — useful when traces are mixed with other log output and you want each
153
+ record to occupy one line.
154
+
155
+ Args:
156
+ pretty: When True (default), print with 2-space indentation. When
157
+ False, print as a compact single-line JSON string.
158
+
159
+ Example:
160
+ sinks=[ConsoleSink()] # pretty output
161
+ sinks=[ConsoleSink(pretty=False)] # compact output
162
+ """
163
+
164
+ def __init__(self, pretty: bool = True) -> None:
165
+ self.pretty = pretty
166
+
167
+ def write(self, record: Dict[str, Any]) -> None:
168
+ """
169
+ Print one trace record to stdout.
170
+
171
+ Args:
172
+ record: A plain dict representing the finished trace.
173
+ """
174
+ if self.pretty:
175
+ print(json.dumps(record, indent=2, default=str))
176
+ else:
177
+ print(json.dumps(record, default=str))