# Audit logging

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

_(Enterprise)_
_(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.

> [!WARNING]
> The `SURREAL_AUDIT_*` variables are registered only by the Enterprise binary. As Community builds never reads them, the server starts normally, nothing is written to the configured path, and no audit file is created. The loud startup failures described below apply to Enterprise builds only, so do not rely on these settings for compliance on a Community build.

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](#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](/docs/manage/observability/configuration.md#audit-log-knobs).

## Events captured

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

<table>
    <thead>
        <tr>
            <th scope="col">Event name</th>
            <th scope="col">Severities</th>
            <th scope="col">Captures</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td scope="row" data-label="Event"><code>surrealdb.audit.statement</code></td>
            <td scope="row" data-label="Severities"><code>Info</code> / <code>Error</code></td>
            <td scope="row" data-label="Captures">Completion of an individual statement.</td>
        </tr>
        <tr>
            <td scope="row" data-label="Event"><code>surrealdb.audit.query</code></td>
            <td scope="row" data-label="Severities"><code>Info</code> / <code>Error</code></td>
            <td scope="row" data-label="Captures">Completion of a multi-statement query.</td>
        </tr>
        <tr>
            <td scope="row" data-label="Event"><code>surrealdb.audit.transaction</code></td>
            <td scope="row" data-label="Severities"><code>Info</code> / <code>Error</code></td>
            <td scope="row" data-label="Captures">Commit or rollback of a transaction.</td>
        </tr>
        <tr>
            <td scope="row" data-label="Event"><code>surrealdb.audit.rpc</code></td>
            <td scope="row" data-label="Severities"><code>Info</code> / <code>Error</code></td>
            <td scope="row" data-label="Captures">Completion of an RPC call.</td>
        </tr>
        <tr>
            <td scope="row" data-label="Event"><code>surrealdb.audit.auth</code></td>
            <td scope="row" data-label="Severities"><code>Info</code> (success) / <code>Warn</code> (any non-success) / <code>Error</code> (<code>outcome=error</code>)</td>
            <td scope="row" data-label="Captures">Sign-in, sign-up and authentication outcomes.</td>
        </tr>
        <tr>
            <td scope="row" data-label="Event"><code>surrealdb.audit.session</code></td>
            <td scope="row" data-label="Severities"><code>Info</code></td>
            <td scope="row" data-label="Captures">Session connect and disconnect events.</td>
        </tr>
        <tr>
            <td scope="row" data-label="Event"><code>surrealdb.audit.http</code></td>
            <td scope="row" data-label="Severities"><code>Info</code> / <code>Error</code> (typically 5xx responses)</td>
            <td scope="row" data-label="Captures">Completion of an HTTP request.</td>
        </tr>
    </tbody>
</table>

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`).

## Record shape

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.
- `outcome` - `success`, `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](#hash-chaining) is enabled.

A captured `statement` event with hash chaining enabled looks like:

```json
{
  "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…"
}
```

## Rotation and durability

- 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.

## Hash chaining

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

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 pass** - `SURREAL_AUDIT_REDACT_TABLES=secrets,pii` performs a case-insensitive replacement of each identifier token with `***`.
3. **Regex pass** - `SURREAL_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`.

## Overflow semantics

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.

## Pipeline self-metrics

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.

| Metric | Notes |
| --- | --- |
| `surrealdb_audit_records` | Cumulative records successfully enqueued. |
| `surrealdb_audit_dropped` | Cumulative records dropped (overflow or queue closed). **Alert on any non-zero rate.** |
| `surrealdb_audit_queue_depth` | Records currently buffered between observer and worker. Sustained depth above ~50% of `SURREAL_AUDIT_QUEUE_CAPACITY` indicates a slow sink. |
| `surrealdb_audit_appended` | Cumulative records the worker wrote to the sink. The gap to `surrealdb_audit_records` is queue depth plus append errors. |
| `surrealdb_audit_append_errors` | Cumulative 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](/docs/manage/observability/slow-query-logging.md#pipeline-self-metrics).

## Related references

- [Configuration → Audit log knobs](/docs/manage/observability/configuration.md#audit-log-knobs) - every audit-log environment variable.
- [Configuration → Compliance checklist](/docs/manage/observability/configuration.md#compliance-checklist) - the minimum tamper-evident configuration.
- [Slow-query logging](/docs/manage/observability/slow-query-logging.md) - the sister pipeline for triage data.
- [Metrics reference → Audit log pipeline self-metrics](/docs/manage/observability/metrics.md#audit-log-pipeline-self-metrics) - the five gauges in the metric catalogue.
