Installation
PyPI and npm packages publish at v1.0.0-beta.0 (roadmap). Until then, install from source.
| SDK | Prerequisites | Native dep? |
|---|---|---|
| Python | Python 3.10+, pip | None — pure Python |
| Go | Go 1.22+ | None — pure Go, CGO_ENABLED=0 friendly |
| Rust | Rust 1.85+ | None — cargo handles everything |
| C++14 | Any C++14 compiler (GCC 5+, Clang 3.4+, MSVC 2015+) | None — single-header copy |
| Node / TypeScript | Node 24+ | None — pure JS |
| Swift | Swift 5.9+ | None — pure Swift |
Today — Install From Source
Registry Publish — Pending v1.0 GA Tag roadmap
5-Minute Quickstart
1Register Audit Codes — Once, at Startup
2Init — Reads Env Vars; FASTEN_AUDIT_DSN Required
3Emit Anywhere
Production: Env-Var Driven
Other Languages — Same Shape
Per-SDK READMEs in the repo carry the equivalent quickstart for each language; they all share spec/row-schema.json as the wire-format source of truth.
| Language | Quickstart | Worked example |
|---|---|---|
| Python | python/README.md | FastAPI service |
| Go | go/README.md | net/http service |
| Node.js / TS | js/README.md | node:http service |
| Rust | rust/README.md | tiny_http service |
| C++14 | cpp/README.md | connector + reader |
| Swift | swift/ package | — |
| Java | placeholder — emit().write() throws UnsupportedOperationException with a pointer to the other SDKs. | |
What You Actually See
Every emit() and log.* writes one NDJSON line to stdout. Docker's log driver captures and rotates it.
emit("USER_CREATED", …) → stdout
log.info("signup_complete", …) → stdout
Both lines share request_id: "d4e5f6a1b2c3" — the join key across all three streams.
Querying Back
Audit responses are {"rows": [...], "total": N, "limit": L, "offset": O}. Sys / api responses are {"rows": [...]} (ring buffers, no offset).
Debugging a Real Incident
Scenario: A pipeline config change was applied at 14:32. By 14:33 the MQTT broker shows message loss.
Without fasten
docker logs gateway | grep 14:32— hundreds of lines, no actor infodocker logs pipeline-engine | grep 14:32— different format, different timestamps- Cross-reference manually — which request caused which effect? Unknown.
- Ask on Slack "who changed the config?" — 20-minute delay
With fasten — One Query, 30 Seconds
Same request_id threads the HTTP config change, MQTT disconnect, and reconnect — across two services and two transports. 30 seconds vs 20 minutes.
The 7 Anchors
5 Ws + H + CORRELATION — enforced at the type level. Caller supplies only code, target, and optional detail. Everything else auto-fills.
| Anchor | Fields | Auto-filled? |
|---|---|---|
| WHO | actor, actor_kind | from ctx / caller |
| WHAT | code, action | code from caller; action from Meta |
| WHEN | timestamp, monotonic_seq | auto |
| WHERE | source_node_id, service_id, tenant_id | auto from fasten.init() |
| WHOM | target, category, domain | target from caller; rest from Meta |
| HOW | method ∈ {http, mqtt, cli, scheduler, ui, agent_tool} | auto from shim / caller |
| CORRELATION | request_id | from context; minted if absent |
WHY lives in detail.reason (free text). A policy plugin can enforce it on mutation codes.
Three Streams
| Stream | Storage | Endpoint | Persistent? |
|---|---|---|---|
| syslog | In-memory ring (10k lines) | /logs/sys | No — ring only |
| API log | Ring + opt-in SQL | /logs/api | Opt-in via FASTEN_API_DSN |
| audit | SQL fasten_audit | /logs/audit | Yes — per-code TTL |
All three carry request_id. A ?request_id=<id> query returns rows from all services that carried it.
Shims
Nothing default-on. Import only what your transport uses. Each shim does one thing: read an id from the wire → stash in context → propagate downstream.
| Shim | Wire convention | Status |
|---|---|---|
shim.http | X-Request-ID header (mint if absent) | bundled |
shim.mqtt | _req field inside payload | bundled |
shim.scheduler | Mints scheduler-<run_id> at job start | bundled |
shim.agent_tool | Agent tool-call context propagation | planned |
| Custom (gRPC, NATS, …) | 10-line pattern: read wire → set ctx | — |
Schema
fasten owns its audit table; your product owns its own DB. They share a request_id join key and never share a table. Two storage topologies are supported:
fasten_audit Columns
| Column | Type | Notes |
|---|---|---|
id | TEXT PK | evt-<20-hex> |
code | TEXT | e.g. USER_CREATED |
actor / actor_kind | TEXT | WHO anchor |
target / category / domain | TEXT | WHOM anchor — adopter-defined strings |
service_id / source_node_id / tenant_id | TEXT | WHERE anchor — from init() |
method | TEXT | HOW anchor |
request_id | TEXT | CORRELATION — join key across streams |
timestamp / monotonic_seq | TEXT / INT | WHEN anchor |
detail | JSON | Adopter-owned free blob; secrets redacted |
fasten creates its audit table via idempotent CREATE TABLE IF NOT EXISTS on init(). Your product creates audit_replay via its own migrations. They must never share a table — use a dedicated DB, a separate schema, or a fasten_ prefix so names cannot collide.
Catalog YAML — Single Source Across SDKs
Code catalogs can be declared in *.codes.yaml files instead of programmatically. Same file works across every SDK; reload is atomic + fault-tolerant (parse / validate fully into a fresh dict, then swap under a lock; on any failure the previous catalog stays active).
Reload is not additive. Codes removed from the file become unknown after a reload — already-stored audit rows stay readable (the wire code field is a free string). Programmatic register() calls survive reload (tracked separately from yaml-loaded codes). Python ships a typegen CLI: fasten codes typegen fleet.codes.yaml > codes_stub.py emits IDE stubs.
Audit-Store Failure Handling
Audit failures must never break the request being audited. emit() defaults to queue mode: rows are pushed onto a bounded in-memory queue, drained by a background thread with exponential backoff (100 ms → 60 s, ±20 % jitter). A locked / down store no longer cascades into 5xxs on the request path.
| Strategy | Behavior | When to use |
|---|---|---|
queue (default) | emit() returns immediately; drainer retries forever | Production. Default for v1.0 GA. |
raise | Synchronous insert; raises AuditStoreError on failure | Tests, dev mode, adopters wanting loud failures during config debugging. |
Sys-Stream Self-Report
The drainer never silently degrades. State transitions write {shape:"sys"} NDJSON lines so existing log aggregation (Loki, Splunk, journald, or whichever hosted log indexer you run) catches audit-pipeline issues without any new alerting plumbing. Adopter request_id propagates onto every line.
| Event | Level | When |
|---|---|---|
audit_drain_failed | warn | First failure after a successful insert |
audit_drain_degraded | error | 5+ consecutive failures |
audit_drain_recovered | info | Insert succeeds after a failure burst |
audit_queue_high_water | warn | Used capacity ≥ 50 % |
audit_queue_near_full | error | Used capacity ≥ 80 % |
Programmatic Snapshot — queue_stats()
Equivalents: Go fasten.GetQueueStats() (returns *QueueStats); JS queueStats(); Rust fasten::queue_stats(); C++ fasten::queue_stats(). All return null / nil / None in raise mode.
Deterministic Shutdown — flush()
For k8s preStop hooks, CLI exit paths, or test teardown — block until pending rows drain. No-op + true in raise mode so adopter shutdown code is mode-agnostic across both strategies.
Cross-language deviation: JS. Node is single-threaded — emit() can't synchronously block on a counting semaphore the way Python / Go / Rust / C++ do. queueCapacity is therefore the high-water-warn threshold in JS, not a hard cap. The drainer + retry-forever-with-backoff matches the others exactly. queueStats(), flush(), and the audit_drain_* events all behave identically. Swift uses a Thread-based in-process drainer with blocking enqueue() like Python / Go.
Security Model
fasten is responsible for the row's integrity from emit to insert, and for any surface fasten itself exposes — but never for the security of storage you brought.
Responsibility Split
| Zone | Owner |
|---|---|
| At write redaction · schema · registry integrity · SQL safety |
fasten |
| At rest file perms · encryption · backup |
You. Perimeter is your DB's existing security — RBAC, KMS, network isolation. |
| At read authn · authz · CORS · rate limit |
You. Mount the reader behind your existing auth (FastAPI Depends, gateway, session middleware). |
| Distribution signed releases · SBOM · CVE disclosure · vuln scanning |
fasten |
Wire Your Existing Auth
The reader is a FastAPI router. Mount it like any other route, behind whatever auth your app already runs.
fasten's reader doesn't add its own check here. Two auth layers fight each other; one well-placed layer is the point.
Reserved Headers
| Header | Purpose |
|---|---|
X-Fasten-Request-Id | Correlation id bundled |
What Stays Out of Scope
- TLS termination — your reverse proxy / load balancer.
- Network ACLs — your VPC / firewall / k8s
NetworkPolicy. - Key distribution — your secret manager (Vault, k8s Secret, AWS SSM).
- DB encryption at rest — your DB layer (e.g. Postgres TDE) or OS-level on the SQLite file.
API Mounting
fasten's reader is a mountable router — not a standalone service. Wire it in like any sub-router under whatever prefix you choose.
Python (FastAPI)
Go (chi)
Node.js (built-in http or Express)
JS doesn't ship a mountable router today; wire X-Request-ID via withRequestID() in your handler, and surface the audit / sys / api streams over your existing HTTP framework. See the js/examples service for the pattern.
Rust (tiny_http or axum)
The Rust SDK is sync-by-default. Wrap each request in with_request_id() and call EmitBuilder::submit() inside.
C++14 (single-header)
Use the bundled FastenReader + reader_simplehttp_main.cpp wiring; or call into your own HTTP server.
Swift (SPM)
SQLite-backed store out of the box; uses system sqlite3 and swift-crypto. Async request_id via @TaskLocal.
Mounted Reader Endpoints — at a Glance
| Path | Purpose |
|---|---|
GET /audit | Query audit rows (filters: actor, target, since, until, code, request_id, domain, source_node_id, offset, limit). Response: {rows, total, limit, offset}. |
GET /sys | Query syslog ring (filters: level, request_id, service_id, limit). |
GET /api | Query API-log ring (filters: method, path, request_id, limit). |
GET /audit/doctor | Audit-pipeline health snapshot — store reachability + row count, queue stats, transport ring depths, redactor state, current init parameters, hash-chain integrity. Same auth as /audit via the router's dependencies=. One curl for compliance auditors / k8s liveness probes / status pages. |
Adopter Middleware Accessors
For adopters writing custom middleware or logging layers that need the same primitives emit() uses internally. Python and Go expose these as accessor functions; JS, Swift, and C++ expose equivalent standalone functions.
| Accessor | Python | Go | Returns |
|---|---|---|---|
| Transport | fasten.transport() | fasten.GetTransport() | Active StdoutTransport — push custom api / sys rows into the ring + stdout |
| Redactor | fasten.redactor() | fasten.RedactDetail(m) | Active Redactor — applies key-pattern + value-shape redaction; use in custom logger processors. JS: coreRedact(json). C++: fasten_redact() C ABI. |
| Audit store | fasten.audit_store() | (via Config.AuditStore) | Active AuditRepository — for custom readers, replication, or outbox layers |
Env-Var Reference
No fasten.yaml. No config daemon. Env-vars only.
| Variable | Default | Purpose |
|---|---|---|
FASTEN_SERVICE_ID | required | WHERE — service identity |
FASTEN_NODE_ID | required | WHERE — host/node identity |
FASTEN_TENANT_ID | unset | WHERE — tenant / org / site (optional) |
FASTEN_AUDIT_DSN | required | Audit store (sqlite:// or postgres://) — fasten refuses to start without it |
FASTEN_API_DSN | ring only | Opt-in API-log SQL persistence |
FASTEN_LEVEL | info | Syslog threshold |
FASTEN_REDACT_KEYS | sensible default | Extra redaction patterns (comma-sep) |
FASTEN_AUDIT_STORE_FAILURE_STRATEGY | queue | P1-15 — queue (async drainer, retry forever) or raise (sync; raises AuditStoreError) |
FASTEN_SYS_RING_SIZE | 10000 | Syslog ring size (lines) |
FASTEN_API_RING_SIZE | 10000 | API-log ring size (lines) |
FASTEN_AUDIT_RETENTION_CLASS_SHORT | 30 | Days for short-class codes |
FASTEN_AUDIT_RETENTION_CLASS_MEDIUM | 180 | Days for medium-class codes |
FASTEN_AUDIT_RETENTION_CLASS_LONG | 1095 (3 yr) | Days for long-class codes |
FASTEN_RETENTION_SWEEP_INTERVAL | 6h | TTL sweep cadence |
Retention + PII
| Class | Default TTL | Example codes |
|---|---|---|
short | 30 days | High-volume operational codes |
medium | 180 days | CONNECTOR_STARTED, SCHEDULE_TRIGGERED |
long | 3 years | USER_CREATED, CONFIG_UPDATED, AUTH_LOGIN |
PII — Three Mechanisms
1. Redaction at emit time — two passes, applied before writing anywhere:
Key-pattern pass — substring match, case-insensitive: api_key, password, passwd, token, secret, authorization, bearer, m2m_key, cert_private, private_key, access_key, session_id, cookie, credential. Any key containing one of these patterns (e.g. customer_token, user_password) has its value replaced with ***. Extend with FASTEN_REDACT_KEYS=ssn,dob or extra_redact_keys= in init().
Value-shape pass — string values that look like known secret formats are replaced with a type-hinting token regardless of key name: JWT → ***JWT***, PEM private key → ***PRIVATE_KEY***, AWS access key (AKIA/ASIA) → ***AWS_KEY***, GitHub token (ghp_/ghs_/…) → ***GH_TOKEN***, Stripe live key → ***STRIPE_KEY***, OpenAI key → ***OPENAI_KEY***, credit-card numbers passing Luhn check → ***CC***. Key-pattern fires first — if the key matches, the value is never inspected for shape.
2. PII class flag — pii_in_detail=True does two things: forces retention to short (30 days) regardless of declared class, and replaces the entire detail dict with {"_redacted":"***","_pii_in_detail":true} at emit time. Individual fields can be preserved via detail_passthrough_keys:
3. TTL sweep — rows beyond their class are deleted on schedule. For GDPR right-to-erasure: delete rows by actor or target, then let TTL sweep clean up the rest.
Operational FAQ
What Is the Latency Overhead?
Queue mode (default): emit() returns in <0.1 ms — the background drainer handles the store write asynchronously. Raise mode: ~0.1–0.5 ms per emit() with SQLite WAL, ~1–5 ms with Postgres. fasten.log.* is ring + stdout only — sub-millisecond in either mode. High-volume codes (>100/sec): set high_volume=True in Meta to skip the SQL insert.
What Is the Memory Footprint?
~8 MB at full occupancy: syslog ring (10k × ~500 B) + API log ring (10k × ~300 B). Tune with FASTEN_SYS_RING_SIZE and FASTEN_API_RING_SIZE.
Does SQLite Contend Under Concurrent Writes?
WAL mode allows concurrent readers + one writer. Typical audit volumes are well below contention. Mark hot codes high_volume=True or switch to Postgres if you see it.
Does fasten Add a Thread or Process?
One drainer thread per process when audit-store failure handling runs in queue mode (the default). Python, Go, and C++ bind to the shared fasten-core C ABI drainer (a single Rust std::thread inside the shared library); Rust runs it natively. JS uses a setImmediate chain (single-threaded event loop, no native deps). Swift uses a Thread-based pure Swift in-process drainer. Set audit_store_failure_strategy="raise" if you want strict synchronous semantics with no background drainer.
What Happens if the DB Is Unavailable?
- The row is written to stdout regardless — Docker / journald captures it.
- Queue mode (default): the row is buffered in-memory; the drainer retries with exponential backoff (100 ms → 60 s, ±20 % jitter).
emit()never raises on store failure. The drainer self-reports state transitions to the sys stream (audit_drain_failed,audit_drain_degraded,audit_drain_recovered) so existing log aggregation catches issues. - Capacity covers queued + in-flight retry combined (default
queue_capacity=100). When saturated,emit()blocks rather than silently drops. JS deviates here — single-threaded event loop meansemit()stays sync; capacity is the high-water-warn threshold instead. - Raise mode:
emit()calls insert synchronously and raisesfasten.AuditStoreErroron failure. Adopter chooses how to react. - Use
fasten.queue_stats()for a programmatic snapshot, orGET /logs/audit/doctorfor the same data over HTTP.
Does fasten Work in Air-Gapped Environments?
Yes. Stdout is the primary transport — no network required. SQLite works fully offline. Rows drain upstream when connectivity resumes.
Deployment Recipes
fasten is in-process, no sidecar. The only orchestration concerns are: pass env vars, mount a writable volume for the SQLite file (or wire Postgres), and call fasten.flush() from your shutdown / preStop hook so the queue drains before the container dies.
Docker Compose
Kubernetes
systemd
Observability — Wiring Drainer Events to Your Stack
P1-15 ships five sys-stream events covering the audit pipeline. They land on your existing log channel (the same stdout NDJSON your aggregator already scrapes) — no separate metrics endpoint required. Wire them to alerts in whatever you already use.
Loki / Grafana
Alert when the audit drainer degrades:
Hosted log monitor (vendor-neutral)
Most hosted log indexers expose the same idea — search by the JSON fields, roll up over a window, alert above a threshold. The query syntax below uses one common dialect; translate to your provider's selector + roll-up grammar as needed.
Prometheus (via vector / fluent-bit log → metric)
Convert the sys events to counters in your log pipeline; fasten doesn't expose a Prometheus endpoint directly (stays in-process, zero deps).
k8s Liveness Probe
The reader's GET /audit/doctor endpoint is k8s-friendly out of the box — JSON response with store.reachable and queue.retry_count_active. Wire it as a liveness probe so the pod restarts if the audit pipeline stays degraded.
Compliance Auditor Cheatsheet
| Event | Auditor question it answers |
|---|---|
audit_drain_failed + audit_drain_recovered pair | "Was there an outage? How long?" — paired timestamps give exact duration. |
audit_drain_degraded alone (no recovery) | "Are audit rows currently making it to the store?" — page someone. |
audit_queue_near_full | "Is the audit pipeline saturated?" — capacity / throughput sizing review. |
queue_stats().drained_total | "How many audit rows were processed in this window?" |
Logging — Structured Sys Stream
fasten's four log functions write {"shape":"sys"} NDJSON lines to stdout and push them into the in-memory syslog ring (queryable via GET /logs/sys). They are not audit rows — no code catalog entry needed, no durable storage, no 5 Ws enforcement. Use them for operational diagnostics alongside emit() for compliance events.
Python (fasten.log.*) | Go | JS (fasten.log.*) | C++ (fasten::log::*) |
|---|---|---|---|
fasten.log.info(event, **fields) | fasten.LogInfo(ctx, event, kv...) | fasten.log.info(event, fields) | fasten::log::info(event, fields) |
fasten.log.warn(event, **fields) | fasten.LogWarn(ctx, event, kv...) | fasten.log.warn(event, fields) | fasten::log::warn(event, fields) |
fasten.log.error(event, **fields) | fasten.LogError(ctx, event, kv...) | fasten.log.error(event, fields) | fasten::log::error(event, fields) |
fasten.log.debug(event, **fields) | fasten.LogDebug(ctx, event, kv...) | fasten.log.debug(event, fields) | fasten::log::debug(event, fields) |
All four auto-stamp request_id from the ambient context, service_id from init(), and timestamp. Any extra keyword arguments (Python), key-value pairs (Go), field object (JS), or Fields map (C++) are merged into the output line.
Python
Go
Key-value pairs follow slog-style: alternating string key + any value. ctx carries the request_id via fasten.WithRequestID(ctx, rid).
JavaScript / TypeScript
fields is a plain object. request_id comes from the active AsyncLocalStorage store set by withRequestID().
C++14
fasten::Fields is std::vector<std::pair<std::string,std::string>>. Request id comes from the active RequestScope.
Logger Shims — Use Your Existing Logging Library
If you already have a logging framework in the codebase, opt-in shims mirror every log call into fasten's syslog ring without changing any existing call sites.
| Language | Shim | What it does |
|---|---|---|
| Go | fasten.NewSlogHandler(base) |
Wraps any slog.Handler. Install once with slog.SetDefault(slog.New(fasten.NewSlogHandler(base))) — existing slog.Info/Warn/Error/Debug calls are mirrored to the fasten ring automatically. The underlying handler still writes to its own destination; no double-write occurs. |
| Python | fasten.shim.structlog.make_fasten_processor()fasten.shim.structlog.configure() |
Pushes every structlog event into fasten's syslog ring. configure() is the one-call opinionated setup: installs the fasten processor, redaction processor, JSON or console renderer, and a stdlib bridge so import logging calls reach the same destination. |
| Python (stdlib) | fasten.shim.stdlib |
Bridges Python's built-in logging module into the fasten ring. Use when structlog is not in the stack. |
| C++ | fasten/shim/spdlog.hpp |
Add fasten::shim::spdlog_sink_mt to your spdlog logger's sink list. Every spdlog::info/warn/… call is mirrored to the fasten ring. Thread-safe; recursion-guarded. |
| C++ | fasten/shim/glog.hpp |
Install fasten::shim::GlogSink as a glog sink. All LOG(INFO), LOG(WARNING), LOG(ERROR) calls flow into the fasten ring. |
| C++ | fasten/shim/boost_log.hpp |
A Boost.Log sink backend — attach to the logging core to mirror Boost.Log output to fasten. |
Shims are side-effect-only: they push into the ring and return the event dict unchanged. Your existing log pipeline (stdout, file, remote sink) is not replaced — fasten is an additive layer.
Public Accessors — flush & queue_stats
Two functions expose the internal queue state from outside the audit pipeline. Use them in shutdown hooks, health probes, and tests.
flush(timeout) — Deterministic Shutdown
Blocks until every queued audit row has been written to the store, or until the timeout expires. Returns True / true if fully drained; False / false if timed out with rows still pending. In raise mode (no drainer) it is a no-op that returns True immediately — shutdown code is therefore mode-agnostic.
| Language | Signature | Unit |
|---|---|---|
| Python | fasten.flush(timeout: float = 5.0) → bool | seconds |
| Go | fasten.Flush(timeout time.Duration) bool | any time.Duration |
| JS | await flush(timeoutMs: number = 5000) → boolean | milliseconds |
| Rust | fasten::flush(timeout: std::time::Duration) → bool | any Duration |
| C++ | fasten::flush(timeout: std::chrono::duration) → bool | any chrono::duration |
queue_stats() — Runtime Health Snapshot
Returns a snapshot of the drainer's current state. Returns None / nil / null in raise mode (no drainer running). Use it in health probes, status pages, or to feed your own metrics pipeline without mounting the full reader.
| Language | Signature | Return type |
|---|---|---|
| Python | fasten.queue_stats() → dict | None | dict or None in raise mode |
| Go | fasten.GetQueueStats() *QueueStats | *QueueStats or nil in raise mode |
| JS | fasten.queueStats() → object | null | object or null in raise mode |
| Rust | fasten::queue_stats() → Option<QueueStats> | Some(stats) or None |
| C++ | fasten::queue_stats() → std::optional<QueueStats> | std::nullopt in raise mode |
When to use which: Use flush() at shutdown and in test teardown to guarantee all rows land before the process exits. Use queue_stats() in liveness probes and status pages — retry_count_active > 0 means the store is temporarily unreachable; depth >= capacity means backpressure is building. Both are also available over HTTP as GET /logs/audit/doctor without any in-process code.
Reader Endpoint Reference
All three endpoints are served by the mountable fasten.reader.router(). Mount it under a prefix (e.g. /api/v1/logs) — the paths below are relative to that prefix. No built-in auth; always pass dependencies=[Depends(...)] or mount behind a gateway.
GET /audit
Query durable audit rows from the fasten_audit SQL table. Supports pagination. All parameters are optional and combinable.
| Parameter | Type | Description |
|---|---|---|
request_id | string | Filter by correlation id — returns all rows from a single logical request across any service that carried it. |
code | string | Exact audit code, e.g. USER_CREATED. |
domain | string | Adopter-defined domain, e.g. user, billing. |
actor | string | WHO — the actor id that performed the action. |
target | string | WHOM — the resource acted on. |
source_node_id | string | WHERE — the node/host that emitted the row. |
tenant_id | string | WHERE — multi-tenant isolation filter. |
since | ISO 8601 datetime | Lower bound on timestamp (inclusive). |
until | ISO 8601 datetime | Upper bound on timestamp (inclusive). |
limit | int (default 100, max 1000) | Page size. |
offset | int (default 0, min 0) | Pagination offset — number of rows to skip. |
Response shape:
GET /sys
Query the in-memory syslog ring (default 10 000 lines, configurable via FASTEN_SYS_RING_SIZE). Ring-only — no offset, no total. Rows are gone when the ring wraps.
| Parameter | Type | Description |
|---|---|---|
level | string | Filter by log level: debug · info · warn · error. |
request_id | string | Filter to sys lines from one logical request. |
service_id | string | Filter by originating service. |
limit | int (default 100, max 1000) | Number of most-recent lines to return. |
Response shape:
GET /audit/doctor
Single-call audit-pipeline health snapshot. Same auth as /audit (applied at router level). Use as a k8s liveness probe, compliance auditor curl, or status-page data source.
| Field | Type | Meaning |
|---|---|---|
store.kind | string | Class name of the active AuditRepository, e.g. SQLiteStore. |
store.reachable | bool | true if the store responded to a count() call without error. |
store.rows | int | null | Total audit rows currently in the store. null if the store doesn't implement count(). |
store.last_insert_at | ISO datetime | null | Timestamp of most recent successful insert. |
store.last_error | string | null | Most recent store error, or null. |
queue | object | null | Full queue_stats() snapshot — null in raise mode. Fields: depth, capacity, high_water, drained_total, retry_count_active, in_backoff_seconds, last_error. |
transport.stdout_active | bool | true if the stdout transport is initialised. |
transport.syslog_ring_depth | int | Current number of lines in the syslog ring. |
transport.api_ring_depth | int | Current number of lines in the API-log ring. |
redactor.active | bool | true if a redactor is configured. |
init.service_id | string | null | Value passed to (or read by) init(). |
init.node_id | string | null | Value passed to (or read by) init(). |
init.tenant_id | string | null | Value passed to (or read by) init(), if any. |
init.failure_strategy | string | "queue" or "raise". |
init.worker_pid | int | OS process id — identifies the worker under multi-worker servers (e.g. uvicorn --workers N). |
chain.verified | bool | null | Hash-chain integrity result for the most recent 50 rows from this node. null if no rows or chain verification could not run. |
chain.breaks | int | 0 if intact; 1 if a tampered or missing row was detected. |
chain.last_verified_at | ISO datetime | null | When the chain check ran. |
k8s liveness probe pattern: parse store.reachable (store up?) and queue.retry_count_active (drainer healthy?). The endpoint itself always returns HTTP 200 — use the field values to drive your probe logic, not the status code.
Glossary
| Term | Means |
|---|---|
anchor | One of the 7 mandatory fields a typed audit row must carry: WHO, WHAT, WHEN, WHERE, WHOM, HOW, CORRELATION. Enforced at the type level — emit refuses to produce a row missing any of these. |
code | The string identifier of an audit event — e.g. USER_CREATED, CONFIG_UPDATED. Adopter-defined, registered once at startup with register(). Maps 1:1 to a Meta describing severity, retention, PII flag. |
domain | An adopter-defined namespace grouping related codes. Plain string — fasten has no opinions. Examples: user, billing, fleet, config. |
category | Within a domain, the sub-group. Adopter-defined. Examples (user domain): account, profile, session. |
actor / actor_kind | WHO did the action. actor is a string identity (user id, service name, agent name); actor_kind is one of user · service · schedule · agent. |
target | WHOM the action acted on — the resource id, never PII. Free string. |
method | HOW the action was triggered. One of http · mqtt · cli · scheduler · ui · agent_tool · sdk. |
request_id | Correlation key threading all three streams for one logical request. Mintable via mint_id(); honored from X-Request-ID by the HTTP shim. |
stream | One of the three NDJSON channels fasten writes to stdout: audit (typed rows, durable), sys (structured logs, ring buffer), api (HTTP access trail, ring buffer or opt-in SQL). |
shim | An opt-in module that propagates request_id across a transport — shim.http, shim.mqtt, shim.scheduler. Read the wire id, stash in context, propagate downstream. |
retention class | One of short (30d) · medium (180d, default) · long (1095d). Per-code; pii_in_detail=True codes are forced to short. |
queue mode vs raise mode | P1-15 store-failure strategies. queue (default) — async drainer, never blocks emit() on store errors. raise — sync; throws AuditStoreError. |
Code Evolution + Compatibility
Renaming a Code
Schema Contract
The 7 anchor columns are stable — never change type. detail is yours to evolve.
- Identity:
id,origin_id,monotonic_seq,timestamp - WHAT:
code,action,severity - WHERE:
service_id,source_node_id,tenant_id - WHO:
actor,actor_kind - WHOM:
target,category,domain - HOW:
method - CORRELATION:
request_id - Free JSON:
detail— you own its schema
OTel / trace_id Bridge
Pass your OpenTelemetry trace_id as fasten's request_id — they become the same id. A future transport/otlp plugin will export audit rows as OpenTelemetry LogRecord entries, letting existing Grafana/Jaeger stacks query fasten rows.