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.