Logs and audit¶
Logs¶
Every entry has the same fields wherever it goes: UTC time, level, category, message, event id,
request id, trace id, and the user's id when there is one. Passwords, cookies, tokens, secrets and
task content are never logged; a test runs the whole server at trace level and checks.
Where they go:
- The console (
docker logs, the systemd journal), as readabletextorjson(LOG_FORMAT). - Files in
LOG_DIR(/data/logs), JSON lines. A file rolls over at midnight UTC or atLOG_FILE_MAX_MB; older files are gzipped and deleted afterLOG_FILE_KEEP_DAYS. Turn files off withLOG_FILES=falsewhen the console is collected anyway. - Log servers, below.
Levels: LOG_LEVEL (default information), and per category with
LOG_LEVEL_OVERRIDES=Punchlist.Sync=debug,Microsoft=warning. Levels and log servers saved in
Admin > Settings apply at once, without a restart. Admin > Logs > Outputs shows each output,
whether it keeps up, and how many entries it dropped.
Sending logs to a log server¶
Set one or more. Each has its own queue of up to 10,000 entries, retried with growing waits; a log server that is down never holds Punchlist up, and what does not fit is dropped and counted.
| Stack | Settings |
|---|---|
| OpenTelemetry (Grafana Alloy, OTel Collector, SigNoz, Elastic) | LOG_OTLP_ENDPOINT=http://alloy:4318 (OTLP/HTTP, JSON), optional LOG_OTLP_HEADERS |
| Grafana Loki | LOG_LOKI_URL=http://loki:3100, optional LOG_LOKI_USERNAME, LOG_LOKI_PASSWORD, LOG_LOKI_TENANT |
| Graylog | LOG_GELF_ADDRESS=udp://graylog:12201 (or tcp://, or http://graylog:12201 for a GELF HTTP input) |
| Elasticsearch, OpenSearch (ELK) | LOG_ELASTIC_URL=https://es:9200, LOG_ELASTIC_API_KEY or user and password, LOG_ELASTIC_INDEX |
| Syslog (rsyslog, syslog-ng, Vector) | LOG_SYSLOG_ADDRESS=udp://rsyslog:514 or tcp://... (RFC 5424, facility local0) |
Loki: entries get the labels service_name="punchlist", host and level, and each line is
the JSON entry, so every field is queryable:
{service_name="punchlist"} | json | level="error"
{service_name="punchlist"} | json | userId="01M5JR..." | line_format "{{.message}}"
Elasticsearch: entries use ECS names (@timestamp, log.level, log.logger, trace.id,
user.id, http.request.id). The default index lands in the built-in logs-*-* data stream, so
Kibana's Discover and Logs show them without a mapping. OpenSearch creates the index on first write.
Graylog: add a GELF input (UDP, TCP or HTTP) on port 12201. Fields arrive as _category,
_request_id, _trace_id, _user_id, and the message's own values as _prop_<name>.
Already collecting container logs with Promtail, Alloy, Filebeat, Fluent Bit or Vector? Set
LOG_FORMAT=json and point them at docker logs, or at /data/logs/*.log.
Access and event log¶
Admin > Logs keeps two logs in the database, apart from your data and never in backups:
- Events: sign-ins (and failed ones, lockouts, provider sign-ins and links), password changes and resets, accounts added, turned off or on, role and group changes, provider unlinks and migration windows, viewing the app as someone (start and end), friendships set by an admin, sign-ups and invite links, sharing (a list shared or made private, people added to or taken off a list or a group, roles changed), settings saved (which ones, never their values), restarts, backups (scheduled ones too, and failed ones), exports, restores and debug dumps.
- Requests: every API and sign-in request, with the route (never the query string or body), the answer, how long it took, the person, address and device.
Each entry says who, the admin viewing the app as them if any, what, about whom, how it ended,
from where and when. Search, filter by area, outcome, person, address and dates, open an entry for
its details, and download the filtered set as CSV. Entries also go to the logs (categories
Punchlist.Audit and Punchlist.Access), so they reach a log server too;
LOG_LEVEL_OVERRIDES=Punchlist.Access=warning keeps requests out of the console.
Old entries are removed hourly: events after AUDIT_RETENTION_DAYS (90), requests after
ACCESS_LOG_RETENTION_DAYS (14). AUDIT_ARCHIVE=true writes them to gzipped JSON lines in
AUDIT_ARCHIVE_DIR first. Behind a reverse proxy, check TRUSTED_PROXIES
so the log shows people's own addresses, not the proxy's.
Debug dump¶
For a bug report: Admin > Logs > Download debug dump, or
The zip holds recent logs, every setting with where it comes from (secrets shown as ********),
the version, build, time zone, platform, database row counts and health. It never holds task
content, password hashes or keys, so it can go with a public issue.