A deployment printed four different log formats and none of them could be joined up: uvicorn's log_config.json for the API, a hand-rolled StreamHandler in main.py for anything not started by uvicorn, logging.basicConfig() in the worker and in the download child (no timestamp at all), and alembic's fileConfig. app/logs.py replaces all four with one JSON line per event (OpenTelemetry's log data model, no SDK): ts, level, severity, source, logger, event code, message, attributes, plus the correlation the context carries — request id, account, download job. Redaction is applied at the HANDLER, not at the call sites: every message, every attribute and every traceback is scrubbed on the way out, so a secret cannot leak by being forgotten at one of ~125 call sites. Passwords, tokens, cookies, secret config values and email addresses never reach a line. The access log is ours now (`http.request`): the route template instead of the concrete path, the status, the duration and the account — and never the query string, which uvicorn's access log printed on every single request (`?account=`, `?source=`, the `?token=` of a share link). A well-formed client request id is accepted and echoed in X-Request-Id, so a browser line and a server line join up. The account comes from `request.state`, filled by the auth dependency: the middleware is the outermost one, so it runs outside SessionMiddleware and in a different task context. `log_level` becomes an admin knob (sysconfig), applied without a restart and re-applied at startup — best-effort on purpose, since a logging knob must never be what stops the app from booting. Every compose service gets bounded json-file logs (3×50 MB): Docker keeps every line forever by default, and the API now writes one line per request. Verified on the localdev stack: JSON everywhere including alembic and the worker, the query strings gone, a supplied X-Request-Id echoed and logged, and three /healthz hits of which only the one made while the level was DEBUG produced a line.
23 lines
1.2 KiB
Bash
23 lines
1.2 KiB
Bash
#!/bin/sh
|
|
set -e
|
|
|
|
echo "Applying database migrations..."
|
|
alembic upgrade head
|
|
|
|
echo "Starting Siftlode API..."
|
|
# --timeout-keep-alive 75: keep idle upstream connections alive longer than the reverse
|
|
# proxy's idle-reuse window. uvicorn's default (5s) is shorter than Caddy's pooled-keepalive
|
|
# timeout, so Caddy would reuse a connection uvicorn had just closed -> broken pipe / reset
|
|
# on the next request -> 502. Non-idempotent POSTs (the player's progress/state writes,
|
|
# fired every 5s) can't be auto-retried by the proxy, so those 502s reached the user.
|
|
#
|
|
# SECURITY: do NOT add --proxy-headers / --forwarded-allow-ips here. ratelimit.client_key trusts
|
|
# X-Forwarded-For only when the TRUE socket peer is a configured proxy (TRUSTED_PROXY_IPS); those
|
|
# flags would make uvicorn overwrite request.client from the forgeable XFF, defeating that check and
|
|
# reopening the rate-limit bypass (SA3).
|
|
#
|
|
# No --log-config: app/logs.py configures logging for every Siftlode process (api, worker, download
|
|
# child, alembic) in one place, and it silences uvicorn's access log in favour of our own
|
|
# `http.request` line (route template, no query string, request id).
|
|
exec uvicorn app.main:app --host 0.0.0.0 --port 8000 --timeout-keep-alive 75
|