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/__init__.py +43 -0
- traceact/budget.py +103 -0
- traceact/config.py +198 -0
- traceact/context.py +130 -0
- traceact/decorators.py +430 -0
- traceact/helpers.py +103 -0
- traceact/ids.py +62 -0
- traceact/sinks.py +177 -0
- traceact/trace.py +1176 -0
- traceact-0.1.0.dist-info/METADATA +94 -0
- traceact-0.1.0.dist-info/RECORD +14 -0
- traceact-0.1.0.dist-info/WHEEL +5 -0
- traceact-0.1.0.dist-info/licenses/LICENSE +21 -0
- traceact-0.1.0.dist-info/top_level.txt +1 -0
traceact/decorators.py
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
# decorators.py
|
|
2
|
+
#
|
|
3
|
+
# Defines @traced_action — the primary developer-facing API for TraceAct.
|
|
4
|
+
#
|
|
5
|
+
# Why a decorator?
|
|
6
|
+
# The decorator form is the most natural way to attach tracing to existing
|
|
7
|
+
# functions. It requires no changes to the function body and wraps the entire
|
|
8
|
+
# call lifecycle (start, input capture, success, error, finish) automatically.
|
|
9
|
+
# The developer focuses on what the function does; TraceAct handles recording
|
|
10
|
+
# the story.
|
|
11
|
+
#
|
|
12
|
+
# How the decorator works:
|
|
13
|
+
# 1. @traced_action(action="...", kind="...") returns a decorator.
|
|
14
|
+
# 2. That decorator wraps the target function in a wrapper.
|
|
15
|
+
# 3. The wrapper calls _create_trace() to build (or skip) a trace.
|
|
16
|
+
# 4. If the trace is a _SkippedTrace, SKIP is pushed onto the ContextVar and
|
|
17
|
+
# the function runs without recording anything.
|
|
18
|
+
# 5. If the trace is a real ActionTrace, it is pushed onto the ContextVar as
|
|
19
|
+
# the active trace, inputs are optionally captured, the function runs, and
|
|
20
|
+
# the trace is finished on success or failure.
|
|
21
|
+
# 6. The ContextVar is always restored in a finally block.
|
|
22
|
+
#
|
|
23
|
+
# Async support:
|
|
24
|
+
# The decorator detects whether the wrapped function is a coroutine function
|
|
25
|
+
# (async def) and returns either a sync or async wrapper accordingly. The logic
|
|
26
|
+
# in both wrappers is identical — the only difference is the use of "await".
|
|
27
|
+
#
|
|
28
|
+
# Input capture:
|
|
29
|
+
# When capture_inputs is specified, the decorator calls _capture_args() to map
|
|
30
|
+
# the positional and keyword arguments to their parameter names via
|
|
31
|
+
# inspect.signature(), applies redaction and size limits, and stores the result
|
|
32
|
+
# on the trace's inputs dict. This never breaks the function call — if capture
|
|
33
|
+
# fails for any reason, it is silently skipped (unless strict=True).
|
|
34
|
+
|
|
35
|
+
import functools
|
|
36
|
+
import inspect
|
|
37
|
+
from typing import Any, Dict, List, Optional, Union
|
|
38
|
+
|
|
39
|
+
from traceact.budget import TraceBudget
|
|
40
|
+
from traceact.config import TraceConfig
|
|
41
|
+
from traceact.context import SKIP, pop_trace, push_trace
|
|
42
|
+
from traceact.trace import (
|
|
43
|
+
ActionTrace,
|
|
44
|
+
_NoOpTrace,
|
|
45
|
+
_SkippedTrace,
|
|
46
|
+
_create_trace,
|
|
47
|
+
_is_sensitive,
|
|
48
|
+
_safe_value,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def traced_action(
|
|
53
|
+
action: str,
|
|
54
|
+
kind: str = "app",
|
|
55
|
+
actor: Optional[str] = None,
|
|
56
|
+
project: Optional[str] = None,
|
|
57
|
+
operation: Optional[str] = None,
|
|
58
|
+
target: Optional[str] = None,
|
|
59
|
+
database: Optional[str] = None,
|
|
60
|
+
capture_inputs: Any = False,
|
|
61
|
+
meta: Optional[Dict[str, Any]] = None,
|
|
62
|
+
config: Optional[TraceConfig] = None,
|
|
63
|
+
budget: Optional[TraceBudget] = None,
|
|
64
|
+
correlation_id: Optional[str] = None,
|
|
65
|
+
) -> Any:
|
|
66
|
+
"""
|
|
67
|
+
Decorator that traces the execution of a function.
|
|
68
|
+
|
|
69
|
+
Attach this to any function — sync or async — to automatically record a
|
|
70
|
+
trace whenever the function is called. The trace captures timing, status,
|
|
71
|
+
errors, and optionally inputs.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
action:
|
|
75
|
+
The name of the action being traced. Use dot-notation to describe
|
|
76
|
+
the action clearly. Examples: "note.create", "payment.authorise",
|
|
77
|
+
"agent.run", "report.export".
|
|
78
|
+
|
|
79
|
+
kind:
|
|
80
|
+
The category of work. This tells readers what kind of system the
|
|
81
|
+
function interacts with. Standard values: "app", "db", "http",
|
|
82
|
+
"file", "model", "cache", "queue", "job", "auth", "payment",
|
|
83
|
+
"email", "export". Default: "app".
|
|
84
|
+
|
|
85
|
+
actor:
|
|
86
|
+
Who or what triggered the action. Examples: "user", "cron",
|
|
87
|
+
"webhook", "agent". Optional.
|
|
88
|
+
|
|
89
|
+
project:
|
|
90
|
+
The application or service this trace belongs to. Useful for
|
|
91
|
+
grouping traces from multiple services in the same output.
|
|
92
|
+
|
|
93
|
+
operation:
|
|
94
|
+
For kind="db", "http", "file" etc. — the specific operation being
|
|
95
|
+
performed (e.g. "insert", "post", "write"). When provided alongside
|
|
96
|
+
target, an initial event is automatically created inside the trace.
|
|
97
|
+
|
|
98
|
+
target:
|
|
99
|
+
The resource the operation acts on (e.g. "notes", "stripe",
|
|
100
|
+
"data/output.json"). Works alongside operation.
|
|
101
|
+
|
|
102
|
+
database:
|
|
103
|
+
For kind="db" traces — the database name or driver (e.g. "sqlite",
|
|
104
|
+
"postgres"). Stored on the initial event alongside operation/target.
|
|
105
|
+
|
|
106
|
+
capture_inputs:
|
|
107
|
+
Controls whether function arguments are automatically recorded.
|
|
108
|
+
False (default): no automatic capture.
|
|
109
|
+
True: capture all named arguments (excluding self/cls), with
|
|
110
|
+
redaction and size limits applied.
|
|
111
|
+
list of strings: capture only the named arguments in the list.
|
|
112
|
+
This is the safest and most explicit form.
|
|
113
|
+
|
|
114
|
+
meta:
|
|
115
|
+
Arbitrary key-value data to attach to the trace. TraceAct stores
|
|
116
|
+
it but does not interpret it. Examples: {"release": "v1.2"}.
|
|
117
|
+
|
|
118
|
+
config:
|
|
119
|
+
A TraceConfig that overrides package-level settings for this trace
|
|
120
|
+
only. Only non-None fields in the config are applied.
|
|
121
|
+
|
|
122
|
+
budget:
|
|
123
|
+
A TraceBudget that overrides inherited limits for this trace only.
|
|
124
|
+
Only non-None fields in the budget are applied. Other fields are
|
|
125
|
+
inherited from the parent trace or the package default.
|
|
126
|
+
|
|
127
|
+
correlation_id:
|
|
128
|
+
A shared ID to link this trace with other traces in the same
|
|
129
|
+
logical workflow (e.g. the same API request, the same job, the
|
|
130
|
+
same order). Useful for connecting traces across functions or
|
|
131
|
+
services.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
A decorator that wraps the target function.
|
|
135
|
+
|
|
136
|
+
Examples:
|
|
137
|
+
|
|
138
|
+
Basic usage:
|
|
139
|
+
@traced_action(action="note.create", kind="app")
|
|
140
|
+
def create_note(title, body):
|
|
141
|
+
...
|
|
142
|
+
|
|
143
|
+
With selective input capture:
|
|
144
|
+
@traced_action(
|
|
145
|
+
action="note.create",
|
|
146
|
+
kind="app",
|
|
147
|
+
capture_inputs=["title", "user_id"],
|
|
148
|
+
)
|
|
149
|
+
def create_note(title, body, user_id):
|
|
150
|
+
...
|
|
151
|
+
|
|
152
|
+
Database function:
|
|
153
|
+
@traced_action(
|
|
154
|
+
action="note.save",
|
|
155
|
+
kind="db",
|
|
156
|
+
operation="insert",
|
|
157
|
+
target="notes",
|
|
158
|
+
database="sqlite",
|
|
159
|
+
)
|
|
160
|
+
def save_note(note):
|
|
161
|
+
...
|
|
162
|
+
|
|
163
|
+
Async function:
|
|
164
|
+
@traced_action(action="payment.authorise", kind="payment")
|
|
165
|
+
async def authorise_payment(amount, currency):
|
|
166
|
+
...
|
|
167
|
+
"""
|
|
168
|
+
def decorator(func: Any) -> Any:
|
|
169
|
+
# Decide now (at decoration time, not call time) which wrapper to use.
|
|
170
|
+
# This avoids an inspect.iscoroutinefunction() check on every call.
|
|
171
|
+
if inspect.iscoroutinefunction(func):
|
|
172
|
+
return _async_wrapper(
|
|
173
|
+
func, action, kind, actor, project, operation, target,
|
|
174
|
+
database, capture_inputs, meta, config, budget, correlation_id,
|
|
175
|
+
)
|
|
176
|
+
else:
|
|
177
|
+
return _sync_wrapper(
|
|
178
|
+
func, action, kind, actor, project, operation, target,
|
|
179
|
+
database, capture_inputs, meta, config, budget, correlation_id,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
return decorator
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ---------------------------------------------------------------------------
|
|
186
|
+
# Sync wrapper
|
|
187
|
+
# ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
def _sync_wrapper(
|
|
190
|
+
func: Any,
|
|
191
|
+
action: str,
|
|
192
|
+
kind: str,
|
|
193
|
+
actor: Optional[str],
|
|
194
|
+
project: Optional[str],
|
|
195
|
+
operation: Optional[str],
|
|
196
|
+
target: Optional[str],
|
|
197
|
+
database: Optional[str],
|
|
198
|
+
capture_inputs: Any,
|
|
199
|
+
meta: Optional[Dict[str, Any]],
|
|
200
|
+
config: Optional[TraceConfig],
|
|
201
|
+
budget: Optional[TraceBudget],
|
|
202
|
+
correlation_id: Optional[str],
|
|
203
|
+
) -> Any:
|
|
204
|
+
"""
|
|
205
|
+
Build and return a sync wrapper for the given function.
|
|
206
|
+
|
|
207
|
+
The wrapper follows this flow on every call:
|
|
208
|
+
1. Ask _create_trace() whether to create a real trace or skip.
|
|
209
|
+
2. If skipped (sampled out): push SKIP onto ContextVar, run function, restore.
|
|
210
|
+
3. If no-op (disabled / depth exceeded): run function with no context changes.
|
|
211
|
+
4. If real trace: push trace, capture inputs, run function, finish, restore.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
@functools.wraps(func)
|
|
215
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
216
|
+
# Build (or decide to skip) the trace. _create_trace handles all the
|
|
217
|
+
# checks: enabled?, skip sentinel?, sampling?, depth limit?.
|
|
218
|
+
trace_or_noop = _create_trace(
|
|
219
|
+
action=action,
|
|
220
|
+
kind=kind,
|
|
221
|
+
actor=actor,
|
|
222
|
+
project=project,
|
|
223
|
+
correlation_id=correlation_id,
|
|
224
|
+
meta=meta,
|
|
225
|
+
config_override=config,
|
|
226
|
+
budget_override=budget,
|
|
227
|
+
operation=operation,
|
|
228
|
+
target=target,
|
|
229
|
+
database=database,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Case 1: The trace was sampled out. We must push SKIP onto the
|
|
233
|
+
# ContextVar so that nested @traced_action calls also skip.
|
|
234
|
+
if isinstance(trace_or_noop, _SkippedTrace):
|
|
235
|
+
token = push_trace(SKIP)
|
|
236
|
+
try:
|
|
237
|
+
return func(*args, **kwargs)
|
|
238
|
+
finally:
|
|
239
|
+
pop_trace(token)
|
|
240
|
+
|
|
241
|
+
# Case 2: Tracing is disabled or depth exceeded. No context changes
|
|
242
|
+
# needed — just run the function directly.
|
|
243
|
+
if isinstance(trace_or_noop, _NoOpTrace):
|
|
244
|
+
return func(*args, **kwargs)
|
|
245
|
+
|
|
246
|
+
# Case 3: We have a real trace. Run the full tracing lifecycle.
|
|
247
|
+
trace: ActionTrace = trace_or_noop
|
|
248
|
+
|
|
249
|
+
# Push the trace onto the ContextVar. Any @traced_action calls made
|
|
250
|
+
# inside func() will find this trace as their parent.
|
|
251
|
+
token = push_trace(trace)
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
# Optionally capture function arguments as trace inputs.
|
|
255
|
+
# This happens before the function runs so inputs are recorded even
|
|
256
|
+
# if the function raises immediately.
|
|
257
|
+
if capture_inputs is not False:
|
|
258
|
+
_capture_inputs(trace, func, args, kwargs, capture_inputs)
|
|
259
|
+
|
|
260
|
+
# Run the actual function.
|
|
261
|
+
result = func(*args, **kwargs)
|
|
262
|
+
|
|
263
|
+
# Success path.
|
|
264
|
+
trace._finish(status="completed")
|
|
265
|
+
return result
|
|
266
|
+
|
|
267
|
+
except Exception as exc:
|
|
268
|
+
# Failure path. The exception is re-raised after the trace is
|
|
269
|
+
# finished — TraceAct never suppresses exceptions.
|
|
270
|
+
trace._finish(status="failed", error=exc)
|
|
271
|
+
raise
|
|
272
|
+
|
|
273
|
+
finally:
|
|
274
|
+
# Always restore the ContextVar, regardless of success or failure.
|
|
275
|
+
pop_trace(token)
|
|
276
|
+
|
|
277
|
+
return wrapper
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# ---------------------------------------------------------------------------
|
|
281
|
+
# Async wrapper
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
def _async_wrapper(
|
|
285
|
+
func: Any,
|
|
286
|
+
action: str,
|
|
287
|
+
kind: str,
|
|
288
|
+
actor: Optional[str],
|
|
289
|
+
project: Optional[str],
|
|
290
|
+
operation: Optional[str],
|
|
291
|
+
target: Optional[str],
|
|
292
|
+
database: Optional[str],
|
|
293
|
+
capture_inputs: Any,
|
|
294
|
+
meta: Optional[Dict[str, Any]],
|
|
295
|
+
config: Optional[TraceConfig],
|
|
296
|
+
budget: Optional[TraceBudget],
|
|
297
|
+
correlation_id: Optional[str],
|
|
298
|
+
) -> Any:
|
|
299
|
+
"""
|
|
300
|
+
Build and return an async wrapper for the given coroutine function.
|
|
301
|
+
|
|
302
|
+
The logic is identical to _sync_wrapper except that the function is awaited.
|
|
303
|
+
ContextVar is async-safe in Python 3.7+: each asyncio Task inherits a copy
|
|
304
|
+
of the current context from the task that created it. Within a single Task,
|
|
305
|
+
the ContextVar behaves like a call-stack variable.
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
@functools.wraps(func)
|
|
309
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
310
|
+
trace_or_noop = _create_trace(
|
|
311
|
+
action=action,
|
|
312
|
+
kind=kind,
|
|
313
|
+
actor=actor,
|
|
314
|
+
project=project,
|
|
315
|
+
correlation_id=correlation_id,
|
|
316
|
+
meta=meta,
|
|
317
|
+
config_override=config,
|
|
318
|
+
budget_override=budget,
|
|
319
|
+
operation=operation,
|
|
320
|
+
target=target,
|
|
321
|
+
database=database,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Sampled out — push SKIP, await, restore.
|
|
325
|
+
if isinstance(trace_or_noop, _SkippedTrace):
|
|
326
|
+
token = push_trace(SKIP)
|
|
327
|
+
try:
|
|
328
|
+
return await func(*args, **kwargs)
|
|
329
|
+
finally:
|
|
330
|
+
pop_trace(token)
|
|
331
|
+
|
|
332
|
+
# Disabled / depth exceeded — just await.
|
|
333
|
+
if isinstance(trace_or_noop, _NoOpTrace):
|
|
334
|
+
return await func(*args, **kwargs)
|
|
335
|
+
|
|
336
|
+
# Real trace.
|
|
337
|
+
trace: ActionTrace = trace_or_noop
|
|
338
|
+
token = push_trace(trace)
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
if capture_inputs is not False:
|
|
342
|
+
_capture_inputs(trace, func, args, kwargs, capture_inputs)
|
|
343
|
+
|
|
344
|
+
result = await func(*args, **kwargs)
|
|
345
|
+
trace._finish(status="completed")
|
|
346
|
+
return result
|
|
347
|
+
|
|
348
|
+
except Exception as exc:
|
|
349
|
+
trace._finish(status="failed", error=exc)
|
|
350
|
+
raise
|
|
351
|
+
|
|
352
|
+
finally:
|
|
353
|
+
pop_trace(token)
|
|
354
|
+
|
|
355
|
+
return wrapper
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
# ---------------------------------------------------------------------------
|
|
359
|
+
# Input capture
|
|
360
|
+
# ---------------------------------------------------------------------------
|
|
361
|
+
|
|
362
|
+
def _capture_inputs(
|
|
363
|
+
trace: ActionTrace,
|
|
364
|
+
func: Any,
|
|
365
|
+
args: tuple,
|
|
366
|
+
kwargs: Dict[str, Any],
|
|
367
|
+
capture_spec: Any,
|
|
368
|
+
) -> None:
|
|
369
|
+
"""
|
|
370
|
+
Capture function arguments and record them on the trace as inputs.
|
|
371
|
+
|
|
372
|
+
This is called before the function body runs, so inputs are always recorded
|
|
373
|
+
even if the function raises immediately.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
trace: The ActionTrace to record inputs on.
|
|
377
|
+
func: The original (unwrapped) function.
|
|
378
|
+
args: Positional arguments passed to the function.
|
|
379
|
+
kwargs: Keyword arguments passed to the function.
|
|
380
|
+
capture_spec: False (no capture), True (all args), or a list of field names.
|
|
381
|
+
|
|
382
|
+
How it works:
|
|
383
|
+
1. inspect.signature() gives us the function's parameter names.
|
|
384
|
+
2. We map positional args to their names using that signature.
|
|
385
|
+
3. We skip "self" and "cls" (the first param of methods).
|
|
386
|
+
4. If capture_spec is a list, we filter to only those names.
|
|
387
|
+
5. We apply redaction and size limits via the trace's config/budget.
|
|
388
|
+
6. We call trace.input() with the sanitised dict.
|
|
389
|
+
|
|
390
|
+
Failures are swallowed (in non-strict mode) so that a problem with
|
|
391
|
+
input capture never breaks the function call.
|
|
392
|
+
"""
|
|
393
|
+
try:
|
|
394
|
+
# Get the parameter list from the function signature.
|
|
395
|
+
sig = inspect.signature(func)
|
|
396
|
+
param_names = list(sig.parameters.keys())
|
|
397
|
+
|
|
398
|
+
# Remove "self" and "cls" — instance method receivers are not inputs.
|
|
399
|
+
if param_names and param_names[0] in ("self", "cls"):
|
|
400
|
+
param_names = param_names[1:]
|
|
401
|
+
args = args[1:]
|
|
402
|
+
|
|
403
|
+
# Build a dict mapping parameter names to their values.
|
|
404
|
+
# Positional args are mapped by position; kwargs are merged in.
|
|
405
|
+
bound: Dict[str, Any] = {}
|
|
406
|
+
for i, val in enumerate(args):
|
|
407
|
+
if i < len(param_names):
|
|
408
|
+
bound[param_names[i]] = val
|
|
409
|
+
bound.update(kwargs)
|
|
410
|
+
|
|
411
|
+
# If capture_spec is a list, keep only the explicitly requested fields.
|
|
412
|
+
# This is the safest form: the developer chose exactly what to record.
|
|
413
|
+
if isinstance(capture_spec, list):
|
|
414
|
+
bound = {k: v for k, v in bound.items() if k in capture_spec}
|
|
415
|
+
|
|
416
|
+
# If capture_spec is True, keep everything (already in bound).
|
|
417
|
+
# We do not need an elif here — the list branch has already filtered.
|
|
418
|
+
|
|
419
|
+
if not bound:
|
|
420
|
+
return
|
|
421
|
+
|
|
422
|
+
# Delegate to trace.input(), which applies redaction and size limits.
|
|
423
|
+
trace.input(bound)
|
|
424
|
+
|
|
425
|
+
except Exception as exc:
|
|
426
|
+
# Input capture should never crash the application.
|
|
427
|
+
if trace._effective_config.strict:
|
|
428
|
+
raise
|
|
429
|
+
# In non-strict mode, silently skip the capture.
|
|
430
|
+
_ = exc
|
traceact/helpers.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# helpers.py
|
|
2
|
+
#
|
|
3
|
+
# Defines TraceHelpersMixin — a mixin class that adds convenience methods to
|
|
4
|
+
# ActionTrace for common event kinds.
|
|
5
|
+
#
|
|
6
|
+
# Why a mixin?
|
|
7
|
+
# The helpers (db, http, file, model) are just thin wrappers around
|
|
8
|
+
# trace.event(). Putting them in a separate mixin keeps trace.py focused on
|
|
9
|
+
# the core trace lifecycle and makes it easy to add more helpers later without
|
|
10
|
+
# touching the main ActionTrace class.
|
|
11
|
+
#
|
|
12
|
+
# Why not standalone functions like trace_db(...)?
|
|
13
|
+
# The methods live on the trace object (trace.db(...)) because the trace object
|
|
14
|
+
# is the natural scope for recording an event. A standalone function would need
|
|
15
|
+
# to look up the active trace from the ContextVar every time it was called,
|
|
16
|
+
# which is less readable and hides the relationship between the call and the
|
|
17
|
+
# trace it belongs to.
|
|
18
|
+
#
|
|
19
|
+
# All helpers use "target" as the resource field name. Aliases like "table",
|
|
20
|
+
# "url", or "path" are not accepted — the grammar stays consistent across all
|
|
21
|
+
# kinds so that traces are uniform regardless of the operation type.
|
|
22
|
+
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TraceHelpersMixin:
|
|
27
|
+
"""
|
|
28
|
+
Convenience methods for recording common event kinds on a trace.
|
|
29
|
+
|
|
30
|
+
These methods are mixed into ActionTrace. They are thin wrappers around
|
|
31
|
+
trace.event() and exist purely to reduce typing for the most frequent
|
|
32
|
+
operations. They add no new behaviour.
|
|
33
|
+
|
|
34
|
+
All methods accept arbitrary keyword arguments (**kwargs) that are passed
|
|
35
|
+
through to trace.event(). This means any field defined in the event schema
|
|
36
|
+
(rows, status, duration_ms, result, etc.) can be provided.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def db(self, operation: str, target: str, **kwargs: Any) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Record a database event.
|
|
42
|
+
|
|
43
|
+
Equivalent to: trace.event(kind="db", operation=operation, target=target, ...)
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
operation: The database operation. Standard values: select, insert,
|
|
47
|
+
update, delete, upsert, transaction, migration.
|
|
48
|
+
target: The table or database resource involved. Example: "notes".
|
|
49
|
+
**kwargs: Additional fields such as rows=1, database="sqlite",
|
|
50
|
+
safe_query="...", params_shape={...}, duration_ms=12.4.
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
trace.db(operation="insert", target="notes", rows=1)
|
|
54
|
+
"""
|
|
55
|
+
self.event(kind="db", operation=operation, target=target, **kwargs)
|
|
56
|
+
|
|
57
|
+
def http(self, operation: str, target: str, **kwargs: Any) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Record an HTTP event.
|
|
60
|
+
|
|
61
|
+
Equivalent to: trace.event(kind="http", operation=operation, target=target, ...)
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
operation: The HTTP method or action. Examples: get, post, put, delete.
|
|
65
|
+
target: The endpoint or service name. Example: "payment-provider".
|
|
66
|
+
**kwargs: Additional fields such as status_code=200, duration_ms=120.
|
|
67
|
+
|
|
68
|
+
Example:
|
|
69
|
+
trace.http(operation="post", target="stripe", status_code=200)
|
|
70
|
+
"""
|
|
71
|
+
self.event(kind="http", operation=operation, target=target, **kwargs)
|
|
72
|
+
|
|
73
|
+
def file(self, operation: str, target: str, **kwargs: Any) -> None:
|
|
74
|
+
"""
|
|
75
|
+
Record a file operation event.
|
|
76
|
+
|
|
77
|
+
Equivalent to: trace.event(kind="file", operation=operation, target=target, ...)
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
operation: The file operation. Examples: read, write, delete, move.
|
|
81
|
+
target: The file path. Example: "data/notes.json".
|
|
82
|
+
**kwargs: Additional fields such as bytes_written=1024.
|
|
83
|
+
|
|
84
|
+
Example:
|
|
85
|
+
trace.file(operation="write", target="data/notes.json")
|
|
86
|
+
"""
|
|
87
|
+
self.event(kind="file", operation=operation, target=target, **kwargs)
|
|
88
|
+
|
|
89
|
+
def model(self, operation: str, target: str, **kwargs: Any) -> None:
|
|
90
|
+
"""
|
|
91
|
+
Record a model call event.
|
|
92
|
+
|
|
93
|
+
Equivalent to: trace.event(kind="model", operation=operation, target=target, ...)
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
operation: The model operation. Examples: completion, embedding, classification.
|
|
97
|
+
target: The model name or identifier. Example: "claude-sonnet-5".
|
|
98
|
+
**kwargs: Additional fields such as tokens_in=1200, tokens_out=300.
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
trace.model(operation="completion", target="claude-sonnet-5", tokens_in=800)
|
|
102
|
+
"""
|
|
103
|
+
self.event(kind="model", operation=operation, target=target, **kwargs)
|
traceact/ids.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# ids.py
|
|
2
|
+
#
|
|
3
|
+
# Generates prefixed, human-readable IDs for traces, events, steps, and
|
|
4
|
+
# correlation groups.
|
|
5
|
+
#
|
|
6
|
+
# Why prefixes?
|
|
7
|
+
# When you read a raw trace record — in a log file, a terminal, or a debugger
|
|
8
|
+
# — a bare UUID tells you nothing about what you are looking at. A prefixed ID
|
|
9
|
+
# like "trc_9f3a1c7b2d44" or "evt_184c90aa22af" is immediately identifiable.
|
|
10
|
+
# Developers know at a glance whether an ID belongs to a trace, an event, or a
|
|
11
|
+
# step without having to cross-reference the field name.
|
|
12
|
+
#
|
|
13
|
+
# Why 12 hex characters?
|
|
14
|
+
# 6 random bytes gives 2^48 (~281 trillion) possible values. Collision risk in
|
|
15
|
+
# a single session is negligible. The IDs are short enough to read but long
|
|
16
|
+
# enough to be unique across all traces in a single run.
|
|
17
|
+
|
|
18
|
+
import secrets
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _generate_id(prefix: str) -> str:
|
|
22
|
+
"""
|
|
23
|
+
Build a prefixed ID from 6 random bytes encoded as lowercase hex.
|
|
24
|
+
|
|
25
|
+
The format is: {prefix}_{12_hex_chars}
|
|
26
|
+
Example: trc_9f3a1c7b2d44
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
prefix: A short string identifying the kind of record (e.g. "trc", "evt").
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
A string ID that is unique with overwhelming probability.
|
|
33
|
+
"""
|
|
34
|
+
return f"{prefix}_{secrets.token_hex(6)}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def new_trace_id() -> str:
|
|
38
|
+
"""Return a fresh trace ID. Example: trc_9f3a1c7b2d44"""
|
|
39
|
+
return _generate_id("trc")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def new_event_id() -> str:
|
|
43
|
+
"""Return a fresh event ID. Example: evt_184c90aa22af"""
|
|
44
|
+
return _generate_id("evt")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def new_step_id() -> str:
|
|
48
|
+
"""Return a fresh step ID. Example: stp_62e1aa49c103"""
|
|
49
|
+
return _generate_id("stp")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def new_correlation_id() -> str:
|
|
53
|
+
"""
|
|
54
|
+
Return a fresh correlation ID for linking related traces.
|
|
55
|
+
Example: corr_71ac4e19aaf0
|
|
56
|
+
|
|
57
|
+
A correlation ID is passed externally (e.g. from a request header or a
|
|
58
|
+
job queue message) rather than generated internally most of the time.
|
|
59
|
+
This function exists for cases where the caller wants to start a new
|
|
60
|
+
correlation group and needs a fresh ID to assign.
|
|
61
|
+
"""
|
|
62
|
+
return _generate_id("corr")
|