traceact 0.4.0__tar.gz → 0.6.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.4.0/traceact.egg-info → traceact-0.6.0}/PKG-INFO +7 -2
- {traceact-0.4.0 → traceact-0.6.0}/README.md +1 -1
- {traceact-0.4.0 → traceact-0.6.0}/USAGE.md +217 -6
- {traceact-0.4.0 → traceact-0.6.0}/pyproject.toml +17 -1
- traceact-0.6.0/tests/test_propagation.py +873 -0
- {traceact-0.4.0 → traceact-0.6.0}/tests/test_redaction.py +1 -1
- {traceact-0.4.0 → traceact-0.6.0}/tests/test_tracelog.py +259 -0
- traceact-0.6.0/tests/test_viewer_query.py +436 -0
- {traceact-0.4.0 → traceact-0.6.0}/traceact/__init__.py +31 -1
- {traceact-0.4.0 → traceact-0.6.0}/traceact/config.py +12 -0
- {traceact-0.4.0 → traceact-0.6.0}/traceact/log.py +157 -9
- traceact-0.6.0/traceact/middleware.py +225 -0
- traceact-0.6.0/traceact/propagation.py +267 -0
- {traceact-0.4.0 → traceact-0.6.0}/traceact/redaction.py +12 -0
- {traceact-0.4.0 → traceact-0.6.0}/traceact/sinks.py +57 -16
- {traceact-0.4.0 → traceact-0.6.0}/traceact/trace.py +37 -0
- {traceact-0.4.0 → traceact-0.6.0}/traceact/viewer/server.py +112 -1
- {traceact-0.4.0 → traceact-0.6.0}/traceact/viewer/static/app.js +197 -5
- {traceact-0.4.0 → traceact-0.6.0}/traceact/viewer/static/index.html +18 -11
- {traceact-0.4.0 → traceact-0.6.0}/traceact/viewer/static/styles.css +55 -0
- {traceact-0.4.0 → traceact-0.6.0/traceact.egg-info}/PKG-INFO +7 -2
- {traceact-0.4.0 → traceact-0.6.0}/traceact.egg-info/SOURCES.txt +4 -0
- traceact-0.6.0/traceact.egg-info/requires.txt +9 -0
- traceact-0.4.0/traceact.egg-info/requires.txt +0 -4
- {traceact-0.4.0 → traceact-0.6.0}/LICENSE +0 -0
- {traceact-0.4.0 → traceact-0.6.0}/MANIFEST.in +0 -0
- {traceact-0.4.0 → traceact-0.6.0}/setup.cfg +0 -0
- {traceact-0.4.0 → traceact-0.6.0}/tests/test_async_sink.py +0 -0
- {traceact-0.4.0 → traceact-0.6.0}/tests/test_decorators.py +0 -0
- {traceact-0.4.0 → traceact-0.6.0}/tests/test_doctor.py +0 -0
- {traceact-0.4.0 → traceact-0.6.0}/tests/test_http_sink.py +0 -0
- {traceact-0.4.0 → traceact-0.6.0}/tests/test_otlp_sink.py +0 -0
- {traceact-0.4.0 → traceact-0.6.0}/tests/test_reader.py +0 -0
- {traceact-0.4.0 → traceact-0.6.0}/tests/test_sinks.py +0 -0
- {traceact-0.4.0 → traceact-0.6.0}/tests/test_sqlite_sink.py +0 -0
- {traceact-0.4.0 → traceact-0.6.0}/traceact/budget.py +0 -0
- {traceact-0.4.0 → traceact-0.6.0}/traceact/context.py +0 -0
- {traceact-0.4.0 → traceact-0.6.0}/traceact/decorators.py +0 -0
- {traceact-0.4.0 → traceact-0.6.0}/traceact/helpers.py +0 -0
- {traceact-0.4.0 → traceact-0.6.0}/traceact/ids.py +0 -0
- {traceact-0.4.0 → traceact-0.6.0}/traceact/viewer/__init__.py +0 -0
- {traceact-0.4.0 → traceact-0.6.0}/traceact/viewer/cli.py +0 -0
- {traceact-0.4.0 → traceact-0.6.0}/traceact/viewer/doctor.py +0 -0
- {traceact-0.4.0 → traceact-0.6.0}/traceact/viewer/instance.py +0 -0
- {traceact-0.4.0 → traceact-0.6.0}/traceact/viewer/reader.py +0 -0
- {traceact-0.4.0 → traceact-0.6.0}/traceact.egg-info/dependency_links.txt +0 -0
- {traceact-0.4.0 → traceact-0.6.0}/traceact.egg-info/entry_points.txt +0 -0
- {traceact-0.4.0 → traceact-0.6.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.6.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
|
|
@@ -26,6 +26,11 @@ License-File: LICENSE
|
|
|
26
26
|
Provides-Extra: dev
|
|
27
27
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
28
28
|
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-randomly>=3.15; extra == "dev"
|
|
30
|
+
Requires-Dist: flask>=3.0; extra == "dev"
|
|
31
|
+
Requires-Dist: starlette>=0.36; extra == "dev"
|
|
32
|
+
Requires-Dist: django>=4.2; extra == "dev"
|
|
33
|
+
Requires-Dist: requests>=2.31; extra == "dev"
|
|
29
34
|
Dynamic: license-file
|
|
30
35
|
|
|
31
36
|
# TraceAct
|
|
@@ -123,7 +128,7 @@ Double-click `launch.command` in the repo root to open TraceAct from Finder with
|
|
|
123
128
|
|
|
124
129
|
The source modal (click the source name in the header) lets you:
|
|
125
130
|
|
|
126
|
-
- **Choose file / Choose folder** — opens a native macOS picker; returns the
|
|
131
|
+
- **Choose file / Choose folder** — opens a native macOS picker; returns the filesystem path for live tailing
|
|
127
132
|
- **Drag and drop** a `.jsonl` file — saved as a static snapshot in `~/.traceact/imports/`
|
|
128
133
|
- **Type a path** — collapsible fallback for pasting an absolute path
|
|
129
134
|
|
|
@@ -93,7 +93,7 @@ Double-click `launch.command` in the repo root to open TraceAct from Finder with
|
|
|
93
93
|
|
|
94
94
|
The source modal (click the source name in the header) lets you:
|
|
95
95
|
|
|
96
|
-
- **Choose file / Choose folder** — opens a native macOS picker; returns the
|
|
96
|
+
- **Choose file / Choose folder** — opens a native macOS picker; returns the filesystem path for live tailing
|
|
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
|
|
|
@@ -17,6 +17,9 @@ traceact/
|
|
|
17
17
|
AsyncSink (background-thread wrapper; public as of v0.4)
|
|
18
18
|
helpers.py — TraceHelpersMixin (trace.db, trace.http, trace.file, trace.model)
|
|
19
19
|
ids.py — ID generation (trc_, evt_, stp_, corr_ prefixes)
|
|
20
|
+
propagation.py — extract_trace_id, inject_headers, propagate context manager,
|
|
21
|
+
_INCOMING_TRACE_ID ContextVar for cross-service correlation
|
|
22
|
+
middleware.py — TraceActMiddleware (WSGI), TraceActASGIMiddleware (ASGI)
|
|
20
23
|
|
|
21
24
|
viewer/
|
|
22
25
|
cli.py — `traceact view` / `traceact show` / `traceact doctor` CLI entry point
|
|
@@ -59,6 +62,7 @@ tests/ — pytest suite (pip install -e ".[dev]" && pytest)
|
|
|
59
62
|
20. [Trace record schema](#trace-record-schema)
|
|
60
63
|
21. [Viewing traces](#viewing-traces)
|
|
61
64
|
22. [Integrating the viewer into your app](#integrating-the-viewer-into-your-app)
|
|
65
|
+
23. [Distributed propagation](#distributed-propagation)
|
|
62
66
|
|
|
63
67
|
---
|
|
64
68
|
|
|
@@ -622,7 +626,7 @@ configure(config=TraceConfig(redaction_presets=["filesystem_paths", "env_vars"])
|
|
|
622
626
|
|
|
623
627
|
An unknown preset name raises `ValueError` immediately at `TraceConfig(...)` construction, not later at trace time.
|
|
624
628
|
|
|
625
|
-
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,
|
|
629
|
+
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, the same way `capture_inputs` does:
|
|
626
630
|
|
|
627
631
|
```python
|
|
628
632
|
@traced_action(action="report.export", kind="app",
|
|
@@ -631,7 +635,7 @@ def export_report(...):
|
|
|
631
635
|
...
|
|
632
636
|
```
|
|
633
637
|
|
|
634
|
-
**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
|
|
638
|
+
**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 (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.
|
|
635
639
|
|
|
636
640
|
**Nested redaction example:**
|
|
637
641
|
|
|
@@ -701,7 +705,7 @@ configure(sinks=[
|
|
|
701
705
|
])
|
|
702
706
|
```
|
|
703
707
|
|
|
704
|
-
**When to use it:** any time the inner sink is slow or remote — an HTTP collector, a database, or a high-latency filesystem. For local files on fast hardware,
|
|
708
|
+
**When to use it:** any time the inner sink is slow or remote — an HTTP collector, a database, or a high-latency filesystem. For local files on fast hardware, `JsonlSink` alone rarely needs the extra wrapping.
|
|
705
709
|
|
|
706
710
|
**Backpressure — and why drops are always observable:**
|
|
707
711
|
|
|
@@ -979,6 +983,32 @@ log.filter(status="failed").render_table(n=25) # cap rows shown
|
|
|
979
983
|
|
|
980
984
|
Each terminal call re-reads the JSONL file(s) — there is no caching, so you always get the current state of a live source.
|
|
981
985
|
|
|
986
|
+
`last()`/`first()` are memory-bounded: they hold at most `n` matching records per file at once rather than collecting every match before truncating. A broad filter (or no filter at all) over a large source costs memory proportional to `n`, not to how much of the source matches.
|
|
987
|
+
|
|
988
|
+
### query() — bounded result plus two completeness flags
|
|
989
|
+
|
|
990
|
+
```python
|
|
991
|
+
result = log.filter(status="failed").query(500)
|
|
992
|
+
result["traces"] # up to 500 matches, newest-first
|
|
993
|
+
result["scan_capped"] # True if max_lines_scanned stopped the scan early
|
|
994
|
+
result["limit_reached"] # True if more than 500 traces matched
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
Two separate reasons a result might not be every match that exists:
|
|
998
|
+
|
|
999
|
+
- `scan_capped` — the scan itself gave up early, per `max_lines_scanned`.
|
|
1000
|
+
- `limit_reached` — the scan finished (or found enough to stop), but `n` (or more) results matched — there may be more beyond what's returned. This is true of `last(n)` too; `last()` just has nowhere to report it, since it returns a plain list. Note that `len(result["traces"]) == n` alone can't tell you this — a bounded scan returns at most `n` regardless of whether `n` or a hundred thousand traces matched, so `limit_reached` is tracked from a count taken during the scan, not inferred from the result's length afterward.
|
|
1001
|
+
|
|
1002
|
+
Use `query()` instead of `last()` when the caller needs to distinguish those two situations — this is what the viewer's `/api/query` endpoint uses under the hood (see [Server-side search](#server-side-search-apiquery) above). `last()`/`first()` keep returning a plain list; `query()` is a separate method rather than a change to their return shape, so nothing about them breaks for existing callers.
|
|
1003
|
+
|
|
1004
|
+
### max_lines_scanned — bounding scan time
|
|
1005
|
+
|
|
1006
|
+
```python
|
|
1007
|
+
log = TraceLog("data/traces.jsonl", max_lines_scanned=200_000)
|
|
1008
|
+
```
|
|
1009
|
+
|
|
1010
|
+
Caps how many lines a scan reads (matched or not) before giving up and returning whatever was found, setting `scan_capped=True` on the next `query()` call. Defaults to `None` — unbounded, the same behaviour as before this existed. Set it when reading a source you don't control the size of, or when a single call needs a predictable worst-case cost (an HTTP handler, for instance — see below).
|
|
1011
|
+
|
|
982
1012
|
### Using TraceLog in tests
|
|
983
1013
|
|
|
984
1014
|
```python
|
|
@@ -1301,7 +1331,7 @@ Pass `--new` to bypass this and force a second instance — useful when you want
|
|
|
1301
1331
|
|
|
1302
1332
|
The "Add source" modal supports three ways to load a source:
|
|
1303
1333
|
|
|
1304
|
-
- **Choose file / Choose folder buttons** — opens a native macOS file or folder picker. The picker runs on the server side via AppleScript (`osascript`), so it returns the
|
|
1334
|
+
- **Choose file / Choose folder buttons** — opens a native macOS file or folder picker. The picker runs on the server side via AppleScript (`osascript`), so it returns the filesystem path and the viewer can tail it live. (Falls back to a `tkinter` dialog on non-macOS platforms.)
|
|
1305
1335
|
- **Drag and drop** — drop a `.jsonl` file onto the drop zone. The browser reads the file contents and posts them to the server, which saves a copy to `~/.traceact/imports/` and adds it as a source. Because the server holds a copy (not the original), this is a **static snapshot** — new writes to the original file won't appear. The viewer labels imported sources accordingly.
|
|
1306
1336
|
- **Type a path** — a collapsible text input for pasting or typing an absolute path. This gives live tailing just like the command-line argument.
|
|
1307
1337
|
|
|
@@ -1315,11 +1345,11 @@ The "Add source" modal supports three ways to load a source:
|
|
|
1315
1345
|
4. Installs or upgrades `traceact` inside that venv.
|
|
1316
1346
|
5. Runs `traceact view`, waits for the server to be ready, then opens the browser.
|
|
1317
1347
|
|
|
1318
|
-
The Terminal window stays open so Ctrl+C stops the viewer
|
|
1348
|
+
The Terminal window stays open so Ctrl+C stops the viewer. To pass a source file at launch from a script: `open launch.command path/to/traces.jsonl`.
|
|
1319
1349
|
|
|
1320
1350
|
### What the viewer shows
|
|
1321
1351
|
|
|
1322
|
-
- **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.
|
|
1352
|
+
- **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, against the currently tailed rows. The row count is capped (25 / 50 / 100 / 250, default 100) and paired with live tailing, so the newest traces are always in view. A pre-filtered view opened via `TraceLog.view()` instead searches the full source on disk — see [Server-side search](#server-side-search-apiquery) below.
|
|
1323
1353
|
- **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.
|
|
1324
1354
|
- **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.
|
|
1325
1355
|
- **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.
|
|
@@ -1341,11 +1371,29 @@ These endpoints are available while a viewer is running. Apps and scripts can ca
|
|
|
1341
1371
|
| `GET` | `/api/pick?type=file\|folder` | — | `{"path":"...","cancelled":bool}` |
|
|
1342
1372
|
| `POST` | `/api/import` | `{"name":"file.jsonl","content":"..."}` | `{"name":"...","path":"...","imported":true}` |
|
|
1343
1373
|
| `GET` | `/api/stream?source=NAME&limit=N` | — | SSE stream: `snapshot` then `append` events |
|
|
1374
|
+
| `GET` | `/api/query?source=NAME&field[__op]=value&limit=N` | — | `{"traces":[...],"scan_capped":bool,"limit_reached":bool,"count":N}` |
|
|
1344
1375
|
|
|
1345
1376
|
`/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.
|
|
1346
1377
|
|
|
1347
1378
|
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.
|
|
1348
1379
|
|
|
1380
|
+
### Server-side search (`/api/query`)
|
|
1381
|
+
|
|
1382
|
+
The trace log's search box and the row-limit setting both operate on the live-tailed buffer — whatever the last N traces happen to be. That's fine for the search box (a quick, instant, client-side filter over what's currently in view), but a `TraceLog.view()` pre-filter is a precise, deliberately-specified query — it needs to find its match regardless of whether that match is still inside the tail window. `/api/query` answers filters against the whole source on disk, via `TraceLog`, and the viewer routes pre-filters through it automatically. You don't need to call it directly for that to happen.
|
|
1383
|
+
|
|
1384
|
+
```
|
|
1385
|
+
GET /api/query?source=traces&status=failed&action__contains=order&limit=200
|
|
1386
|
+
```
|
|
1387
|
+
|
|
1388
|
+
- Every query param except `source` and `limit` is a filter field, in the same `field` / `field__contains` / `field__startswith` / `field__endswith` form as `TraceLog.filter()`. Multiple params are ANDed, same as chaining `.filter()` calls.
|
|
1389
|
+
- `__re` is not accepted here — it's rejected with `400`. `TraceLog.filter(field__re=...)` only makes sense when the pattern comes from trusted code; over HTTP it's arbitrary caller-supplied input, and a catastrophic-backtracking pattern could hang the request. Use `__re` directly against `TraceLog` in Python instead.
|
|
1390
|
+
- **`limit` is hard-capped at 1000 server-side.** Requesting `limit=5000` against a source with 3000 matches returns only the newest 1000 — but not silently: `count` in the response is the count *returned*, not the count that matched, and `limit_reached` (below) tells you whether more may exist.
|
|
1391
|
+
- The response carries two separate completeness flags, both `false` when the result is everything that matched:
|
|
1392
|
+
- **`scan_capped`** — `true` if the scan hit its internal line-read ceiling before finishing reading the source.
|
|
1393
|
+
- **`limit_reached`** — `true` if more traces matched than `limit` allowed back (including when a too-large requested `limit` was clamped to 1000). `count == limit` is not itself a safe signal that nothing more exists — a bounded scan always returns at most `limit` regardless of whether `limit` or a hundred thousand traces matched, so this comes from a count taken during the scan, not from inspecting the returned list's length afterward.
|
|
1394
|
+
|
|
1395
|
+
Either flag `true` means the same thing to a caller: this may not be every match. The viewer shows "results may be incomplete" next to the pre-filter badges when either is set, rather than presenting a partial result as if it were exhaustive.
|
|
1396
|
+
|
|
1349
1397
|
### Notes
|
|
1350
1398
|
|
|
1351
1399
|
- The viewer is a **local, single-node development tool**. It reads files on the machine it runs on. Exposing one machine's traces to another over the network (`traceact serve`) is planned for a later version.
|
|
@@ -1464,6 +1512,165 @@ Because `launch_or_connect` starts the viewer on `127.0.0.1` by default, the ret
|
|
|
1464
1512
|
|
|
1465
1513
|
---
|
|
1466
1514
|
|
|
1515
|
+
## Distributed propagation
|
|
1516
|
+
|
|
1517
|
+
When a traced action in Service A calls Service B, TraceAct links the two
|
|
1518
|
+
services' traces together with two separate HTTP headers — separate because
|
|
1519
|
+
they answer separate questions:
|
|
1520
|
+
|
|
1521
|
+
| Header | Sets on the receiving trace | Answers |
|
|
1522
|
+
|---|---|---|
|
|
1523
|
+
| `traceact-trace-id` | `upstream_trace_id` | "which trace in another service triggered me?" (causal lineage) |
|
|
1524
|
+
| `traceact-correlation-id` | `correlation_id` | "which wider workflow do I belong to?" (business grouping, passed through untouched) |
|
|
1525
|
+
|
|
1526
|
+
Keeping them separate matters: a trace can have an upstream parent in one
|
|
1527
|
+
service *and* belong to a correlation group that was assigned several hops
|
|
1528
|
+
earlier. Folding one into the other silently discards whichever one loses.
|
|
1529
|
+
|
|
1530
|
+
### Outbound: stamp both headers
|
|
1531
|
+
|
|
1532
|
+
`inject_headers()` reads the active trace and stamps its `trace_id` (always)
|
|
1533
|
+
and its `correlation_id` (if set) onto an outbound headers dict. It never
|
|
1534
|
+
mutates the dict you pass in.
|
|
1535
|
+
|
|
1536
|
+
```python
|
|
1537
|
+
import requests
|
|
1538
|
+
from traceact import inject_headers
|
|
1539
|
+
|
|
1540
|
+
with ActionTrace.start(action="order.submit", correlation_id="corr_wf_9f2a") as trace:
|
|
1541
|
+
headers = inject_headers({"Content-Type": "application/json"})
|
|
1542
|
+
requests.post("https://payments.internal/charge", json=payload, headers=headers)
|
|
1543
|
+
```
|
|
1544
|
+
|
|
1545
|
+
Works the same with `httpx`, `urllib`, or any other HTTP client — `inject_headers`
|
|
1546
|
+
just returns a plain dict.
|
|
1547
|
+
|
|
1548
|
+
### Inbound: extract the headers (manual)
|
|
1549
|
+
|
|
1550
|
+
Use the `propagate` context manager when you want explicit control, or when
|
|
1551
|
+
your framework isn't covered by the automatic middleware. Pass the framework's
|
|
1552
|
+
header object **directly** — `request.headers` works as-is on Flask, Django,
|
|
1553
|
+
FastAPI, and Starlette, and so does a plain dict in any casing:
|
|
1554
|
+
|
|
1555
|
+
```python
|
|
1556
|
+
from traceact import propagate, ActionTrace
|
|
1557
|
+
|
|
1558
|
+
def handle_charge(request):
|
|
1559
|
+
with propagate(request.headers):
|
|
1560
|
+
with ActionTrace.start(action="charge.process") as trace:
|
|
1561
|
+
# trace.upstream_trace_id == Service A's trace_id
|
|
1562
|
+
# trace.correlation_id == whatever correlation Service A had
|
|
1563
|
+
...
|
|
1564
|
+
```
|
|
1565
|
+
|
|
1566
|
+
HTTP header names are case-insensitive and every framework reconstructs them
|
|
1567
|
+
differently (Flask/Werkzeug and Django hand back `Traceact-Trace-Id`,
|
|
1568
|
+
Title-Case; ASGI delivers raw lowercase bytes). `propagate()` and
|
|
1569
|
+
`extract_trace_id()`/`extract_correlation_id()` normalise all of that
|
|
1570
|
+
internally — pass the header object however your framework gives it to you,
|
|
1571
|
+
including `dict(request.headers)` if you already have that.
|
|
1572
|
+
|
|
1573
|
+
### Inbound: automatic via middleware (Flask / Django)
|
|
1574
|
+
|
|
1575
|
+
```python
|
|
1576
|
+
from traceact import TraceActMiddleware
|
|
1577
|
+
|
|
1578
|
+
# Flask
|
|
1579
|
+
app.wsgi_app = TraceActMiddleware(app.wsgi_app)
|
|
1580
|
+
|
|
1581
|
+
# Django (in wsgi.py)
|
|
1582
|
+
from django.core.wsgi import get_wsgi_application
|
|
1583
|
+
application = TraceActMiddleware(get_wsgi_application())
|
|
1584
|
+
```
|
|
1585
|
+
|
|
1586
|
+
Reads both headers from the WSGI environ and applies them to every trace
|
|
1587
|
+
started during that request — including traces started while a streamed
|
|
1588
|
+
response body is generated (the context is held until the response is fully
|
|
1589
|
+
closed, not just until the view function returns). If neither header is
|
|
1590
|
+
present, the middleware is fully transparent.
|
|
1591
|
+
|
|
1592
|
+
### Inbound: automatic via middleware (FastAPI / Starlette)
|
|
1593
|
+
|
|
1594
|
+
```python
|
|
1595
|
+
from traceact import TraceActASGIMiddleware
|
|
1596
|
+
from fastapi import FastAPI
|
|
1597
|
+
|
|
1598
|
+
app = FastAPI()
|
|
1599
|
+
app.add_middleware(TraceActASGIMiddleware)
|
|
1600
|
+
```
|
|
1601
|
+
|
|
1602
|
+
Or wrap manually:
|
|
1603
|
+
|
|
1604
|
+
```python
|
|
1605
|
+
app = TraceActASGIMiddleware(app)
|
|
1606
|
+
```
|
|
1607
|
+
|
|
1608
|
+
### How the chain looks in the viewer and TraceLog
|
|
1609
|
+
|
|
1610
|
+
Both services write their own trace records. Query by whichever link answers
|
|
1611
|
+
your question — `upstream_trace_id` for "what did this specific call trigger",
|
|
1612
|
+
`correlation_id` for "show me the whole workflow":
|
|
1613
|
+
|
|
1614
|
+
```python
|
|
1615
|
+
from traceact import TraceLog
|
|
1616
|
+
|
|
1617
|
+
log = TraceLog("traces.jsonl")
|
|
1618
|
+
log.filter(upstream_trace_id="trc_abc123").all() # traces this call triggered
|
|
1619
|
+
log.filter(correlation_id="corr_wf_9f2a").all() # the entire workflow
|
|
1620
|
+
log.filter(correlation_id="corr_wf_9f2a").view() # same, in the browser viewer
|
|
1621
|
+
```
|
|
1622
|
+
|
|
1623
|
+
The inspector's summary card shows both when present, each in full (not
|
|
1624
|
+
shortened) so they can be copied and searched against another service's logs.
|
|
1625
|
+
|
|
1626
|
+
### `ai_prompts` redaction preset
|
|
1627
|
+
|
|
1628
|
+
For AI pipelines where trace payloads must not store raw prompt text, model
|
|
1629
|
+
responses, or conversation history, enable the `ai_prompts` preset:
|
|
1630
|
+
|
|
1631
|
+
```python
|
|
1632
|
+
configure(
|
|
1633
|
+
config=TraceConfig(redaction_presets=["ai_prompts"]),
|
|
1634
|
+
)
|
|
1635
|
+
```
|
|
1636
|
+
|
|
1637
|
+
Redacted fields include: `raw_prompt`, `prompt_content`, `system_prompt`,
|
|
1638
|
+
`raw_response`, `response_content`, `conversation`, `message_content`,
|
|
1639
|
+
`file_content`, `source_excerpt`, `context_window`, `completion`,
|
|
1640
|
+
`generation`, `output_text`. Safe fields like `model`, `latency_ms`,
|
|
1641
|
+
`prompt_id`, and token counts are unaffected.
|
|
1642
|
+
|
|
1643
|
+
| What gets redacted | What stays |
|
|
1644
|
+
|---|---|
|
|
1645
|
+
| Raw prompts and responses | Model name, version |
|
|
1646
|
+
| System prompts | Token counts (as long as field name has no "token" substring) |
|
|
1647
|
+
| Conversation history | Latency, cost |
|
|
1648
|
+
| File and source excerpts | Trace IDs, correlation IDs |
|
|
1649
|
+
|
|
1650
|
+
Combine with other presets:
|
|
1651
|
+
|
|
1652
|
+
```python
|
|
1653
|
+
TraceConfig(redaction_presets=["ai_prompts", "api_keys"])
|
|
1654
|
+
```
|
|
1655
|
+
|
|
1656
|
+
### Reference: headers
|
|
1657
|
+
|
|
1658
|
+
| Header | Outbound (inject) | Inbound (extract) |
|
|
1659
|
+
|---|---|---|
|
|
1660
|
+
| `traceact-trace-id` | Active `ActionTrace.trace_id` | Sets `upstream_trace_id` |
|
|
1661
|
+
| `traceact-correlation-id` | Active `ActionTrace.correlation_id`, if set | Sets `correlation_id` |
|
|
1662
|
+
|
|
1663
|
+
Accepted header collection types on the inbound side: any object with
|
|
1664
|
+
`.items()` (dict in any casing, Werkzeug/Django/Starlette header objects,
|
|
1665
|
+
`requests.CaseInsensitiveDict`), a list of `(name, value)` pairs, or raw ASGI
|
|
1666
|
+
`[(b"name", b"value")]` byte pairs. Matching is case-insensitive regardless of
|
|
1667
|
+
which form you pass.
|
|
1668
|
+
|
|
1669
|
+
WSGI: the header arrives as `HTTP_TRACEACT_TRACE_ID` in the environ.
|
|
1670
|
+
ASGI: the header arrives as bytes in `scope["headers"]`.
|
|
1671
|
+
|
|
1672
|
+
---
|
|
1673
|
+
|
|
1467
1674
|
## Quick reference
|
|
1468
1675
|
|
|
1469
1676
|
```python
|
|
@@ -1476,5 +1683,9 @@ from traceact import (
|
|
|
1476
1683
|
traced_action, # decorator
|
|
1477
1684
|
JsonlSink, # write traces to a .jsonl file
|
|
1478
1685
|
ConsoleSink, # print traces to stdout
|
|
1686
|
+
propagate, # context manager for inbound propagation
|
|
1687
|
+
inject_headers, # stamp outbound request headers
|
|
1688
|
+
TraceActMiddleware, # WSGI auto-propagation (Flask, Django)
|
|
1689
|
+
TraceActASGIMiddleware, # ASGI auto-propagation (FastAPI, Starlette)
|
|
1479
1690
|
)
|
|
1480
1691
|
```
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "traceact"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.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"
|
|
@@ -35,6 +35,22 @@ dependencies = []
|
|
|
35
35
|
dev = [
|
|
36
36
|
"pytest>=7.0",
|
|
37
37
|
"pytest-asyncio>=0.21",
|
|
38
|
+
# Test-order randomisation. Activates automatically once installed — no
|
|
39
|
+
# flag needed. Kept on deliberately: an order-dependent failure means
|
|
40
|
+
# shared/global state is leaking between tests, which is a real bug.
|
|
41
|
+
# Never pin --randomly-seed in CI to "stabilise" a run; that hides exactly
|
|
42
|
+
# the failure mode this plugin exists to catch.
|
|
43
|
+
"pytest-randomly>=3.15",
|
|
44
|
+
# Real framework objects for propagation tests (extract_trace_id,
|
|
45
|
+
# TraceActMiddleware, TraceActASGIMiddleware). These are test-only —
|
|
46
|
+
# traceact itself has zero runtime dependencies. Testing against a
|
|
47
|
+
# hand-built dict instead of an actual Flask/Django/Starlette request
|
|
48
|
+
# object was exactly the gap that let a header-parsing bug through
|
|
49
|
+
# 43 green tests; these are here so that never happens again.
|
|
50
|
+
"flask>=3.0",
|
|
51
|
+
"starlette>=0.36",
|
|
52
|
+
"django>=4.2",
|
|
53
|
+
"requests>=2.31",
|
|
38
54
|
]
|
|
39
55
|
|
|
40
56
|
[project.urls]
|