traceact 0.2.1__tar.gz → 0.3.0__tar.gz
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-0.2.1/traceact.egg-info → traceact-0.3.0}/PKG-INFO +16 -1
- {traceact-0.2.1 → traceact-0.3.0}/README.md +15 -0
- {traceact-0.2.1 → traceact-0.3.0}/USAGE.md +311 -22
- {traceact-0.2.1 → traceact-0.3.0}/pyproject.toml +5 -1
- traceact-0.3.0/tests/test_decorators.py +146 -0
- traceact-0.3.0/tests/test_doctor.py +85 -0
- traceact-0.3.0/tests/test_reader.py +109 -0
- traceact-0.3.0/tests/test_redaction.py +141 -0
- traceact-0.3.0/tests/test_sinks.py +69 -0
- {traceact-0.2.1 → traceact-0.3.0}/traceact/__init__.py +6 -1
- {traceact-0.2.1 → traceact-0.3.0}/traceact/config.py +36 -0
- {traceact-0.2.1 → traceact-0.3.0}/traceact/decorators.py +61 -12
- traceact-0.3.0/traceact/redaction.py +63 -0
- {traceact-0.2.1 → traceact-0.3.0}/traceact/sinks.py +40 -1
- {traceact-0.2.1 → traceact-0.3.0}/traceact/trace.py +67 -22
- {traceact-0.2.1 → traceact-0.3.0}/traceact/viewer/cli.py +48 -0
- traceact-0.3.0/traceact/viewer/doctor.py +184 -0
- {traceact-0.2.1 → traceact-0.3.0}/traceact/viewer/server.py +13 -0
- {traceact-0.2.1 → traceact-0.3.0}/traceact/viewer/static/app.js +94 -2
- {traceact-0.2.1 → traceact-0.3.0}/traceact/viewer/static/index.html +14 -0
- {traceact-0.2.1 → traceact-0.3.0}/traceact/viewer/static/styles.css +61 -0
- {traceact-0.2.1 → traceact-0.3.0/traceact.egg-info}/PKG-INFO +16 -1
- {traceact-0.2.1 → traceact-0.3.0}/traceact.egg-info/SOURCES.txt +7 -0
- {traceact-0.2.1 → traceact-0.3.0}/LICENSE +0 -0
- {traceact-0.2.1 → traceact-0.3.0}/MANIFEST.in +0 -0
- {traceact-0.2.1 → traceact-0.3.0}/setup.cfg +0 -0
- {traceact-0.2.1 → traceact-0.3.0}/traceact/budget.py +0 -0
- {traceact-0.2.1 → traceact-0.3.0}/traceact/context.py +0 -0
- {traceact-0.2.1 → traceact-0.3.0}/traceact/helpers.py +0 -0
- {traceact-0.2.1 → traceact-0.3.0}/traceact/ids.py +0 -0
- {traceact-0.2.1 → traceact-0.3.0}/traceact/viewer/__init__.py +0 -0
- {traceact-0.2.1 → traceact-0.3.0}/traceact/viewer/instance.py +0 -0
- {traceact-0.2.1 → traceact-0.3.0}/traceact/viewer/reader.py +0 -0
- {traceact-0.2.1 → traceact-0.3.0}/traceact.egg-info/dependency_links.txt +0 -0
- {traceact-0.2.1 → traceact-0.3.0}/traceact.egg-info/entry_points.txt +0 -0
- {traceact-0.2.1 → traceact-0.3.0}/traceact.egg-info/requires.txt +0 -0
- {traceact-0.2.1 → traceact-0.3.0}/traceact.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: traceact
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: X-ray vision for your code. Lightweight action-level tracing for Python.
|
|
5
5
|
Author: Mohammed Shehu
|
|
6
6
|
License-Expression: MIT
|
|
@@ -127,6 +127,14 @@ The source modal (click the source name in the header) lets you:
|
|
|
127
127
|
- **Drag and drop** a `.jsonl` file — saved as a static snapshot in `~/.traceact/imports/`
|
|
128
128
|
- **Type a path** — collapsible fallback for pasting an absolute path
|
|
129
129
|
|
|
130
|
+
### Health checks
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
traceact doctor [SOURCE]
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Checks Python version, that `~/.traceact` is writable, whether a viewer is already running, and (if `SOURCE` is given) that the file or folder parses as valid trace data. Useful for ruling out setup problems before debugging your own code. The same checks are also available from the viewer itself — Settings > **Run diagnostics**. See [USAGE.md](USAGE.md#viewing-traces) for full output and exit-code details.
|
|
137
|
+
|
|
130
138
|
## Concepts
|
|
131
139
|
|
|
132
140
|
| Concept | Meaning |
|
|
@@ -174,6 +182,13 @@ document.getElementById("btn-viewer").addEventListener("click", async () => {
|
|
|
174
182
|
|
|
175
183
|
Python 3.9+. No runtime dependencies.
|
|
176
184
|
|
|
185
|
+
## Development
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
pip install -e ".[dev]"
|
|
189
|
+
pytest
|
|
190
|
+
```
|
|
191
|
+
|
|
177
192
|
## Full reference
|
|
178
193
|
|
|
179
194
|
See [USAGE.md](USAGE.md) for complete API documentation: all decorator and context manager parameters, helper methods (`trace.db`, `trace.http`, `trace.file`, `trace.model`), input capture, parent/child traces, sinks, budget configuration, the trace record schema, test isolation, and the full viewer server API.
|
|
@@ -97,6 +97,14 @@ The source modal (click the source name in the header) lets you:
|
|
|
97
97
|
- **Drag and drop** a `.jsonl` file — saved as a static snapshot in `~/.traceact/imports/`
|
|
98
98
|
- **Type a path** — collapsible fallback for pasting an absolute path
|
|
99
99
|
|
|
100
|
+
### Health checks
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
traceact doctor [SOURCE]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Checks Python version, that `~/.traceact` is writable, whether a viewer is already running, and (if `SOURCE` is given) that the file or folder parses as valid trace data. Useful for ruling out setup problems before debugging your own code. The same checks are also available from the viewer itself — Settings > **Run diagnostics**. See [USAGE.md](USAGE.md#viewing-traces) for full output and exit-code details.
|
|
107
|
+
|
|
100
108
|
## Concepts
|
|
101
109
|
|
|
102
110
|
| Concept | Meaning |
|
|
@@ -144,6 +152,13 @@ document.getElementById("btn-viewer").addEventListener("click", async () => {
|
|
|
144
152
|
|
|
145
153
|
Python 3.9+. No runtime dependencies.
|
|
146
154
|
|
|
155
|
+
## Development
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
pip install -e ".[dev]"
|
|
159
|
+
pytest
|
|
160
|
+
```
|
|
161
|
+
|
|
147
162
|
## Full reference
|
|
148
163
|
|
|
149
164
|
See [USAGE.md](USAGE.md) for complete API documentation: all decorator and context manager parameters, helper methods (`trace.db`, `trace.http`, `trace.file`, `trace.model`), input capture, parent/child traces, sinks, budget configuration, the trace record schema, test isolation, and the full viewer server API.
|
|
@@ -12,20 +12,27 @@ traceact/
|
|
|
12
12
|
config.py — TraceConfig, configure(), reset_config()
|
|
13
13
|
budget.py — TraceBudget, TraceBudget.production() preset
|
|
14
14
|
context.py — ContextVar for active trace, SKIP sentinel
|
|
15
|
-
|
|
15
|
+
redaction.py — SENSITIVE_PATTERNS baseline + REDACTION_PRESETS registry
|
|
16
|
+
sinks.py — JsonlSink (thread-safe, rotation via max_bytes), ConsoleSink,
|
|
17
|
+
AsyncSink (not yet public)
|
|
16
18
|
helpers.py — TraceHelpersMixin (trace.db, trace.http, trace.file, trace.model)
|
|
17
19
|
ids.py — ID generation (trc_, evt_, stp_, corr_ prefixes)
|
|
18
20
|
|
|
19
21
|
viewer/
|
|
20
|
-
cli.py — `traceact view` / `traceact show` CLI entry point
|
|
22
|
+
cli.py — `traceact view` / `traceact show` / `traceact doctor` CLI entry point
|
|
21
23
|
server.py — stdlib ThreadingHTTPServer; SPA + REST + SSE
|
|
22
|
-
reader.py — SourceReader: JSONL snapshot + live byte-offset tail
|
|
24
|
+
reader.py — SourceReader: JSONL snapshot + live byte-offset tail,
|
|
25
|
+
with inode-based delete+recreate detection
|
|
26
|
+
doctor.py — run_checks(): shared health-check logic behind both
|
|
27
|
+
`traceact doctor` and GET /api/doctor (Settings > Run diagnostics)
|
|
23
28
|
instance.py — single-instance coordination (state file + HTTP probe);
|
|
24
29
|
launch_or_connect() for embedding in app backends
|
|
25
30
|
static/
|
|
26
31
|
index.html — single-page app shell
|
|
27
32
|
styles.css — design system (dark theme, CSS custom properties)
|
|
28
33
|
app.js — all frontend logic (no framework, no build step)
|
|
34
|
+
|
|
35
|
+
tests/ — pytest suite (pip install -e ".[dev]" && pytest)
|
|
29
36
|
```
|
|
30
37
|
|
|
31
38
|
## Contents
|
|
@@ -41,14 +48,16 @@ traceact/
|
|
|
41
48
|
9. [Touches](#touches)
|
|
42
49
|
10. [Errors](#errors)
|
|
43
50
|
11. [Parent and child traces](#parent-and-child-traces)
|
|
44
|
-
12. [
|
|
45
|
-
13. [
|
|
46
|
-
14. [
|
|
47
|
-
15. [
|
|
48
|
-
16. [
|
|
49
|
-
17. [
|
|
50
|
-
18. [
|
|
51
|
-
19. [
|
|
51
|
+
12. [Framework recipes](#framework-recipes)
|
|
52
|
+
13. [Background jobs and correlation IDs](#background-jobs-and-correlation-ids)
|
|
53
|
+
14. [Input capture](#input-capture)
|
|
54
|
+
15. [Sinks](#sinks)
|
|
55
|
+
16. [Budget configuration](#budget-configuration)
|
|
56
|
+
17. [TraceConfig fields](#traceconfig-fields)
|
|
57
|
+
18. [Test isolation](#test-isolation)
|
|
58
|
+
19. [Trace record schema](#trace-record-schema)
|
|
59
|
+
20. [Viewing traces](#viewing-traces)
|
|
60
|
+
21. [Integrating the viewer into your app](#integrating-the-viewer-into-your-app)
|
|
52
61
|
|
|
53
62
|
---
|
|
54
63
|
|
|
@@ -127,7 +136,7 @@ from traceact import traced_action, TraceConfig, TraceBudget
|
|
|
127
136
|
operation="insert", # creates an initial event if provided with target
|
|
128
137
|
target="notes", # the resource (table, endpoint, file, model)
|
|
129
138
|
database="sqlite", # for kind="db" traces
|
|
130
|
-
capture_inputs=
|
|
139
|
+
capture_inputs=True, # None (defer to package config) | False | True | ["field1", "field2"]
|
|
131
140
|
meta={"release": "v1.2"}, # arbitrary key-value data
|
|
132
141
|
config=TraceConfig(strict=True),# override package config for this trace only
|
|
133
142
|
budget=TraceBudget(max_events=50), # override budget for this trace only
|
|
@@ -393,9 +402,153 @@ When `sample_rate < 1.0` and a trace is sampled out, a skip sentinel is pushed o
|
|
|
393
402
|
|
|
394
403
|
---
|
|
395
404
|
|
|
405
|
+
## Framework recipes
|
|
406
|
+
|
|
407
|
+
`@traced_action` and `ActionTrace` work on any callable — there's no framework integration to install. The only decision per framework is *where* to call `configure()` and *which* functions to wrap. These are patterns, not new API surface.
|
|
408
|
+
|
|
409
|
+
### FastAPI
|
|
410
|
+
|
|
411
|
+
Call `configure()` once, at import time or in a startup event, then wrap your route handlers or (more usefully) the service functions they call:
|
|
412
|
+
|
|
413
|
+
```python
|
|
414
|
+
# app/tracing.py — imported once, before routes are registered
|
|
415
|
+
from traceact import configure, TraceConfig, JsonlSink
|
|
416
|
+
|
|
417
|
+
configure(
|
|
418
|
+
config=TraceConfig(sink_mode="blocking"),
|
|
419
|
+
sinks=[JsonlSink("data/traces/traces.jsonl")],
|
|
420
|
+
)
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
```python
|
|
424
|
+
# app/routers/notes.py
|
|
425
|
+
from fastapi import APIRouter
|
|
426
|
+
from traceact import traced_action
|
|
427
|
+
|
|
428
|
+
router = APIRouter()
|
|
429
|
+
|
|
430
|
+
@traced_action(action="note.create", kind="app", actor="user")
|
|
431
|
+
async def _create_note(title: str, body: str) -> dict:
|
|
432
|
+
...
|
|
433
|
+
return {"note_id": "note_123"}
|
|
434
|
+
|
|
435
|
+
@router.post("/notes")
|
|
436
|
+
async def create_note(payload: dict):
|
|
437
|
+
return await _create_note(payload["title"], payload["body"])
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Wrapping the inner service function (not the route handler directly) keeps the trace's `action` name stable even if you later rename the route or move it behind a different path. `correlation_id` is a good place to pass FastAPI's request ID if you have request-ID middleware:
|
|
441
|
+
|
|
442
|
+
```python
|
|
443
|
+
@traced_action(action="note.create", kind="app", correlation_id=request.state.request_id)
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
### Django
|
|
447
|
+
|
|
448
|
+
Django's request/response cycle is a natural boundary for `correlation_id`, and views or service functions are the natural place for `@traced_action`:
|
|
449
|
+
|
|
450
|
+
```python
|
|
451
|
+
# myapp/apps.py — configure once when the app is ready
|
|
452
|
+
from django.apps import AppConfig
|
|
453
|
+
from traceact import configure, TraceConfig, JsonlSink
|
|
454
|
+
|
|
455
|
+
class MyAppConfig(AppConfig):
|
|
456
|
+
name = "myapp"
|
|
457
|
+
|
|
458
|
+
def ready(self):
|
|
459
|
+
configure(
|
|
460
|
+
config=TraceConfig(sink_mode="blocking"),
|
|
461
|
+
sinks=[JsonlSink("data/traces/traces.jsonl")],
|
|
462
|
+
)
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
```python
|
|
466
|
+
# myapp/services.py
|
|
467
|
+
from traceact import traced_action
|
|
468
|
+
|
|
469
|
+
@traced_action(action="order.process", kind="app", actor="user")
|
|
470
|
+
def process_order(order_id: int) -> None:
|
|
471
|
+
...
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
```python
|
|
475
|
+
# myapp/views.py
|
|
476
|
+
from django.http import JsonResponse
|
|
477
|
+
from myapp.services import process_order
|
|
478
|
+
|
|
479
|
+
def process_order_view(request, order_id):
|
|
480
|
+
process_order(order_id)
|
|
481
|
+
return JsonResponse({"status": "ok"})
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
If you want every request to carry a correlation ID automatically (so all traces from one request share it), set it from middleware using Python's `contextvars` and read it in the view before calling the traced function — TraceAct doesn't read Django's request object itself, so the ID has to be passed explicitly into `correlation_id=`.
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
## Background jobs and correlation IDs
|
|
489
|
+
|
|
490
|
+
`correlation_id` links traces that belong to the same logical unit of work — a request, a job, a batch run — even when they happen across different function calls or processes. TraceAct doesn't generate or propagate it automatically across a queue boundary; the queue's message *is* the propagation mechanism, so you pass the ID through it like any other job argument.
|
|
491
|
+
|
|
492
|
+
**Enqueuing** — generate (or reuse) a correlation ID before the job goes on the queue:
|
|
493
|
+
|
|
494
|
+
```python
|
|
495
|
+
import uuid
|
|
496
|
+
from myqueue import enqueue
|
|
497
|
+
|
|
498
|
+
def start_export(user_id: int):
|
|
499
|
+
correlation_id = f"corr_{uuid.uuid4().hex[:12]}"
|
|
500
|
+
enqueue("export_report", user_id=user_id, correlation_id=correlation_id)
|
|
501
|
+
return correlation_id # e.g. return to the client so it can poll status
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
**Celery:**
|
|
505
|
+
|
|
506
|
+
```python
|
|
507
|
+
from celery import shared_task
|
|
508
|
+
from traceact import traced_action
|
|
509
|
+
|
|
510
|
+
@shared_task(name="export_report")
|
|
511
|
+
@traced_action(action="report.export", kind="job", actor="worker")
|
|
512
|
+
def export_report(user_id: int, correlation_id: str = None):
|
|
513
|
+
# correlation_id is picked up automatically by @traced_action's
|
|
514
|
+
# correlation_id kwarg only if passed through explicitly:
|
|
515
|
+
...
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
`@traced_action`'s `correlation_id` parameter isn't populated from task kwargs automatically — pass it explicitly so the decorator sees it:
|
|
519
|
+
|
|
520
|
+
```python
|
|
521
|
+
@shared_task(name="export_report")
|
|
522
|
+
def export_report(user_id: int, correlation_id: str = None):
|
|
523
|
+
_export_report(user_id, correlation_id=correlation_id)
|
|
524
|
+
|
|
525
|
+
@traced_action(action="report.export", kind="job", actor="worker")
|
|
526
|
+
def _export_report(user_id: int, correlation_id: str = None):
|
|
527
|
+
...
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
**RQ:**
|
|
531
|
+
|
|
532
|
+
```python
|
|
533
|
+
from rq import Queue
|
|
534
|
+
from traceact import traced_action
|
|
535
|
+
|
|
536
|
+
@traced_action(action="report.export", kind="job", actor="worker")
|
|
537
|
+
def export_report(user_id: int, correlation_id: str = None):
|
|
538
|
+
...
|
|
539
|
+
|
|
540
|
+
queue.enqueue(export_report, user_id=42, correlation_id="corr_abc123")
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
**Why this can't be automatic:** `contextvars.ContextVar` (which powers automatic parent/child linking within one process) does not cross a queue boundary — the worker that picks up the job runs in a different process with a fresh, empty context. The correlation ID has to travel as ordinary job data, the same way you'd pass any other argument the job needs.
|
|
544
|
+
|
|
545
|
+
Traces from the enqueue side and the worker side won't share a `parent_trace_id` (they're different processes, so there's no shared ContextVar to link them), but they will share `correlation_id`. The viewer's search box doesn't currently filter by `correlation_id` (it matches on action, kind, status, and touched targets); to pull together one job's traces today, use "Copy JSON" on a trace to get its `correlation_id`, then `grep` or `jq` the JSONL file for it: `jq 'select(.correlation_id == "corr_abc123")' data/traces/traces.jsonl`.
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
396
549
|
## Input capture
|
|
397
550
|
|
|
398
|
-
By default, `@traced_action` doesn't capture function arguments.
|
|
551
|
+
By default, `@traced_action` doesn't capture function arguments — `capture_inputs` defaults to `None`, which means "defer to the package-level setting from `configure()`, or no capture if that isn't set either."
|
|
399
552
|
|
|
400
553
|
**Capture selected fields (recommended):**
|
|
401
554
|
|
|
@@ -409,7 +562,7 @@ def create_note(title, body, user_id):
|
|
|
409
562
|
...
|
|
410
563
|
```
|
|
411
564
|
|
|
412
|
-
**Capture all arguments:**
|
|
565
|
+
**Capture all arguments (per decorator):**
|
|
413
566
|
|
|
414
567
|
```python
|
|
415
568
|
@traced_action(action="note.create", kind="app", capture_inputs=True)
|
|
@@ -417,7 +570,19 @@ def create_note(title, body, user_id):
|
|
|
417
570
|
...
|
|
418
571
|
```
|
|
419
572
|
|
|
420
|
-
**
|
|
573
|
+
**Capture all arguments (package-wide default):**
|
|
574
|
+
|
|
575
|
+
```python
|
|
576
|
+
configure(config=TraceConfig(capture_inputs=True))
|
|
577
|
+
|
|
578
|
+
@traced_action(action="note.create", kind="app") # no capture_inputs needed here
|
|
579
|
+
def create_note(title, body, user_id):
|
|
580
|
+
...
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
`capture_inputs=` on the decorator is shorthand for `config=TraceConfig(capture_inputs=...)` — both resolve through the same package-default → `configure()` → decorator-override chain, so a package-level default set via `configure()` is honoured by any decorator that doesn't explicitly override it.
|
|
584
|
+
|
|
585
|
+
**Global kill switch** (cannot be re-enabled by any decorator, not even one that explicitly passes `capture_inputs=True`):
|
|
421
586
|
|
|
422
587
|
```python
|
|
423
588
|
configure(config=TraceConfig(capture_inputs=False))
|
|
@@ -427,12 +592,59 @@ configure(config=TraceConfig(capture_inputs=False))
|
|
|
427
592
|
**What TraceAct does when capture is enabled:**
|
|
428
593
|
1. Maps positional args to parameter names using `inspect.signature()`.
|
|
429
594
|
2. Skips `self` and `cls`.
|
|
430
|
-
3. Redacts arguments whose names match sensitive patterns: `password`, `passwd`, `secret`, `token`, `api_key`, `apikey`, `private_key`, `privatekey`, `access_key`, `accesskey`, `auth`, `credential`, `credentials`, `credit_card`, `card_number`, `cvv`, `ssn`.
|
|
431
|
-
4.
|
|
432
|
-
5.
|
|
595
|
+
3. Redacts arguments whose names match sensitive patterns: `password`, `passwd`, `pwd`, `secret`, `token`, `api_key`, `apikey`, `private_key`, `privatekey`, `access_key`, `accesskey`, `auth`, `credential`, `credentials`, `credit_card`, `card_number`, `cvv`, `ssn`.
|
|
596
|
+
4. Recurses into nested dicts and lists-of-dicts, so a sensitive field buried inside a request body or config object is also redacted — not just top-level keys (see below).
|
|
597
|
+
5. Truncates values larger than `max_payload_bytes`.
|
|
598
|
+
6. Converts non-serialisable types to `[TypeName]`.
|
|
433
599
|
|
|
434
600
|
**`trace.input()` always works** regardless of the `capture_inputs` setting.
|
|
435
601
|
|
|
602
|
+
### Redaction presets
|
|
603
|
+
|
|
604
|
+
The pattern list above is always on. Layer additional field-name patterns on top of it with `redaction_presets`:
|
|
605
|
+
|
|
606
|
+
```python
|
|
607
|
+
from traceact import configure, TraceConfig, REDACTION_PRESETS
|
|
608
|
+
|
|
609
|
+
print(sorted(REDACTION_PRESETS))
|
|
610
|
+
# ['api_keys', 'env_vars', 'filesystem_paths', 'http']
|
|
611
|
+
|
|
612
|
+
configure(config=TraceConfig(redaction_presets=["filesystem_paths", "env_vars"]))
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
| Preset | Extra patterns |
|
|
616
|
+
|---|---|
|
|
617
|
+
| `"api_keys"` | `jwt`, `bearer`, `signing_key`, `encryption_key`, `hmac_key`, `master_key` |
|
|
618
|
+
| `"http"` | `cookie`, `set_cookie`, `session_id`, `csrf_token`, `x_forwarded_for`, `remote_addr`, `client_ip` |
|
|
619
|
+
| `"filesystem_paths"` | `path`, `filepath`, `file_path`, `dir`, `directory`, `workdir`, `cwd`, `home_dir`, `homedir` |
|
|
620
|
+
| `"env_vars"` | `env`, `environ`, `environment`, `envvar`, `env_var`, `dotenv` |
|
|
621
|
+
|
|
622
|
+
An unknown preset name raises `ValueError` immediately at `TraceConfig(...)` construction, not later at trace time.
|
|
623
|
+
|
|
624
|
+
Also settable per-decorator, same as any other `TraceConfig` field — but note that a decorator-level `redaction_presets` **replaces** the package-level list rather than adding to it, exactly like `capture_inputs` does:
|
|
625
|
+
|
|
626
|
+
```python
|
|
627
|
+
@traced_action(action="report.export", kind="app",
|
|
628
|
+
config=TraceConfig(redaction_presets=["api_keys"]))
|
|
629
|
+
def export_report(...):
|
|
630
|
+
...
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
**These are field-name patterns, not content scanning.** A value is redacted because of what its *key* is called, not what it contains. `trace.input({"path": "/Users/mo/secret"})` is redacted by the `filesystem_paths` preset; `trace.input({"location": "/Users/mo/secret"})` is not, because `"location"` doesn't match any active pattern. This mirrors the baseline mechanism exactly (same substring, case-insensitive matching) — it's simple and has no false-positive risk from scanning arbitrary string content, at the cost of missing secrets stored under an unexpected field name.
|
|
634
|
+
|
|
635
|
+
**Nested redaction example:**
|
|
636
|
+
|
|
637
|
+
```python
|
|
638
|
+
trace.input({
|
|
639
|
+
"request": {
|
|
640
|
+
"headers": {"authorization": "Bearer abc123"},
|
|
641
|
+
"body": {"user_id": 42},
|
|
642
|
+
},
|
|
643
|
+
})
|
|
644
|
+
# stored as:
|
|
645
|
+
# {"request": {"headers": {"authorization": "[redacted]"}, "body": {"user_id": 42}}}
|
|
646
|
+
```
|
|
647
|
+
|
|
436
648
|
---
|
|
437
649
|
|
|
438
650
|
## Sinks
|
|
@@ -448,6 +660,14 @@ JsonlSink("data/traces/traces.jsonl")
|
|
|
448
660
|
JsonlSink("/absolute/path/to/traces.jsonl")
|
|
449
661
|
```
|
|
450
662
|
|
|
663
|
+
**Rotation (`max_bytes`):** by default the file grows without limit. Pass `max_bytes` to cap the active file's size — once the next write would exceed it, the current file is renamed to `<path>.<UTC timestamp>` and a fresh file starts at `path`:
|
|
664
|
+
|
|
665
|
+
```python
|
|
666
|
+
JsonlSink("data/traces/traces.jsonl", max_bytes=50_000_000) # cap ~50 MB per file
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
Rotation renames rather than deletes, so history isn't lost — it's just no longer at `path`. Point the viewer at the containing **folder** rather than the single file to see the active file plus every rotated segment merged together: `traceact view data/traces/`. TraceAct doesn't currently delete old rotated segments on a schedule; clean them up yourself (e.g. a cron job or a retention script) if disk usage matters.
|
|
670
|
+
|
|
451
671
|
### ConsoleSink
|
|
452
672
|
|
|
453
673
|
Prints traces to stdout.
|
|
@@ -592,6 +812,13 @@ def test_create_note():
|
|
|
592
812
|
reset_config()
|
|
593
813
|
```
|
|
594
814
|
|
|
815
|
+
**TraceAct's own test suite** lives in `tests/` at the repo root and follows this exact pattern — a `_clean_config` autouse fixture in `tests/conftest.py` calls `reset_config()` before and after every test. Run it with:
|
|
816
|
+
|
|
817
|
+
```bash
|
|
818
|
+
pip install -e ".[dev]"
|
|
819
|
+
pytest
|
|
820
|
+
```
|
|
821
|
+
|
|
595
822
|
---
|
|
596
823
|
|
|
597
824
|
## Trace record schema
|
|
@@ -704,6 +931,34 @@ The viewer reads any line that parses as JSON and looks like a trace; malformed
|
|
|
704
931
|
|
|
705
932
|
You can also run it as a module: `python -m traceact.viewer.cli view SOURCE`.
|
|
706
933
|
|
|
934
|
+
### Health checks (`traceact doctor`)
|
|
935
|
+
|
|
936
|
+
```bash
|
|
937
|
+
traceact doctor [SOURCE]
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
Runs a handful of local checks and prints a pass/fail report — useful when tracing "isn't working" and you want to rule out setup problems before debugging your own code:
|
|
941
|
+
|
|
942
|
+
- Python version meets the 3.9 minimum
|
|
943
|
+
- the `~/.traceact` state directory exists and is writable (single-instance coordination and drag-drop imports depend on this)
|
|
944
|
+
- whether a viewer is currently running (informational only — `doctor` doesn't require one)
|
|
945
|
+
- if `SOURCE` is given: that the path exists and its lines parse as valid trace records
|
|
946
|
+
|
|
947
|
+
```
|
|
948
|
+
$ traceact doctor data/traces/traces.jsonl
|
|
949
|
+
traceact doctor
|
|
950
|
+
|
|
951
|
+
✓ Python 3.11 (OK, 3.9+ required)
|
|
952
|
+
· traceact 0.3.0
|
|
953
|
+
✓ State directory (/Users/you/.traceact) is writable
|
|
954
|
+
· No viewer currently running (not required).
|
|
955
|
+
✓ data/traces/traces.jsonl: 42/42 line(s) look like valid traces across 1 file(s)
|
|
956
|
+
|
|
957
|
+
All checks passed.
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
Exits `0` if every check that can fail passed, `1` otherwise. A missing running viewer is never treated as a failure.
|
|
961
|
+
|
|
707
962
|
### Single-instance behaviour
|
|
708
963
|
|
|
709
964
|
Running `traceact view` a second time — even for a different source — reuses a viewer that is already running rather than spawning a second server. The new source is added to the running viewer and a browser tab is opened on it. This avoids accumulating background processes across repeated launches during a dev session.
|
|
@@ -734,10 +989,14 @@ The Terminal window stays open so Ctrl+C stops the viewer cleanly. To pass a sou
|
|
|
734
989
|
|
|
735
990
|
### What the viewer shows
|
|
736
991
|
|
|
737
|
-
- **Trace log** — a live, newest-first table of traces (time, action, status, duration, and touch/error/budget counts). A search box filters by action, kind, status, or touched target. The row count is capped (25 / 50 / 100 / 250, default 100) and paired with live tailing, so the newest traces are always in view.
|
|
738
|
-
- **Trace inspector** — selecting a trace shows its own ID, its parent and root trace IDs (when it is a child trace), kind, duration, and touch/error counts. "Copy JSON" copies the full record.
|
|
992
|
+
- **Trace log** — a live, newest-first table of traces (time, action, status, duration, and touch/error/budget counts). A search box filters by action, kind, status, correlation ID, or touched target. The row count is capped (25 / 50 / 100 / 250, default 100) and paired with live tailing, so the newest traces are always in view.
|
|
993
|
+
- **Trace inspector** — selecting a trace shows its own ID, its parent and root trace IDs (when it is a child trace), correlation ID (when present, shown in full), kind, duration, and touch/error counts. "Copy JSON" copies the full record.
|
|
739
994
|
- **Trace map** — a visual of one trace: the action as origin, its events and resources as connected nodes, with per-node status and a red marker on failures. Plays as a sequential step-through replay, with a speed slider (1×–10×, live, persisted) and pause/play.
|
|
740
|
-
- **Settings** — accent colour, display density, default trace view, row count,
|
|
995
|
+
- **Settings** — accent colour, display density, default trace view, row count, default replay speed, and a **Run diagnostics** button — all persisted to `localStorage` except diagnostics, which runs fresh each time.
|
|
996
|
+
|
|
997
|
+
### Run diagnostics (Settings)
|
|
998
|
+
|
|
999
|
+
The Settings page has a "Run diagnostics" button that runs the exact same checks as `traceact doctor` on the command line, via `GET /api/doctor` — Python version, state directory writability, whether a viewer is running, and (if a source is loaded) whether its trace data looks valid. Results appear as a checklist with a short progress indicator, and each failing check shows a one-line explanation of what it means and what to do about it. Useful when something isn't showing up in the log and you want to rule out a setup problem without opening a terminal.
|
|
741
1000
|
|
|
742
1001
|
### Viewer server API
|
|
743
1002
|
|
|
@@ -745,13 +1004,16 @@ These endpoints are available while a viewer is running. Apps and scripts can ca
|
|
|
745
1004
|
|
|
746
1005
|
| Method | Path | Request | Response |
|
|
747
1006
|
|---|---|---|---|
|
|
748
|
-
| `GET` | `/api/health` | — | `{"status":"ok","version":"0.
|
|
1007
|
+
| `GET` | `/api/health` | — | `{"status":"ok","version":"0.3.0","sources":N}` |
|
|
1008
|
+
| `GET` | `/api/doctor?source=` | — | `{"ok":bool,"version":"...","checks":[{"label","status","message","hint"?}]}` |
|
|
749
1009
|
| `GET` | `/api/sources` | — | `[{"name":"...","path":"..."}]` |
|
|
750
1010
|
| `POST` | `/api/sources` | `{"path":"..."}` | `{"name":"...","path":"..."}` |
|
|
751
1011
|
| `GET` | `/api/pick?type=file\|folder` | — | `{"path":"...","cancelled":bool}` |
|
|
752
1012
|
| `POST` | `/api/import` | `{"name":"file.jsonl","content":"..."}` | `{"name":"...","path":"...","imported":true}` |
|
|
753
1013
|
| `GET` | `/api/stream?source=NAME&limit=N` | — | SSE stream: `snapshot` then `append` events |
|
|
754
1014
|
|
|
1015
|
+
`/api/doctor`'s `status` is `"pass"`, `"fail"`, or `"info"`; `hint` is present only on `"fail"` checks. `ok` is `true` only if every `"pass"`/`"fail"` check passed — `"info"` checks (traceact's version, whether a viewer is running) never affect it.
|
|
1016
|
+
|
|
755
1017
|
The SSE stream delivers one `snapshot` message (the last N traces as a JSON array) then `append` messages for each newly-written trace. A `": keepalive"` comment is sent every 0.5 s when nothing new arrives, to keep the connection alive through proxies.
|
|
756
1018
|
|
|
757
1019
|
### Notes
|
|
@@ -843,6 +1105,33 @@ async function openViewer() {
|
|
|
843
1105
|
|
|
844
1106
|
Note: cross-origin `fetch` to the viewer will always be blocked by CORS unless the viewer adds an `Access-Control-Allow-Origin` header (it doesn't, by design). Use `mode: "no-cors"` only to warm up the connection; don't try to read the response. The backend-route approach above is cleaner because it doesn't depend on the client being on the same machine as the viewer.
|
|
845
1107
|
|
|
1108
|
+
### Admin-safe viewer pattern
|
|
1109
|
+
|
|
1110
|
+
**The viewer has no authentication of its own** — no login, no token, no session check. This is intentional: it's a local dev tool, and adding auth would mean TraceAct owning credential storage, which it explicitly doesn't want to do. That means access control is entirely your responsibility if more than one trusted person can reach the machine it runs on.
|
|
1111
|
+
|
|
1112
|
+
**Rule of thumb:** never bind the viewer to a non-localhost host (`--host 0.0.0.0` or similar) unless you put your own authentication in front of it. Binding `127.0.0.1` (the default) means only processes on the same machine can reach it, which is the safe default for local development.
|
|
1113
|
+
|
|
1114
|
+
If you want a "traceact viewer" button inside an internal team tool (like the FastAPI/Flask examples above), gate the *button and the route*, not the viewer itself:
|
|
1115
|
+
|
|
1116
|
+
```python
|
|
1117
|
+
# The route that launches/connects to the viewer sits behind your app's
|
|
1118
|
+
# own admin-only auth dependency — the viewer itself stays on localhost
|
|
1119
|
+
# and is never exposed directly.
|
|
1120
|
+
from fastapi import Depends
|
|
1121
|
+
from traceact.viewer.instance import launch_or_connect
|
|
1122
|
+
|
|
1123
|
+
@router.get("/api/launch-viewer")
|
|
1124
|
+
async def launch_viewer(user=Depends(require_admin)):
|
|
1125
|
+
loop = asyncio.get_event_loop()
|
|
1126
|
+
url = await loop.run_in_executor(None, launch_or_connect,
|
|
1127
|
+
"data/traces/traces.jsonl")
|
|
1128
|
+
return {"url": url}
|
|
1129
|
+
```
|
|
1130
|
+
|
|
1131
|
+
Because `launch_or_connect` starts the viewer on `127.0.0.1` by default, the returned URL only works for someone on the same machine as the server process — appropriate for a local dev box, not for handing a link to a remote teammate. If your team needs to view traces from a shared/remote environment, don't punch the viewer through to the network; instead have each person run `traceact view` against a synced or shared copy of the trace file on their own machine, or wait for a future TraceAct release aimed at that use case (see Notes above — network-exposed viewing is explicitly out of scope for the current local-only design).
|
|
1132
|
+
|
|
1133
|
+
**Trace data itself may be sensitive.** Traces can contain request bodies, resource identifiers, and (if `capture_inputs` is misconfigured) unredacted arguments. Treat a running viewer, and the JSONL files it reads, with the same access discipline as application logs.
|
|
1134
|
+
|
|
846
1135
|
---
|
|
847
1136
|
|
|
848
1137
|
## Quick reference
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "traceact"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "X-ray vision for your code. Lightweight action-level tracing for Python."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -57,3 +57,7 @@ include = ["traceact*"]
|
|
|
57
57
|
# Ship the viewer's static frontend (HTML/CSS/JS) inside the wheel.
|
|
58
58
|
[tool.setuptools.package-data]
|
|
59
59
|
"traceact.viewer" = ["static/*"]
|
|
60
|
+
|
|
61
|
+
[tool.pytest.ini_options]
|
|
62
|
+
testpaths = ["tests"]
|
|
63
|
+
asyncio_mode = "auto"
|