Expand description
Structured audit log for state-mutating API calls.
Implements PATH-TO-V1 v0.4 exit-criterion Audit log: every request
that mutates server state writes one JSON record describing who called,
what they did, what came back, and how long it took. Read-only calls
(GET /healthz, GET /metrics, GET /jobs/{id}) emit nothing — they
would drown the log in noise.
§Record shape
Each record is a single JSON object emitted on its own line:
{
"ts_unix_ms": 1716491220123,
"request_id": "b8b6f7e0-...-...",
"actor": {
"kind": "bearer",
"token_id": 14217683...,
"scope": { "kind": "tenant_set", "tenants": [1, 2] }
},
"action": "invoke_function",
"resource": {
"function_id": "f47ac10b-...-...",
"tenant_id": 7
},
"outcome": { "status_code": 200, "error_kind": null },
"latency_ms": 12,
"peer_addr": "10.0.0.1:54321",
"client_cert_subject": null
}Field-stability rules match the rest of the public surface: keys and the
discriminant tags inside AuditAction and AuditActorKind are part
of the public contract; additive forward-compatible fields are allowed.
See docs/AUDIT-LOG.md for the wire-format
contract.
§Sinks
AuditSink is a small trait so consumers can plug in whatever
durable target they prefer. The crate ships two implementations:
StdoutJsonSink— writes each record to stdout viaprintln!. The default sink: stdout is what container runtimes capture and ship. Also mirrors each record attracing::info!level so OpenTelemetry tail-sampling sees it.FileJsonSink— appends to a file. The file is opened append-only and the writes serialise through anArc<std::sync::Mutex<File>>. The critical section iswrite_all+flushof ~512 bytes — measured below in the latency budget section. PERF: the blocking write is offloaded totokio::task::spawn_blockingwhen a runtime is present so it never parks the async response path.NoopSink— drops every record. Used whenTENSOR_WASM_API_AUDIT_LOG=noneand exposed for tests.
§Configuration
Read at server start via AuditConfig::from_env:
TENSOR_WASM_API_AUDIT_LOG value | Resulting sink |
|---|---|
| unset / empty | StdoutJsonSink (default — stdout) |
none | NoopSink (audit disabled) |
stdout | StdoutJsonSink (explicit form) |
file:/path/to/audit.log | FileJsonSink (append-only at that path) |
§Latency budget
The audit middleware runs after the handler completes, so it never
delays the response from the application’s point of view in the sense
of “client sees the body later” — axum::middleware::from_fn happens to
serialise the post-handler work into the response future, but the bytes
the client receives flow once the inner future yields. The relevant
cost is therefore the CPU time spent in AuditSink::emit:
StdoutJsonSink:serde_json::to_stringon a ~10-field struct + oneprintln!. The macro takes the stdout lock for the duration of the write. Locally measured at ~6–18 µs per call on a modern x86 workstation (cold) — well inside the < 100 µs budget.FileJsonSink:serde_json::to_string+Mutex<File>::lock()+write_all+flush. The flush forces awrite(2)per record so crashes lose at most one record; on commodity NVMe the worst case we observed is ~30–80 µs (Linux ext4, single-writer). On a slow disk or under contention this can spike — the documented mitigation below applies.
PERF (resolved): the blocking write no longer runs inline on the async
response path. Both StdoutJsonSink and FileJsonSink now offload
the write_all + flush (or println!) to
tokio::task::spawn_blocking when an active tokio runtime is detected
(the production middleware path), and fall back to a synchronous write
otherwise (unit tests / non-async embedders). emit stays sync and the
AuditSink trait stays object-safe: serialisation happens on the
caller’s thread and only the owned line (plus a cheap Arc file-handle
clone for the file sink) is moved into the worker. Ordering across
concurrent writers stays well-defined — the worker re-acquires the inner
Mutex<File> and O_APPEND makes each append atomic, so records never
interleave. Records are queued, never dropped, when the bound is hit.
Read-only routes do not invoke the sink at all (the route filter
short-circuits before serialisation), so the entire mechanism is zero
cost on the dominant GET /metrics scrape path.
Structs§
- Audit
Actor - Authenticated principal that issued the request.
- Audit
Config - Resolved audit-log configuration loaded from the process environment.
- Audit
Outcome - What came back from the handler.
- Audit
Outcome Ext - Marker type inserted into the response extensions by handlers so the
audit middleware can recover the
error.kindvalue without re-parsing the JSON body. The handler stamps this when it converts anApiErrorinto a response. - Audit
Record - Top-level audit record emitted once per state-mutating HTTP call.
- Audit
Resource - Resource the action targeted. Both fields are optional: not every
action binds to both, and
POST /functionshas no function id at the time the route is matched (the new id is assigned by the handler). - File
Json Sink - File-backed sink. Records are appended to
pathas JSONL. - Noop
Sink - No-op sink. Selected by
TENSOR_WASM_API_AUDIT_LOG=nonefor deployments where the operator aggregates Prometheus + OTel and does not want a third audit stream. - Stdout
Json Sink - Default sink — writes each record to stdout as a single JSON line and
mirrors it at
tracing::info!for OTel correlation. - Trusted
Proxies - Allowlist of reverse-proxy peer addresses whose
X-Forwarded-Client-Certheaders the audit middleware will trust.
Enums§
- Audit
Action - Catalogue of state-mutating actions recognised by the audit log.
- Audit
Actor Kind - Discriminator on
AuditActor::kind. - Token
Scope View - Stable, JSON-friendly projection of a
TokenScope.
Constants§
- AUDIT_
LOG_ DISABLED_ VALUE - Sentinel value that disables the audit log entirely
(
TENSOR_WASM_API_AUDIT_LOG=none). - AUDIT_
LOG_ FILE_ PREFIX - Prefix that selects the file sink:
file:/absolute/path/to/audit.log. - AUDIT_
LOG_ STDOUT_ VALUE - Sentinel value that selects the explicit stdout sink. Equivalent to leaving the variable unset.
- ENV_
AUDIT_ LOG - Environment variable selecting the audit-log destination. See module docs for the accepted values.
- ENV_
TRUSTED_ XFCC_ PROXIES - Environment variable carrying a comma-separated allowlist of IPv4/IPv6
addresses or CIDR ranges whose
X-Forwarded-Client-Certheaders the audit middleware will trust. Empty / unset = never trust XFCC (the safe default — seeTrustedProxies). - HEADER_
XFCC - Header name from which the audit middleware recovers the
client-certificate Subject when an XFCC-aware reverse proxy is in
front of the gateway. See
docs/deployment/mtls.md§4.4.
Traits§
- Audit
Sink - Pluggable target for
AuditRecordemission.
Functions§
- audit_
log_ middleware - Axum middleware that synthesises one
AuditRecordper state-mutating HTTP call and dispatches it to the configuredAuditSink.