Skip to content

Logs

Atlas produces two logs that are easy to confuse but answer different questions, and are deliberately never merged into one.

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=201

It’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 StreamHandler flushes 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 makes docker logs -f or 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.