audit · correlation · open source

One request_id.
Three streams. Four pillars.

Who changed what, when, and what happened nextone query.
Not Jaeger + Loki + Docker logs + logrotate + a homegrown audit table.

Apache-2.0 library · no daemon Python · Go · Node · Rust · C++ OTel-compatible
emit.py
import fasten
from fasten.shim.http import RequestIDMiddleware, APILogger

fasten.init()                            # reads env
app.add_middleware(RequestIDMiddleware)  # X-Request-ID
app.add_middleware(APILogger)            # api row per request

fasten.emit(code="USER_CREATED", target="u-42",
    actor="admin", detail={"email": "a@ex.com"})
fasten.log.info("user_created", user_id="u-42")

# Three streams — audit · sys · api
# One request_id threads all three.

Get started in 30 seconds

One env var picks the engine.
Same code, SQLite to Postgres.

fasten is a library — runs in your process, Cloud optional.
Audit rows go to durable storage.

SQLite — single-node

dev · single-host prod

One file on disk. Zero deps. WAL mode. Perfect for dev, tests, edge nodes, single-host services.

# .env
FASTEN_SERVICE_ID=my-service
FASTEN_NODE_ID=host-01
FASTEN_AUDIT_DSN=sqlite:///./audit.db

# app.py
import fasten
fasten.init()  # reads env

Postgres — multi-node / fleet

beta

Same code, change one env var. Postgres is supported natively; fleet-scale load + failover validation lands with v1.0 GA.

# .env
FASTEN_SERVICE_ID=my-service
FASTEN_NODE_ID=host-01
FASTEN_AUDIT_DSN=postgres://user:pw
  @db:5432/app?table=audit_log

# app.py — identical
import fasten
fasten.init()  # reads env

The emit call is identical in both modes. Storage swapped at the env-var boundary.

where it fits

Three pillars held together with string.
fasten adds the fourth — and provides the string.

Logs · Metrics · Traces · Audit. Plus one request_id in every stream, so a metric spike, a trace, a log line and an audit row join in one query.

Logs

DIY correlation, label-matching

Typed syslog. request_id auto-stamped by shims.

Metrics

Prometheus, no cross-pillar link

Exemplars carry request_id. Pivot to logs / audit.

Traces

OTel / Jaeger. Strong HTTP, weak elsewhere.

request_id ↔ trace_id. Shims close MQTT / scheduler gaps.

Audit new

CSVs. Unenforced JSON. Ad-hoc tables.

First-class. 7 anchors typed. Per-code retention. PII-aware.

audit model

5 Ws + H — enforced at the type level.

The classical audit vocabulary. No string-soup attributes.

+ CORRELATION request_id threads every stream, every transport
WHOactor · actor_kindadmin · scheduler · system · agent
WHATcode · actionPIPELINE_DEPLOYED · TOOL_CALLED
WHENtimestamp · seqUTC ms · per-node monotonic
WHEREnode · service · tenantmulti-tenant / multi-site filter
WHOMtarget · category · domainthe object of the action
HOWmethodhttp · mqtt · cli · scheduler · ui · agent_tool

query

Read it back — CLI first,
API when you need it.

Audit is only useful if it's queryable. Three ways out:

  • The bundled fasten CLI — for operators and CI.
  • A central aggregator (self-hosted, or fasten Cloud) — for fleets.
  • The HTTP reader — only when your service owns the UI.

CLI — fasten

bundled

Three operator utilities, bundled. Not a product surface.

$ fasten dump
# every registered code, sorted: id,domain,severity
# feeds the cross-language consistency gate
AUTH_LOGIN,user,info
ORDER_PLACED,commerce,info
PAYMENT_PROCESSED,commerce,info
USER_CREATED,user,info
...
$ fasten tail --stream audit --request-id a1b2c3d4
# stream audit rows for one request_id from local or remote
2026-04-22T12:34:56Z  USER_CREATED       admin   target=u-42
2026-04-22T12:34:56Z  ORDER_PLACED       u-42    target=ord-9001
2026-04-22T12:34:57Z  PAYMENT_PROCESSED  system  target=ord-9001
$ fasten doctor
# env vars? store reachable? shims wired? sample emit roundtrip?
✓ FASTEN_SERVICE_ID=api-server
✓ FASTEN_NODE_ID=host-01
✓ audit store reachable (sqlite:///./logs.db)
✓ http shim wired
✓ emit roundtrip OK (id=evt-01HZ...)

HTTP — mountable router

optional

Mount where you need programmatic read access — central aggregator, your own UI, external clients. Three endpoints, identical shape across services.

$ curl "http://$HOST/api/v1/logs/audit?request_id=a1b2c3d4"
$ curl "http://$HOST/api/v1/logs/audit?code=PIPELINE_DEPLOYED&since=2026-04-20"
$ curl "http://$HOST/api/v1/logs/audit?actor=admin&target=u-42&offset=0&limit=20"
$ curl "http://$HOST/api/v1/logs/sys?request_id=a1b2c3d4"
$ curl "http://$HOST/api/v1/logs/api?request_id=a1b2c3d4"
# Mount in your service — Python
app.mount("/api/v1/logs", fasten.reader.router())

# Go
r.Mount("/api/v1/logs", Fasten.Reader())

same shape, six languages

init → emit + log.
That's the entire API surface.

Each language's signature matches its dominant structured-logging idiom (structlog / slog / tracing / pino / SLF4J).

# .env — secrets never appear in code
FASTEN_SERVICE_ID=api-server
FASTEN_NODE_ID=host-01
FASTEN_AUDIT_DSN=postgres://user:pw@db/app?table=audit_log

# app.py
import fasten
from fasten.shim.http import RequestIDMiddleware, APILogger

fasten.init()                                      # reads env
app.add_middleware(RequestIDMiddleware)            # mints / honours X-Request-ID
app.add_middleware(APILogger, skip={"/health"}) # one api row per request

@app.post("/users")
async def create(u, actor):
    fasten.emit(code="USER_CREATED", target=u.id,
               actor=actor.id, detail={"email": u.email})
    fasten.log.info("user_created", user_id=u.id)
// .env
FASTEN_SERVICE_ID=checkout-svc
FASTEN_AUDIT_DSN=postgres://user:${DB_PW}@db/shop?table=audit_log

// main.go
import (
    "github.com/nerdapplabs/fasten-go/fasten"
    httpshim "github.com/nerdapplabs/fasten-go/fasten/shim/http"
)

fasten.Init(fasten.Config{})                        // reads env

r := chi.NewRouter()
r.Use(httpshim.RequestID)                           // mint/honour X-Request-ID
r.Use(fasten.APILogger("/health"))                 // one api row per request

r.Post("/orders", func(w http.ResponseWriter, req *http.Request) {
    ctx := req.Context()
    fasten.Emit(ctx, audit.CodeOrderPlaced,
        fasten.Target(orderID),
        fasten.Detail(map[string]any{"amount": amount}),
    )
    fasten.Log(ctx).Info("order_placed", "order_id", orderID)
})
// .env
FASTEN_SERVICE_ID=notification-svc
FASTEN_AUDIT_DSN=sqlite:///data/app.db?table=audit_log

// server.ts
import fasten, { EmitInput, withRequestID, mintID } from '@nerdapplabs/fasten';

fasten.init();                                     // reads env

app.post('/sensors/:id', async (req, res) => {
    // honour or mint X-Request-ID; everything emit/log inside inherits it
    const rid = req.headers['x-request-id'] ?? mintID();
    await withRequestID(rid, async () => {
        fasten.emit({
            code: 'SENSOR_UPDATED',
            target: req.params.id, actor: 'system',
            detail: { reading: req.body.value },
        } satisfies EmitInput);
        fasten.log.info('sensor_updated', { id: req.params.id });
    });
    res.sendStatus(200);
});
// .env
FASTEN_SERVICE_ID=batch-worker
FASTEN_AUDIT_DSN=sqlite:///var/lib/worker/audit.db?table=audit_log

// main.rs
use fasten::{EmitBuilder, Config};

let cfg = fasten::config_from_env()?;

// Fluent Emit — anchors auto-filled from ctx + config
fasten::EmitBuilder::new("BATCH_PROCESSED", "batch-4211")
    .actor("worker-01", "service")
    .detail(detail_map)
    .emit()
    .await?;

// tracing-style log
tracing::info!(batch_id = "4211", "batch_completed");

retention · pii · api log

Tune without code changes.
Per-code TTL. PII-aware.
API log opt-in.

Not every audit row deserves the same retention.

  • Compliance-core events — stay for years.
  • High-volume trigger events — sweep early.
  • Anything tagged pii_in_detail — forced minimisation.
  • API log — opt-in persistence; default is ring-buffer only.

Per-code retention class

audit

Every code declares a class: short, medium, long. The retention sweep deletes expired rows — on edges, never before they've replicated to the aggregator.

# defaults, per class
short    30 days    # e.g., TRIGGER_FIRED, BATCH_PROCESSED
medium   180 days   # e.g., CONFIG_UPDATED, JOB_COMPLETED
long     3 years    # e.g., AUTH_LOGIN, PAYMENT_COMPLETED
# override via env — no code change
FASTEN_AUDIT_RETENTION_CLASS_LONG=2555          # 7y (regulated)
FASTEN_AUDIT_RETENTION_OVERRIDE="long=7y,medium=1y"
FASTEN_RETENTION_SWEEP_INTERVAL=6h

PII minimisation + API log persistence

opt-in

Codes declared pii_in_detail: true force the short class regardless — shortest safe retention by default. API log rows go ring-buffer + stdout by default; persist them via FASTEN_API_DSN when you need a durable HTTP access trail.

# .env — opt into persistent API log
FASTEN_API_DSN=postgres://user:pw@db/app?table=api_log
FASTEN_API_RETENTION_DAYS=180                   # HIPAA-friendly
# code declaration — retention + PII class on the code
register("user", {
  "AUTH_LOGIN":  Meta(..., retention_class="long"),
  "AUTH_FAILED": Meta(..., retention_class="long",
                            pii_in_detail=True),
})

# or declare codes in *.codes.yaml — single source across every SDK
fasten.codes.load("fleet.codes.yaml")
Retention applies to all three streams.
Syslog + API log also have ring-buffer size caps: FASTEN_SYS_RING_SIZE · FASTEN_API_RING_SIZE
Docker log-driver rotation handles on-disk stdout size.

AI agent fit

Agents call tools. Tools touch resources.
Who audits that today? Nobody.

LangSmith, OTel, OpenAI Assistants — LLM-ops or timing tools, not typed audit. fasten fits natively: every tool call is an audit row — actor / target / method / correlation.

Multi-agent workflows

Agent A calls agent B calls a tool. One request_id threads the chain.

Cost accounting

Every TOOL_CALLED row carries detail.cost_usd. Sum by actor for billing.

Regulated AI

Clinical-summariser touches PHI → audit row with pii_in_detail, GDPR retention.

Agent debugging

"Why did the agent say X yesterday?" One request_id query pulls the trail.

language coverage

Wire it once. Read it anywhere.

The audit row schema, the 7 anchors, the request_id contract — identical across implementations. A row written in Go reads cleanly from Python. A Rust service feeds a Node + TypeScript report.

Pythonreference
pip install fasten
Gousable
git clone & install from source — module path lands with v1.0 GA
C++14single-header
#include "fasten.hpp"
Node + TypeScriptbeta
npm install @nerdapplabs/fasten
Rustbeta
cargo add fasten
Javacoming soon
sh.fasten:fasten — coming soon

Source-install today. pip install ./python, npm install ./js, or cargo add --path ./rust from the repo. Go works today via its module path. C++14 is always a single-header copy. Registry publish lands with the v1.0 GA tag.

advanced · optional

Beyond HTTP — cross-transport correlation.

HTTP is table stakes. fasten's wedge is the transports HTTP-only tools skip:

  • MQTT — industrial / IoT / pub-sub.
  • scheduler — autonomous, cron-fired jobs.

Each is an opt-in shim. Most adopters never need these.

MQTT round-trip

industrial · IoT

mqtt_shim.publish() auto-injects _req into the payload. The subscriber's mqtt_shim.handler() reads it back into ctx. Every log the subscriber emits during handling carries the same id.

# publisher (HTTP handler — user sets a new setpoint)
await fasten.shim.mqtt.publish(
    client, f"devices/{device_id}/setpoint",
    {"target_temp_c": 22.5},
)
# shim injects _req = current_request_id() into payload
# subscriber (device firmware, any language)
@mqtt_shim.handler("devices/+/setpoint")
async def on_setpoint(msg, ctx):
    # ctx.request_id == original HTTP caller's id
    fasten.log.info("setpoint_received")
    fasten.emit(code="DEVICE_SETPOINT_UPDATED", ...)
# operator — one query stitches both services
$ fasten tail --stream audit --request-id abc
SETPOINT_REQUESTED       api-server  actor=admin   target=dev-42
DEVICE_SETPOINT_UPDATED  device-42   actor=system  # same id ✓

Scheduler shim

autonomous jobs

Cron-fired jobs have no external caller. Without a shim, every scheduled audit row has null or freshly-minted request_id. The scheduler shim mints scheduler-<run_id> at job start, stashes it in ctx.

@scheduler.cron("0 */6 * * *")
@fasten.shim.scheduler.instrument
async def nightly_cleanup():
    # request_id = "scheduler-a1b2c3d4"
    fasten.emit(code="RETENTION_SWEEP_RAN", ...)
    fasten.log.info("cleanup_done")
# all rows for this run share the same id

Custom transport (gRPC, NATS, AMQP, Kafka)? Write a 10-line shim following the same read-id-from-wire / set-ctx / call-downstream pattern.

what it replaces

Honest split. No overclaiming.

Stack today
Replaced?
Where it lands
Jaeger · causal debugging
✓ yes
One request_id query pulls every row from every service.
Jaeger · span-level waterfall
✗ no
Keep Jaeger. request_id ↔ trace_id so they coexist.
Loki · per-node audit search
✓ yes
The mountable /logs/audit reader queries the durable store directly. Fleet-wide aggregation lands with fasten Cloud (in development).
Loki · fleet syslog at scale
✗ no
Ship stdout to Loki downstream if you need it.
Docker logs
kept
stdout stays the contract. Used through /logs/sys reader.
Homegrown audit tables
✓ yes
Native AuditRepository. 7 anchors enforced.

origin

Why we built this

  1. 01

    The setup.

    An industrial edge platform — many services, many languages — MQTT where HTTP can't reach.

    • Gateway · Python
    • Fleet manager · Go
    • Sync relay · Go
    • Connectors · Node + Go
  2. 02

    The recurring question.

    "This event fired at 14:02 — show me what happened next across all these services."

    Answer, every week: grep + timestamp math + cursing.

  3. 03

    What we tried — in the order we tried it.

    1. Our log libraries. structlog and slog format beautifully — but they're loggers, not auditors. No typed shape, no correlation contract. Both still ship in our stack; fasten sits alongside.
    2. OpenTelemetry. Traces threaded HTTP cleanly. The moment we crossed into MQTT, scheduler-fired jobs, or a deploy pipeline, we were propagating context by hand — and OTel still wouldn't commit to a typed audit row.
    3. Our own audit tables. Worked. Then drifted. Three services, three shapes, no shared retention contract — the classic in-house path.
    4. Buying it. Splunk cost more than the rest of our infrastructure combined. Enterprise compliance tools were opaque black boxes — we couldn't audit the auditor.

    None of these, alone or stitched, covered the gap: typed audit threaded across every transport, with one correlation key.

  4. 04

    So we built the missing piece.

    • 5 Ws + H enforced at the type level.
    • One request_id threaded across every transport.
    • A storage contract portable across six languages.

    Open-source because:

    • Regulated buyers won't adopt a closed audit library.
    • Our moat is the paid data plane — fasten Cloud.
    • The community writes adapters we'd never ship ourselves.

faq

Common questions.

Do I have to replace my existing logger (structlog / slog / pino)?

No. fasten sits alongside your existing logger. Keep using structlog / slog / pino / winston for free-form application logs; use fasten.emit() for typed audit events. fasten.log.* is optional — it mirrors the idioms of each language (kwargs in Python, positional pairs in Go, object-first in pino) if you want one logger across both streams.

How is this different from OpenTelemetry?

OTel is deliberately untyped at the semantic layer. fasten commits to a typed audit shape and ships shims for transports beyond HTTP (MQTT, scheduler). The two coexist — keep OTel for span-level tracing; use fasten for the typed audit row + cross-transport correlation. A direct request_id ↔ trace_id bridge is on the roadmap, not shipped.

Do I need fasten Cloud to use the SDK?

No. The SDK is Apache-2.0 and complete on its own. fasten Cloud is the commercial fleet-aggregation + compliance-report layer; single-node and small-fleet deployments use the SDK alone.

Is there a daemon, sidecar, or agent to run?

No. fasten runs in your process — no fastend, no sidecar, no required hosted service. The bundled CLI is a debug tool, not a background process.

Is this production-ready?

v1.0.0-beta. The reference implementation runs in our own platform; the public API is the one that ships at v1.0 GA. Safe for pilots, evaluators, internal tools today. Audit-store failure handling shipped queue-by-default in v1.0.0-beta across all five SDKs (background drainer with exp backoff; emit() stays off the request path). One known caveat before betting critical compliance reporting on it: Postgres fleet-scale validation lands with v1.1, not v1.0 GA. Tracked publicly in the issue tracker.

What about GDPR / HIPAA / regulated-industry compliance?

The SDK gives you the primitives: per-code pii_in_detail flag forces minimum retention (GDPR minimisation), secret-key redaction runs before emit, audit rows carry actor / target / method / timestamp in a defensible typed shape. Generated compliance reports (HIPAA, SOC 2, GMP, ISO 26262, FSSC 22000) are planned for fasten Cloud, which is in development — the SDK is built to feed it.

What does the SDK cost?

Nothing. Apache-2.0, no seat limits, no per-emit charges, no free-tier gotchas. The paid layer (fasten Cloud) is in development; pricing will be published once it ships.

Why not LangSmith or Langfuse for AI-agent audit?

LangSmith locks you to LangChain. Langfuse is close in scope but LLM-ops-first. fasten coexists — you can ship rows to both; they answer different questions.

What's the performance overhead?

~0.1–0.5 ms per emit() with SQLite WAL. ~1–5 ms with Postgres. Ring buffers use ~8 MB per service at 10 k lines. No background goroutines or threads — all synchronous in the calling thread. High-volume codes can be sampled via short retention class + periodic sweep.

How do I contribute?

fasten is open-source at github.com/nerdapplabs/fasten. Issues + PRs are welcome; a CONTRIBUTING guide is in flight and will land before the v1.0 GA tag.

open-core

SDK: free forever. Audit data plane: paid.

FREE

fasten SDK

Apache-2.0 · zero-dep core · 5 languages

  • Per-language libraries — Python, Go, Node + TS, Rust, C++14.
  • Bundled CLI — dump · tail · doctor.
  • Mountable /api/v1/logs/{audit,sys,api} reader.
  • Three streams + 7 audit anchors enforced at the type level.
  • Bring-your-own DB — SQLite, Postgres.
Get the SDK
PAID · coming soon

fasten Cloud

The audit data plane — hosted or self-hosted.

  • Multi-node audit aggregation across the fleet.
  • Compliance reports — HIPAA, SOC 2, GMP, ISO 26262, FSSC 22000.
  • Tamper-evident archive — chained hashing + Sigstore Rekor seal.
  • Tiered retention — hot (Postgres) / cold (S3 + Parquet) / WORM.
  • SLA-backed support, SSO, audit-log-of-the-audit-log.
Talk to us →