A thread-safe, asynchronous observability toolkit for Python.
Project description
sentinel-trace
A thread-safe, asynchronous observability toolkit for Python. Drops structured JSONL logs to disk from a background worker so your hot path stays fast.
Install
pip install sentinel-trace
Usage
from sentinel import AsyncLogger, SentinelConfig, TimeBlock, tag, trace
logger = AsyncLogger(SentinelConfig(log_file="app.jsonl"))
@trace(logger)
def fetch_user(user_id: int) -> dict:
...
@trace(logger, label="checkout") # group records under a domain term
@trace(logger, slow_ms=500) # flag records >500ms as `slow: true`
@trace(logger, sample=0.1) # keep ~10% of root traces
async def fetch_user_async(user_id: int) -> dict:
...
def crunch():
with TimeBlock(logger, label="data_crunching"):
...
# Custom tags attach to every record emitted inside the scope.
with tag(user_id=42, request_id="req-001"):
fetch_user(42)
logger.flush() # block until queue drained
logger.shutdown() # idempotent; also runs at interpreter exit
@trace() (no args) uses a process-wide default logger that writes to
sentinel_logs.jsonl in the working directory.
Tracing
Every @trace and TimeBlock call emits a record carrying trace_id,
span_id, and parent_span_id. Nested calls share trace_id; child records
link to their parent. Records form a tree post-processable with jq, DuckDB,
or pandas.
{"timestamp": "2026-04-30T00:00:00+00:00", "function": "fetch_user", "duration_ms": 12.3, "cpu_time_ms": 0.4, "status": "success", "error_type": null, "error": null, "trace_id": "a1b2c3d4...", "span_id": "e5f6...", "parent_span_id": "9a8b..."}
When @trace is given a label, it's appended to the function name in brackets
(run[checkout]). When tags are in scope, records carry a tags object. When
slow_ms is set and breached, records carry "slow": true.
Sampling
@trace(sample=0.1) keeps ~10% of root traces. The decision is made once at
the trace root and inherited by every child span — a kept trace stays kept
end-to-end, so flame charts are never missing nodes. Sampled-out calls still
execute the wrapped function and propagate context; they just don't emit a
record.
CLI
A sentinel executable is installed with the package:
sentinel tail app.jsonl # one-shot pretty-print (flat)
sentinel tail app.jsonl --tree # render each trace as a tree
sentinel tail app.jsonl --follow # tail -f mode (composes with --tree)
sentinel tail app.jsonl --no-color
python -m sentinel tail app.jsonl # equivalent module form
Flat mode (default) prints one line per record in arrival order, colorized by status. Short trace IDs are shown for grouping; tags render inline.
Tree mode (--tree) buffers records by trace_id and flushes one complete
trace at a time, drawing the span hierarchy with box-drawing characters:
trace 6abe7cc3
render_profile 5.24ms success [user_id=42 request_id=req-001]
└─ fetch_user 5.16ms success [user_id=42 request_id=req-001]
└─ db_query 5.09ms success [user_id=42 request_id=req-001]
[67d772ca] maybe_slow 50.12ms success SLOW
[724f8b5d] cpu_bound 4.00ms success
Single-span traces collapse to one line with an inline [<short_id>] prefix.
On EOF (or process exit in --follow mode), any incomplete traces — those
whose root span never arrived — are flushed flat as orphans so nothing is
lost.
NO_COLOR honored; color auto-disabled when stdout is not a TTY.
When to use this — and when not to
Sentinel is a thin, single-process, file-backed timing logger. Pick it when:
- You want microsecond-overhead
@trace/TimeBlockon a service or batch job. - You're happy to grep /
jq/ load JSONL into DuckDB or pandas after the fact. - You don't want to operate an OpenTelemetry collector, Datadog Agent, or Prometheus exporter for this workload.
Reach for something else when:
- You need distributed tracing across services with span propagation — use OpenTelemetry.
- You need real-time metrics with PromQL queries — use Prometheus + a metrics library.
- You need full-fidelity APM (DB query plans, code-level profiling, user-session replay) — that's what Datadog / New Relic / Sentry exist for.
Known limitations
- No log rotation. The file grows unbounded. Use
logrotateexternally or pass a date-stampedlog_filepath. - No multi-process aggregation. Each process writes its own file. If
multiple processes share a
log_file, writes are not arbitrated — one of them will lose lines. - Worker self-heal does not auto-restart. I/O failures are detected and
surfaced via
logger.is_healthy(); the worker refuses further records rather than silently dropping them. Restart the process to recover. - No backpressure by default (
enqueue_timeout=0.0). Saturating the queue drops records with a warning. Setenqueue_timeout > 0if you'd rather block producers than drop. - No trace ID / span propagation, no automatic argument capture, no resource (memory/CPU) metrics yet — see the roadmap.
Operational tips
- Call
logger.flush(timeout=...)before forking or before invoking external processes that depend on the log being on disk. - Catch
is_healthy() == Falsein long-running services and alert / restart. - For high-throughput services, increase
max_queue_sizeproportionally to your peak per-second log rate × peak worker write latency.
Development
pip install -e ".[dev]"
ruff check sentinel tests examples
ruff format --check sentinel tests examples
mypy sentinel
bandit -r sentinel -ll
pytest --cov=sentinel --cov-report=term-missing
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sentinel_trace-0.2.0.tar.gz.
File metadata
- Download URL: sentinel_trace-0.2.0.tar.gz
- Upload date:
- Size: 29.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f7c2d7ec330a763f0578bd0af33c5b4a5e969eb2c1e3d351a29f20b1a106da3d
|
|
| MD5 |
0e35726ef2cbc5a0264ec661a55bfbbd
|
|
| BLAKE2b-256 |
cb0035d2e804e18cf2202fc8a8a9451a03b740a034d60cee26d035ef5575ecc2
|
File details
Details for the file sentinel_trace-0.2.0-py3-none-any.whl.
File metadata
- Download URL: sentinel_trace-0.2.0-py3-none-any.whl
- Upload date:
- Size: 20.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
31dc17f0d52e36df95bd018f9439e2403eca08fc86cd6cc454c49f488cd52551
|
|
| MD5 |
94ff6402b338083c73db8a495e4a4900
|
|
| BLAKE2b-256 |
7fa9c1bfcd0c2f338056ca6347a7efdb596d681777f64b5a7e441352390d3fea
|