# Audit Log (https://portunus.bybee.dev/en/docs/observability/audit-log)



Every `/v1/*` request emits exactly one structured tracing event:

* `event = "operator.allow"` (INFO) on success.
* `event = "operator.deny"` (WARN) on RBAC denial.

Each entry carries `actor`, `method`, `path`, `outcome`, and (on deny)
the `RbacError::code()` reason. **Raw bearer tokens never reach the
audit code path** — the audit emitter only sees the post-verify
`OperatorIdentity`.

## Persistence [#persistence]

Since v0.8 the audit log is durable in the SQLite `audit` table.
Earlier versions used an in-memory ring buffer (capacity 1000) — the
SPA's audit page has used the new envelope endpoint since then.

## Reading the log [#reading-the-log]

### Live tail (v0.7-shape, back-compat) [#live-tail-v07-shape-back-compat]

```sh
curl -H "Authorization: Bearer $PORTUNUS_OPERATOR_TOKEN" \
     'http://127.0.0.1:7080/v1/audit?limit=100&outcome=deny'
# → JSON array of AuditEntry objects, newest-first
```

This path is byte-stable for v0.7 callers (no `since`/`until`/`cursor`).

### Historic scroll-back (v0.8+ envelope) [#historic-scroll-back-v08-envelope]

```sh
# Last 24 hours, paginated
curl -H "Authorization: Bearer $PORTUNUS_OPERATOR_TOKEN" \
     'http://127.0.0.1:7080/v1/audit?since=2026-05-07T15:00:00Z&limit=50'
# → {"entries":[...], "next_cursor":"abc123", "count":50}

curl -H "Authorization: Bearer $PORTUNUS_OPERATOR_TOKEN" \
     'http://127.0.0.1:7080/v1/audit?cursor=abc123&limit=50'
# → next page
```

The Web UI's audit page uses the envelope automatically — operator
clicks "Load earlier" to walk the cursor.

### Filter by outcome [#filter-by-outcome]

```sh
?outcome=allow
?outcome=deny
```

The Web UI also exposes a client-side outcome filter for fast scrolling
over already-fetched rows.

## Pruning [#pruning]

The durable audit table is bounded by the prune CLI, not by a fixed
ring size:

```sh
# Dry run
portunus-server audit prune --before 2026-05-01T00:00:00Z --dry-run

# Real prune (BEGIN IMMEDIATE + PRAGMA incremental_vacuum)
portunus-server audit prune --before 2026-05-01T00:00:00Z
```

Run from cron daily / weekly to bound database growth.

## Buffer overflow signal [#buffer-overflow-signal]

`portunus_audit_buffer_drops_total` increments whenever an audit entry
is lost to backpressure: an eviction from the in-memory ring (at
capacity 1000) or an overflow of the durable writer's bounded hand-off
queue. Both are semantically "we lost an audit entry" and share the one
counter. It should stay at zero in normal operation; sustained growth
means the durable writer can't keep up. `portunus_audit_durable_writer_lag_seconds`
is the leading indicator — it climbs before drops start.

## What is **not** audited [#what-is-not-audited]

The audit log captures **operator** events only — pushing rules,
managing users, etc. Data-plane events (forwarded byte counts,
rate-limit rejects, SNI route decisions, DNS failures) are
**tracing-only** and do not enter the audit ring or the SQLite audit
table. This is a deliberate constitutional invariant (D13) so:

* The audit table size scales with operator activity, not traffic.
* Data-plane noise doesn't drown out RBAC events.
* Per-tenant ownership remains the audit's organising principle.

## Export to JSON [#export-to-json]

The Web UI's "Download as JSON" button on the audit page emits an
NDJSON file of currently-visible rows:

```
audit-2026-05-09.ndjson
{"timestamp":"…","actor":"alice","method":"POST","path":"/v1/rules","outcome":"deny","reason":"port_outside_grant"}
{"timestamp":"…",…}
```

`head -1` of the file is a complete `AuditEntry` JSON object — feed it
to your SIEM or a downstream pipeline.
