feat: persistent rotating logs + live Logs UI#64
Merged
Conversation
Add a native, in-process pino multistream setup: logs fan out to console, a size-rotating NDJSON file (persisted next to the sqlite DB under the config volume), a live SSE hub, and the existing OTel bridge — all without a worker thread (which thread-stream transports don't survive under Bun) or external logrotate. The management API gains GET /logs?lines=N&level= (backfill from the persisted file) and GET /logs/stream (SSE live tail via the hub, which taps the same log pipe so it's oblivious to rotation). New env: LOG_TO_FILE, LOG_DIR, LOG_MAX_FILE_BYTES, LOG_MAX_FILES.
New /logs page: backfills the last 500 lines from GET /logs, then live-tails via an EventSource against /logs/stream through the BFF proxy. Level filter (reloads backfill + reconnects the stream), pause/resume, clear, click-to-expand row detail, and stick-to-bottom auto-scroll. Adds a Logs nav entry.
…ters - log-store: constructing the rotating file sink no longer throws at import time. A non-writable log dir now warns to stderr and disables file logging instead of crashing the process before startup. - backfill: read across rotated files (.1, .2, …) until `lines` is satisfied, so recent history isn't dropped right after a rotation. - level filter: fail closed in both backfill and the SSE stream — a record without a numeric level is excluded under a level floor, not let through. - ui: close the existing EventSource before reloading on a filter change, so the old stream can't append previous-filter lines during the backfill.
A fast level change could start two reload()s; if the older backfill resolved last it would overwrite rows with stale, previous-filter data. Each reload now takes a sequence ticket and only the newest may replace rows, clear pending, or reconnect the stream.
0fd864a to
5034483
Compare
… stream Bun's HTTP server withholds the response header block until the first byte of the body is written. The live-logs SSE stream stays idle until the first log event (or the 15s keep-alive), so its `text/event-stream` headers were never flushed and a reverse proxy in front (Traefik) hung indefinitely waiting for them — the browser's EventSource and direct clients both saw a dead connection. Open the stream with a `:` comment (ignored by EventSource) so the first body byte — and thus the headers — go out immediately, and add `X-Accel-Buffering: no` for nginx-family proxies. Verified against a raw socket: without the comment the header block never terminates; with it, headers flush at t=0.
A fast level-filter change closes the old EventSource and opens a new one, but a `message`/`open`/`error` callback already dispatched from the old stream could still run after the switch and append a previous-filter record into the freshly loaded rows (or flip `connected`). Capture the created EventSource and gate each callback on it still being the active `source`, so only the current stream can mutate state. Complements the reload ticket that already guards backfill.
5034483 to
797dbac
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds durable log storage the UI can read, per the design we settled on: an in-process rotating NDJSON file (no external logrotate, no worker threads) plus a management API the Nuxt UI consumes for backfill + a live SSE tail. Pitchfork/supervisor is intentionally not part of this — deferred to a later change.
Backend
logger.tsnow fans everything through one in-process pinomultistream:RotatingFileSink— a ~90-line native sink (node:fs, synchronous). Rotates before the overflowing write so every file stays withinmaxBytes; renamesjack.ndjson → .1, shifts.Nup, prunes pastmaxFiles. No dependency (your call), stable active-file path.LogHub— doubles as amultistreamdestination: parses each line and pushes it to live SSE subscribers, and servesbackfill()(last N lines) from the persisted file. Live delivery taps the log pipe directly, so it's oblivious to rotation — no file-tailing/reopen dance.dirname(APP_CONFIG_PATH)/logs=/config/logs, the same mounted volume as the sqlite DB. No Dockerfile/compose change. New env (all defaulted):LOG_TO_FILE,LOG_DIR,LOG_MAX_FILE_BYTES(10 MiB),LOG_MAX_FILES(5).GET /logs?lines=N&level=(backfill) andGET /logs/stream(SSE live tail).Redaction already runs in pino's
logformatter, so the file/hub receive pre-scrubbed lines.UI
New Logs page (+ nav entry): backfills the last 500 lines, then live-tails via
EventSourcethrough the existing BFF proxy. Level filter (reloads backfill + reconnects), pause/resume, clear, click-to-expand row detail, stick-to-bottom auto-scroll.Verification
GET /logs?level=warnreturns exactly the warn+error lines, no-key → 401,/logs/stream→text/event-stream.nuxt typecheckclean, eslint clean, and a full productionnuxt buildsucceeds (SFC compiles/bundles).res.bodyas a stream for every GET, so the live tail should pass through unchanged.Requires the gitignored generated schema types (
bun run generateinpackages/schemas) for typecheck — unchanged from current CI expectations.Greptile Summary
This PR adds persistent log storage and a live Logs page. The main changes are:
Confidence Score: 5/5
This looks safe to merge.
Reviews (8): Last reviewed commit: "fix(ui): ignore log stream callbacks fro..." | Re-trigger Greptile