access_log

Optional request log for the whole node — one log, every site, every response: any status, 404s and 500s included, plus explicit RATELIMIT events. Each database record carries the host, so per-site analytics is one WHERE away. The block lives in the ROOT settings.json; remove it to disable logging entirely (no thread, no files). Storage locations are fixed — nothing to configure.

keydefaultdescription
modefilefile — daily files under ./log/file/; sqlite — an access_log table in ./log/sqlite/access.db; duckdb — the same table in a columnar store under ./log/duckdb/
keep_days30how many days to keep (including today); older files are deleted at rotation, older rows — by a daily DELETE
"access_log": { "mode": "sqlite", "keep_days": 30 }

mode=sqlite — the records as table rows (ts, ip, method, proto, host, path, status, dur_us, ua, referer; RATELIMIT events get method=RATELIMIT), written in batched transactions. The referer column stores where the visitor came from (sanitized, capped, NULL when absent). The log database is reachable in /soe under the reserved name log — read-only, authorized by the DEFAULT site’s users — so traffic analytics is a console query away: SELECT path, COUNT(*) c FROM access_log WHERE status = 404 GROUP BY path ORDER BY c DESC; This example runs in sqlite mode, so that query works on your own visits right away.

mode=duckdb — the same records in a columnar analytics store: built for questions over months of traffic — top pages, traffic sources, status breakdowns across every site — that a row-store answers slowly. Retention is the same keep_days.

mode=file — "like the big ones": one file per UTC day, finished days are zstd-compressed, old files are deleted automatically. access-2026-07-07.log is the current day; at midnight (UTC) it becomes access-2026-07-07.log.zst and a new file starts. To read a compressed day: zstd -dc access-2026-07-07.log.zst | less.

Line format — timestamp, client IP, method, full URL with scheme and host (query string is omitted — it may contain sensitive values), status, duration, user-agent, and the site that answered:

2026-07-07T01:16:35Z 203.0.113.7 GET https://example.com/docs/getting-started 200 1.23ms "Mozilla/5.0 …" site=example.com

A triggered rate limit is logged as its own event with the limiter key — useful because /soe replies HTTP 200 with the error in the body, so the status alone would not show it:

2026-07-07T01:16:35Z 203.0.113.7 RATELIMIT auth:ip:203.0.113.7 /cms/login/start 429 -

The table is indexed by time, IP, status and path. The client IP honors trusted_proxies (see the server chapter): behind a proxy it is the X-Real-IP value, otherwise the connection address. Logging is non-blocking in both modes — requests never wait for storage; if it stalls, lines are dropped rather than queued forever.

← All articles in this group