traceact 0.4.0__tar.gz → 0.5.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.5.0}/PKG-INFO +6 -1
- {traceact-0.4.0 → traceact-0.5.0}/USAGE.md +167 -0
- {traceact-0.4.0 → traceact-0.5.0}/pyproject.toml +17 -1
- traceact-0.5.0/tests/test_propagation.py +873 -0
- {traceact-0.4.0 → traceact-0.5.0}/tests/test_redaction.py +1 -1
- {traceact-0.4.0 → traceact-0.5.0}/traceact/__init__.py +31 -1
- {traceact-0.4.0 → traceact-0.5.0}/traceact/config.py +12 -0
- traceact-0.5.0/traceact/middleware.py +225 -0
- traceact-0.5.0/traceact/propagation.py +267 -0
- {traceact-0.4.0 → traceact-0.5.0}/traceact/redaction.py +12 -0
- {traceact-0.4.0 → traceact-0.5.0}/traceact/sinks.py +57 -16
- {traceact-0.4.0 → traceact-0.5.0}/traceact/trace.py +37 -0
- {traceact-0.4.0 → traceact-0.5.0}/traceact/viewer/static/app.js +107 -2
- {traceact-0.4.0 → traceact-0.5.0}/traceact/viewer/static/index.html +18 -11
- {traceact-0.4.0 → traceact-0.5.0}/traceact/viewer/static/styles.css +42 -0
- {traceact-0.4.0 → traceact-0.5.0/traceact.egg-info}/PKG-INFO +6 -1
- {traceact-0.4.0 → traceact-0.5.0}/traceact.egg-info/SOURCES.txt +3 -0
- traceact-0.5.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.5.0}/LICENSE +0 -0
- {traceact-0.4.0 → traceact-0.5.0}/MANIFEST.in +0 -0
- {traceact-0.4.0 → traceact-0.5.0}/README.md +0 -0
- {traceact-0.4.0 → traceact-0.5.0}/setup.cfg +0 -0
- {traceact-0.4.0 → traceact-0.5.0}/tests/test_async_sink.py +0 -0
- {traceact-0.4.0 → traceact-0.5.0}/tests/test_decorators.py +0 -0
- {traceact-0.4.0 → traceact-0.5.0}/tests/test_doctor.py +0 -0
- {traceact-0.4.0 → traceact-0.5.0}/tests/test_http_sink.py +0 -0
- {traceact-0.4.0 → traceact-0.5.0}/tests/test_otlp_sink.py +0 -0
- {traceact-0.4.0 → traceact-0.5.0}/tests/test_reader.py +0 -0
- {traceact-0.4.0 → traceact-0.5.0}/tests/test_sinks.py +0 -0
- {traceact-0.4.0 → traceact-0.5.0}/tests/test_sqlite_sink.py +0 -0
- {traceact-0.4.0 → traceact-0.5.0}/tests/test_tracelog.py +0 -0
- {traceact-0.4.0 → traceact-0.5.0}/traceact/budget.py +0 -0
- {traceact-0.4.0 → traceact-0.5.0}/traceact/context.py +0 -0
- {traceact-0.4.0 → traceact-0.5.0}/traceact/decorators.py +0 -0
- {traceact-0.4.0 → traceact-0.5.0}/traceact/helpers.py +0 -0
- {traceact-0.4.0 → traceact-0.5.0}/traceact/ids.py +0 -0
- {traceact-0.4.0 → traceact-0.5.0}/traceact/log.py +0 -0
- {traceact-0.4.0 → traceact-0.5.0}/traceact/viewer/__init__.py +0 -0
- {traceact-0.4.0 → traceact-0.5.0}/traceact/viewer/cli.py +0 -0
- {traceact-0.4.0 → traceact-0.5.0}/traceact/viewer/doctor.py +0 -0
- {traceact-0.4.0 → traceact-0.5.0}/traceact/viewer/instance.py +0 -0
- {traceact-0.4.0 → traceact-0.5.0}/traceact/viewer/reader.py +0 -0
- {traceact-0.4.0 → traceact-0.5.0}/traceact/viewer/server.py +0 -0
- {traceact-0.4.0 → traceact-0.5.0}/traceact.egg-info/dependency_links.txt +0 -0
- {traceact-0.4.0 → traceact-0.5.0}/traceact.egg-info/entry_points.txt +0 -0
- {traceact-0.4.0 → traceact-0.5.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.5.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
|
|
@@ -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
|
|
|
@@ -1464,6 +1468,165 @@ Because `launch_or_connect` starts the viewer on `127.0.0.1` by default, the ret
|
|
|
1464
1468
|
|
|
1465
1469
|
---
|
|
1466
1470
|
|
|
1471
|
+
## Distributed propagation
|
|
1472
|
+
|
|
1473
|
+
When a traced action in Service A calls Service B, TraceAct links the two
|
|
1474
|
+
services' traces together with two separate HTTP headers — separate because
|
|
1475
|
+
they answer separate questions:
|
|
1476
|
+
|
|
1477
|
+
| Header | Sets on the receiving trace | Answers |
|
|
1478
|
+
|---|---|---|
|
|
1479
|
+
| `traceact-trace-id` | `upstream_trace_id` | "which trace in another service triggered me?" (causal lineage) |
|
|
1480
|
+
| `traceact-correlation-id` | `correlation_id` | "which wider workflow do I belong to?" (business grouping, passed through untouched) |
|
|
1481
|
+
|
|
1482
|
+
Keeping them separate matters: a trace can have an upstream parent in one
|
|
1483
|
+
service *and* belong to a correlation group that was assigned several hops
|
|
1484
|
+
earlier. Folding one into the other silently discards whichever one loses.
|
|
1485
|
+
|
|
1486
|
+
### Outbound: stamp both headers
|
|
1487
|
+
|
|
1488
|
+
`inject_headers()` reads the active trace and stamps its `trace_id` (always)
|
|
1489
|
+
and its `correlation_id` (if set) onto an outbound headers dict. It never
|
|
1490
|
+
mutates the dict you pass in.
|
|
1491
|
+
|
|
1492
|
+
```python
|
|
1493
|
+
import requests
|
|
1494
|
+
from traceact import inject_headers
|
|
1495
|
+
|
|
1496
|
+
with ActionTrace.start(action="order.submit", correlation_id="corr_wf_9f2a") as trace:
|
|
1497
|
+
headers = inject_headers({"Content-Type": "application/json"})
|
|
1498
|
+
requests.post("https://payments.internal/charge", json=payload, headers=headers)
|
|
1499
|
+
```
|
|
1500
|
+
|
|
1501
|
+
Works the same with `httpx`, `urllib`, or any other HTTP client — `inject_headers`
|
|
1502
|
+
just returns a plain dict.
|
|
1503
|
+
|
|
1504
|
+
### Inbound: extract the headers (manual)
|
|
1505
|
+
|
|
1506
|
+
Use the `propagate` context manager when you want explicit control, or when
|
|
1507
|
+
your framework isn't covered by the automatic middleware. Pass the framework's
|
|
1508
|
+
header object **directly** — `request.headers` works as-is on Flask, Django,
|
|
1509
|
+
FastAPI, and Starlette, and so does a plain dict in any casing:
|
|
1510
|
+
|
|
1511
|
+
```python
|
|
1512
|
+
from traceact import propagate, ActionTrace
|
|
1513
|
+
|
|
1514
|
+
def handle_charge(request):
|
|
1515
|
+
with propagate(request.headers):
|
|
1516
|
+
with ActionTrace.start(action="charge.process") as trace:
|
|
1517
|
+
# trace.upstream_trace_id == Service A's trace_id
|
|
1518
|
+
# trace.correlation_id == whatever correlation Service A had
|
|
1519
|
+
...
|
|
1520
|
+
```
|
|
1521
|
+
|
|
1522
|
+
HTTP header names are case-insensitive and every framework reconstructs them
|
|
1523
|
+
differently (Flask/Werkzeug and Django hand back `Traceact-Trace-Id`,
|
|
1524
|
+
Title-Case; ASGI delivers raw lowercase bytes). `propagate()` and
|
|
1525
|
+
`extract_trace_id()`/`extract_correlation_id()` normalise all of that
|
|
1526
|
+
internally — pass the header object however your framework gives it to you,
|
|
1527
|
+
including `dict(request.headers)` if you already have that.
|
|
1528
|
+
|
|
1529
|
+
### Inbound: automatic via middleware (Flask / Django)
|
|
1530
|
+
|
|
1531
|
+
```python
|
|
1532
|
+
from traceact import TraceActMiddleware
|
|
1533
|
+
|
|
1534
|
+
# Flask
|
|
1535
|
+
app.wsgi_app = TraceActMiddleware(app.wsgi_app)
|
|
1536
|
+
|
|
1537
|
+
# Django (in wsgi.py)
|
|
1538
|
+
from django.core.wsgi import get_wsgi_application
|
|
1539
|
+
application = TraceActMiddleware(get_wsgi_application())
|
|
1540
|
+
```
|
|
1541
|
+
|
|
1542
|
+
Reads both headers from the WSGI environ and applies them to every trace
|
|
1543
|
+
started during that request — including traces started while a streamed
|
|
1544
|
+
response body is generated (the context is held until the response is fully
|
|
1545
|
+
closed, not just until the view function returns). If neither header is
|
|
1546
|
+
present, the middleware is fully transparent.
|
|
1547
|
+
|
|
1548
|
+
### Inbound: automatic via middleware (FastAPI / Starlette)
|
|
1549
|
+
|
|
1550
|
+
```python
|
|
1551
|
+
from traceact import TraceActASGIMiddleware
|
|
1552
|
+
from fastapi import FastAPI
|
|
1553
|
+
|
|
1554
|
+
app = FastAPI()
|
|
1555
|
+
app.add_middleware(TraceActASGIMiddleware)
|
|
1556
|
+
```
|
|
1557
|
+
|
|
1558
|
+
Or wrap manually:
|
|
1559
|
+
|
|
1560
|
+
```python
|
|
1561
|
+
app = TraceActASGIMiddleware(app)
|
|
1562
|
+
```
|
|
1563
|
+
|
|
1564
|
+
### How the chain looks in the viewer and TraceLog
|
|
1565
|
+
|
|
1566
|
+
Both services write their own trace records. Query by whichever link answers
|
|
1567
|
+
your question — `upstream_trace_id` for "what did this specific call trigger",
|
|
1568
|
+
`correlation_id` for "show me the whole workflow":
|
|
1569
|
+
|
|
1570
|
+
```python
|
|
1571
|
+
from traceact import TraceLog
|
|
1572
|
+
|
|
1573
|
+
log = TraceLog("traces.jsonl")
|
|
1574
|
+
log.filter(upstream_trace_id="trc_abc123").all() # traces this call triggered
|
|
1575
|
+
log.filter(correlation_id="corr_wf_9f2a").all() # the entire workflow
|
|
1576
|
+
log.filter(correlation_id="corr_wf_9f2a").view() # same, in the browser viewer
|
|
1577
|
+
```
|
|
1578
|
+
|
|
1579
|
+
The inspector's summary card shows both when present, each in full (not
|
|
1580
|
+
shortened) so they can be copied and searched against another service's logs.
|
|
1581
|
+
|
|
1582
|
+
### `ai_prompts` redaction preset
|
|
1583
|
+
|
|
1584
|
+
For AI pipelines where trace payloads must not store raw prompt text, model
|
|
1585
|
+
responses, or conversation history, enable the `ai_prompts` preset:
|
|
1586
|
+
|
|
1587
|
+
```python
|
|
1588
|
+
configure(
|
|
1589
|
+
config=TraceConfig(redaction_presets=["ai_prompts"]),
|
|
1590
|
+
)
|
|
1591
|
+
```
|
|
1592
|
+
|
|
1593
|
+
Redacted fields include: `raw_prompt`, `prompt_content`, `system_prompt`,
|
|
1594
|
+
`raw_response`, `response_content`, `conversation`, `message_content`,
|
|
1595
|
+
`file_content`, `source_excerpt`, `context_window`, `completion`,
|
|
1596
|
+
`generation`, `output_text`. Safe fields like `model`, `latency_ms`,
|
|
1597
|
+
`prompt_id`, and token counts are unaffected.
|
|
1598
|
+
|
|
1599
|
+
| What gets redacted | What stays |
|
|
1600
|
+
|---|---|
|
|
1601
|
+
| Raw prompts and responses | Model name, version |
|
|
1602
|
+
| System prompts | Token counts (as long as field name has no "token" substring) |
|
|
1603
|
+
| Conversation history | Latency, cost |
|
|
1604
|
+
| File and source excerpts | Trace IDs, correlation IDs |
|
|
1605
|
+
|
|
1606
|
+
Combine with other presets:
|
|
1607
|
+
|
|
1608
|
+
```python
|
|
1609
|
+
TraceConfig(redaction_presets=["ai_prompts", "api_keys"])
|
|
1610
|
+
```
|
|
1611
|
+
|
|
1612
|
+
### Reference: headers
|
|
1613
|
+
|
|
1614
|
+
| Header | Outbound (inject) | Inbound (extract) |
|
|
1615
|
+
|---|---|---|
|
|
1616
|
+
| `traceact-trace-id` | Active `ActionTrace.trace_id` | Sets `upstream_trace_id` |
|
|
1617
|
+
| `traceact-correlation-id` | Active `ActionTrace.correlation_id`, if set | Sets `correlation_id` |
|
|
1618
|
+
|
|
1619
|
+
Accepted header collection types on the inbound side: any object with
|
|
1620
|
+
`.items()` (dict in any casing, Werkzeug/Django/Starlette header objects,
|
|
1621
|
+
`requests.CaseInsensitiveDict`), a list of `(name, value)` pairs, or raw ASGI
|
|
1622
|
+
`[(b"name", b"value")]` byte pairs. Matching is case-insensitive regardless of
|
|
1623
|
+
which form you pass.
|
|
1624
|
+
|
|
1625
|
+
WSGI: the header arrives as `HTTP_TRACEACT_TRACE_ID` in the environ.
|
|
1626
|
+
ASGI: the header arrives as bytes in `scope["headers"]`.
|
|
1627
|
+
|
|
1628
|
+
---
|
|
1629
|
+
|
|
1467
1630
|
## Quick reference
|
|
1468
1631
|
|
|
1469
1632
|
```python
|
|
@@ -1476,5 +1639,9 @@ from traceact import (
|
|
|
1476
1639
|
traced_action, # decorator
|
|
1477
1640
|
JsonlSink, # write traces to a .jsonl file
|
|
1478
1641
|
ConsoleSink, # print traces to stdout
|
|
1642
|
+
propagate, # context manager for inbound propagation
|
|
1643
|
+
inject_headers, # stamp outbound request headers
|
|
1644
|
+
TraceActMiddleware, # WSGI auto-propagation (Flask, Django)
|
|
1645
|
+
TraceActASGIMiddleware, # ASGI auto-propagation (FastAPI, Starlette)
|
|
1479
1646
|
)
|
|
1480
1647
|
```
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "traceact"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.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]
|