Logs
Atlas produces two logs that are easy to confuse but answer different questions, and are deliberately never merged into one.
The access log: every request, on stdout
Section titled “The access log: every request, on stdout”A dedicated Python logger, sitting right alongside uvicorn’s own
uvicorn.access, prints one line per request:
INFO: 2026-08-25T10:03:12+02:00 (user=alice) method=POST path=/api/v1/documents status=201It’s a plain logging.Logger writing to stdout through a StreamHandler
— nothing fancier than what uvicorn itself uses, styled to match its
colored LEVEL: message output so a line from Atlas doesn’t visually
stand out from uvicorn’s own. Two details matter operationally:
- Unbuffered. Python’s
StreamHandlerflushes on every single emitted record, so a line is on stdout the instant it’s logged — not sitting in a buffer waiting to be flushed later. That’s what makesdocker logs -for any log shipper tailing stdout actually real-time. - Propagates to the root logger, rather than being isolated to its own
namespace. Nothing else in the app configures the root logger, so this
costs nothing in normal operation — but it’s what lets a test harness or
any tool that hooks the root logger (pytest’s
caplog, for one) still observe these lines without Atlas needing to expose a separate API for it.
Severity follows HTTP status, decided in exactly one place so escalating
what counts as noteworthy only ever needs to change once: 5xx → error,
401/403 → warning, everything else → info. An unhandled exception —
a real bug, not a modeled error response — is logged with a full traceback
at error level, then re-raised unchanged so FastAPI’s own handling still
sees it.
This log answers “what is the API doing right now” — every request, read or write, authenticated or not. It’s operational noise by design: useful for graphing traffic and catching failures, not for answering “who changed this document.”
The audit trail: what changed, at the application level
Section titled “The audit trail: what changed, at the application level”That second question is answered by a completely different mechanism: an
AuditEvent, written to Postgres — not a log line — for every mutating
governance action specifically (index, upload, rename, delete, lock, a role
change, …), never for a plain read. Where the access log is infrastructure
plumbing that happens to sit inside the app, the audit trail is written
by the application, as a deliberate business record: it carries the
actor, the bucket and key, an action type, and action-specific structured
detail — and it’s what webhooks actually dispatch from, and
what GET /events searches.
| Access log | AuditEvent | |
|---|---|---|
| Written by | The web framework layer, for every request | The application, only for a mutating governance action |
| Format | A stdout text line | A structured Postgres row |
| Queryable | No — grep your log shipper | Yes — GET /events, filterable by workspace, action, actor, key |
| Retention | Whatever your log shipper keeps | Never pruned |
| Drives webhooks | No | Yes |
Nothing to configure on either side — both are always on. Point your log
shipper at the process’s standard output for the access log; query
GET /events for the audit trail, or subscribe to it via
webhooks instead of polling.