Skip to main content

Module audit

Module audit 

Source
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 via println!. The default sink: stdout is what container runtimes capture and ship. Also mirrors each record at tracing::info! level so OpenTelemetry tail-sampling sees it.
  • FileJsonSink — appends to a file. The file is opened append-only and the writes serialise through an Arc<std::sync::Mutex<File>>. The critical section is write_all + flush of ~512 bytes — measured below in the latency budget section. PERF: the blocking write is offloaded to tokio::task::spawn_blocking when a runtime is present so it never parks the async response path.
  • NoopSink — drops every record. Used when TENSOR_WASM_API_AUDIT_LOG=none and exposed for tests.

§Configuration

Read at server start via AuditConfig::from_env:

TENSOR_WASM_API_AUDIT_LOG valueResulting sink
unset / emptyStdoutJsonSink (default — stdout)
noneNoopSink (audit disabled)
stdoutStdoutJsonSink (explicit form)
file:/path/to/audit.logFileJsonSink (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_string on a ~10-field struct + one println!. 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 a write(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§

AuditActor
Authenticated principal that issued the request.
AuditConfig
Resolved audit-log configuration loaded from the process environment.
AuditOutcome
What came back from the handler.
AuditOutcomeExt
Marker type inserted into the response extensions by handlers so the audit middleware can recover the error.kind value without re-parsing the JSON body. The handler stamps this when it converts an ApiError into a response.
AuditRecord
Top-level audit record emitted once per state-mutating HTTP call.
AuditResource
Resource the action targeted. Both fields are optional: not every action binds to both, and POST /functions has no function id at the time the route is matched (the new id is assigned by the handler).
FileJsonSink
File-backed sink. Records are appended to path as JSONL.
NoopSink
No-op sink. Selected by TENSOR_WASM_API_AUDIT_LOG=none for deployments where the operator aggregates Prometheus + OTel and does not want a third audit stream.
StdoutJsonSink
Default sink — writes each record to stdout as a single JSON line and mirrors it at tracing::info! for OTel correlation.
TrustedProxies
Allowlist of reverse-proxy peer addresses whose X-Forwarded-Client-Cert headers the audit middleware will trust.

Enums§

AuditAction
Catalogue of state-mutating actions recognised by the audit log.
AuditActorKind
Discriminator on AuditActor::kind.
TokenScopeView
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-Cert headers the audit middleware will trust. Empty / unset = never trust XFCC (the safe default — see TrustedProxies).
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§

AuditSink
Pluggable target for AuditRecord emission.

Functions§

audit_log_middleware
Axum middleware that synthesises one AuditRecord per state-mutating HTTP call and dispatches it to the configured AuditSink.