• Start

Audit logging

The Enterprise audit log pipeline: events captured, record shape, rotation, hash chaining, redaction and pipeline self-metrics.

Enterprise
Available since: v3.1.0

The audit log pipeline records authoritative, identity-bound events that operators and auditors need to reconstruct who did what, when, and with what outcome. It is part of SurrealDB Enterprise and is off by default — set SURREAL_AUDIT_SINK=file and SURREAL_AUDIT_FILE_PATH to enable it.

Records flow through two parallel paths:

  1. Durable file sink. A bounded queue feeds a background worker that appends each record to an NDJSON file. Optional SHA-256 hash chaining provides tamper-evidence; size-based rotation and a tunable fsync cadence keep the file manageable. This is the primary path for compliance and SIEM ingestion.

  2. OpenTelemetry logs. The same record can also be emitted as an OTel LogRecord on the SDK logger provider. Off by default; opt in per pipeline with SURREAL_AUDIT_OTEL_EXPORT=true. Compliance-sensitive deployments typically keep this off and rely on the file sink.

The observer hot path never blocks on I/O. Redaction runs synchronously on the executor thread before the record reaches the queue, so the worker can write raw bytes straight to the sink and the OTel emit cannot leak unredacted content.

The full set of configuration variables lives on the configuration reference.

Each event surfaces as an OTel LogRecord with a specific event name and a severity that depends on the outcome:

Event nameSeveritiesCaptures
surrealdb.audit.statementInfo / ErrorCompletion of an individual statement.
surrealdb.audit.queryInfo / ErrorCompletion of a multi-statement query.
surrealdb.audit.transactionInfo / ErrorCommit or rollback of a transaction.
surrealdb.audit.rpcInfo / ErrorCompletion of an RPC call.
surrealdb.audit.authInfo (success) / Warn (any non-success) / Error (outcome=error)Sign-in, sign-up and authentication outcomes.
surrealdb.audit.sessionInfoSession connect and disconnect events.
surrealdb.audit.httpInfo / Error (typically 5xx responses)Completion of an HTTP request.

The OTel LogRecord body is a short human-readable string. The structured fields live on attributes (db.namespace, db.name, db.user, db.statement, session.id, client.address, surrealdb.statement_type, surrealdb.outcome, surrealdb.duration_ms, surrealdb.error_class).

Audit and slow-query files contain one JSON record per line, each terminated by a newline. Audit and slow-query records share a common envelope; audit records additionally carry the event_type that distinguishes the seven event variants above.

Audit event_type values: statement, query, transaction, rpc, auth, session, http.

Envelope fields (common to all records):

  • ts — RFC 3339 timestamp.

  • event_type — see above.

  • outcomesuccess, error, cancelled, or (for auth events) denied / failed.

  • duration_ms — wall-clock duration of the captured operation.

  • Identity context — namespace, database, user resolved from the session.

  • sql — captured statement text when SURREAL_AUDIT_INCLUDE_SQL=true; absent otherwise.

  • prev_hash / hash — SHA-256 chain fields, present when hash chaining is enabled.

A captured statement event with hash chaining enabled looks like:

{
"ts": "2026-03-04T10:23:11.482Z",
"event_type": "statement",
"outcome": "success",
"duration_ms": 14,
"namespace": "acme",
"database": "prod",
"user": "svc_orders",
"sql": "UPDATE orders:abc SET status = 'shipped'",
"prev_hash": "f1a3…",
"hash": "c92e…"
}
  • File mode 0600 on Unix. The parent directory must exist; the server refuses to start otherwise.

  • Size-based rotation at SURREAL_AUDIT_FILE_ROTATE_BYTES (default 256 MiB).

  • The oldest rotation is dropped once SURREAL_AUDIT_FILE_ROTATE_KEEP (default 8) generations are present.

  • Mid-stream fsync cadence is governed by SURREAL_AUDIT_FSYNC_EVERY. Rotation and graceful shutdown always flush and sync_data regardless of cadence.

When SURREAL_AUDIT_HASH_CHAIN=true every record carries:

  • prev_hash — SHA-256 of the previous record in the same file. Absent on the genesis record at the start of a new file.

  • hash — SHA-256 of this record's canonical serialisation (including prev_hash).

Rotation closes a chain and starts a new one with a fresh genesis record. A detector verifies the chain by recomputing each hash sequentially and comparing against the stored hash.

Hash chaining requires SURREAL_AUDIT_FSYNC_EVERY=1. Without per-record fsync, the chain pointer could advance for records that are not durably on disk, leaving on-disk gaps the chain still references and silently weakening the guarantee. Startup fails loudly when the two knobs disagree.

Redaction is applied synchronously on the executor thread before the record reaches the queue, so the worker, the file sink, and the OTel logger all see the same already-scrubbed text. Three layered passes run in order:

  1. Literal pass — when SURREAL_AUDIT_REDACT_LITERALS=true every single- or double-quoted span in the SQL is replaced with '***' / "***".

  2. Identifier-token passSURREAL_AUDIT_REDACT_TABLES=secrets,pii performs a case-insensitive replacement of each identifier token with ***.

  3. Regex passSURREAL_AUDIT_REDACT_REGEX="<pat1>;<pat2>" (note: semicolon-separated) compiles each pattern at startup. An invalid pattern fails startup; valid patterns run in order against the SQL text.

The slow-query log pipeline supports the same three passes under SURREAL_SLOW_QUERY_REDACT_LITERALS, SURREAL_SLOW_QUERY_REDACT_TABLES and SURREAL_SLOW_QUERY_REDACT_REGEX.

Neither overflow policy offers a lossless guarantee:

  • drop — single non-blocking try_send. On Full or Closed the record is dropped and the surrealdb_audit_dropped gauge increments.

  • block — bounded busy-yield loop (200 retries with std::thread::yield_now). yield_now does not park the OS thread, so on a multi-threaded runtime the drain task can make progress between yields and short bursts may be absorbed without drops. On a current_thread runtime the producer holds the only worker and the policy degrades to immediate drop. There is no wall-clock time-bound on the loop — the budget caps iterations only.

The audit pipeline defaults to block because audit records are compliance-sensitive; the slow-query pipeline defaults to drop because slow-query records are triage data.

Whichever policy is configured, alert on rate(surrealdb_audit_dropped[5m]) > 0 and rate(surrealdb_audit_append_errors[5m]) > 0. Both indicate records were lost.

Five observable gauges expose the live state of the pipeline. Each is read at scrape time from atomic counters on the worker, so the cost is zero when nothing consumes the metric.

MetricNotes
surrealdb_audit_recordsCumulative records successfully enqueued.
surrealdb_audit_droppedCumulative records dropped (overflow or queue closed). Alert on any non-zero rate.
surrealdb_audit_queue_depthRecords currently buffered between observer and worker. Sustained depth above ~50% of SURREAL_AUDIT_QUEUE_CAPACITY indicates a slow sink.
surrealdb_audit_appendedCumulative records the worker wrote to the sink. The gap to surrealdb_audit_records is queue depth plus append errors.
surrealdb_audit_append_errorsCumulative sink-write failures. Alert on any non-zero rate.

The slow-query pipeline exposes the same shape under the surrealdb_slow_query_* prefix — see slow-query logging.

Was this page helpful?