Durable logging subsystem on log4cl: stdout default, init-file sinks, structured events #77
Labels
No labels
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
priority:P1
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
nsaspy/quasar#77
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Quasar needs one coherent logging facility built on log4cl instead of the current mix of
diagnostic-log, ad-hocformatto stderr, and subsystem-specific helpers. Default output remains stdout; a durable append-only file sink becomes available through the existing init-file configuration system.Current logging behavior
control-plane/src/debug-logging.lispdefinesdiagnostic-log, the de-facto logging API. It writes to*error-output*(stderr), alwaysfinish-outputs, with format[quasar] ISO8601Z LEVEL subsystem event key=value ...where fields are printed with~S.parse-log-level/log-level-rank(:debug :info :warn :error :off), configured from env varQUASAR_LOG_LEVELonly (debugby default,infowhenCIis set). The init-file config system (quasar.config) has no logging surface at all.control-plane command.received/ok/async-dispatch/unknown/failed/crashed,workspace operation.begin/applied/failed, with plist fields like:request-id :command :workspace :client :async :revision :index :type :id :document-id :dtype :graph-id :code :message :details :condition.diagnostic-log:control-plane.lisp:69andcontrol-plane.lisp:451andactors/melissa/async-control-plane.lisp:62print[control-plane] subscriber failed/unexpected errorwith no timestamp or level.websocket-server.lisphas its ownlog-websocketprinting[websocket] ...to stderr with no timestamp or level.debug-logging.lispis loaded last insystems/quasar-control.asdso itsdispatch-messagewraps the final async implementation; it also carries workspace diagnostic-context helpers used bycommit-operations.Problems / gaps
StandardOutput) do not hold; there is no way to select a sink.quasar.config, documented indocs/CONFIGURATION.md) cannot express logging at all.diagnostic-logevaluates and formats on the caller (actor) thread with a global-level check; there is no common lock between the different writers, so concurrent Sento actors can interleave.Proposed architecture (log4cl-based)
Layered so the four concerns stay separate:
quasar.logpackage:log-event level subsystem event &rest fieldsplus conveniencelog-debug/log-info/log-warn/log-error/log-fatal. Internally calls log4cl (log:logmacros) under a dedicated logger category.diagnostic-logremains as a deprecated alias forlog-eventso existing call sites (workspace diagnostics, dispatch instrumentation) keep working unchanged.ISO8601Z LEVEL subsystem event key=value...(human text for stdout/stderr). Aquasar.json-lines-layoutrenders one JSON object per line for the file sink so durable records are machine-parseable and non-interleaved (singlewrite-sequence/write-stringper record).:stdout(console appender on*standard-output*, default),:stderr,:file(log4clfile-appender, opens:if-exists :append),:off. Daily rotation via log4cldaily-file-appenderis possible later without API change.:immediate-flush t(finish-output per record); stdout keeps log4cl's default flush behavior;quasar.log:flush-logs/quasar.log:shutdown-loggingflush and close, called fromquasar.app:stopso SIGINT/SIGTERM unwinds cleanly.Serialization: log4cl stream appenders are
serialized-appenders (bordeaux-threads recursive lock), which makes concurrent Sento actor threads safe and prevents interleaved records. Encoding/formatting of one record happens inside that lock.Init-file configuration surface
The init file already runs
(in-package #:quasar.config)before the app starts (quasar.app:main→safe-load-init). Add toquasar.config:Invalid values (unknown sink, unknown level, unknown format,
:filewithout a path) fail closed at startup, like the rest of init-file handling:quasar.log:apply-configsignals a clear error duringmain, aborting startup rather than silently falling back.Stdout-default semantics
npm run devkeeps exposing server output in the terminal (dev.mjs already pipes child stdout), and systemd/container defaults (StandardOutput=journal,docker logs) keep working with no config.*log-sink*is:file.Durability semantics
:if-exists :append), so restarts preserve prior logs; directories are created on demand.:immediate-flush tby default: each record is flushed to the OS before the logging call returns, so an abruptSIGKILLloses at most the record being written.flush-intervalcan relax this for throughput.temp-appendersemantics) instead of crashing Quasar; a logging error inside a log call is never re-signaled into actor code.quasar.app:stop(SIGINT/SIGTERM path) flushes and closes the sink.daily-file-appenderis available; not configured by default (YAGNI), but the sink layer leaves room for it.Structured log fields
Minimum supported per record: timestamp (ISO 8601 UTC), severity/level, subsystem, event name, human message where appropriate, structured key/value context, request ID, workspace ID, actor/component identity, error/condition details, crash/fatal events (
control-plane command.crashed, plus newapp fatal/app shutdown/app startlifecycle events). Existing subsystem/event vocabulary (control-plane command.received,control-plane command.ok,workspace operation.begin,workspace operation.applied, ...) is preserved verbatim. JSON sink records:{"timestamp","level","subsystem","event","message","fields":{...},"request_id","workspace_id"}withnullwhen absent.Testing strategy
New
control-plane/tests/logging-tests.lisp(registered inquasar-tests.asd) using the existingquasar.testscheckframework, plus a capture-appender harness:npm run bench:loggingor an ASDF-performable script)Full real suites must pass:
npm run test:lisp(SBCL, quasar-tests) and frontend suites;npm run smokefor startup behavior.Acceptance criteria
control-plane command.received,control-plane command.ok,workspace operation.begin,workspace operation.appliedand the rest of the event vocabulary unchanged in output.formatwrites in control-plane/websocket-server route through the new facility.quasar-controldeps, test-lisp/run-production quickload lists, and dependency-check script contract.docs/CONFIGURATION.mddocuments sinks, levels, defaults, file locations, and systemd/container recommendations.npm run test:lispand frontend tests pass; benchmark numbers recorded in the PR.Migration / backward compatibility
diagnostic-logstays as an alias;debug-logging.lispremains the last-loaded instrumentation file; call sites keep their current shape.:stderr. stdout content changes format slightly for previously-unstructured lines (they gain timestamps/levels), which is the point of the issue.QUASAR_LOG_LEVELprecedence and values keep working.Implementation status
Branch
feat/durable-loggingimplements the design described in this issue on top of log4cl.What landed
control-plane/src/logging.lisp— newquasar.logpackage:log-eventstructured API, text layout (historical[quasar] ISO8601Z LEVEL subsystem event key=valueformat), JSON-lines layout for the durable file sink,apply-configwith fail-closed validation,flush-logs/shutdown-logging.quasar.configgains*log-sink*(:stdoutdefault),*log-file-path*(nil →$XDG_DATA_HOME/quasar/logs/quasar.log),*log-level*(nil → env precedence),*log-file-format*(:json),*log-immediate-flush*(t).quasar.app:mainapplies logging config right after init-file load;quasar.app:stopflushes and closes the sink;app start/start-failed/stoplifecycle events added.formatwrites (control-plane dispatch, subscriber failure, websocket server) now route throughquasar.logwith structured events;diagnostic-logremains as a compatibility alias.quasar-control.asd+ all three launcher scripts);check-control-plane-deps.mjsguards it.control-plane/tests/logging-tests.lisp— 14 focused tests (stdout default, file override, level filtering, structured fields text+JSON, concurrency line-completeness, restart persistence, flush/shutdown, sink failure containment, invalid config fail-closed, event vocabulary compatibility).scripts/bench-logging/npm run bench:logging.Reference numbers (20,000 events/scenario, SBCL 2.6.6): stdout text 4.4 µs/event ~544 B; file JSON immediate-flush 6.7 µs/event ~1244 B; file JSON interval flush 4.5 µs/event ~860 B; level-filtered event 0.07 µs/event ~96 B.
Test results
quasar-tests, all subsystem test runners): pass, 0 failures.node scripts/check-control-plane-deps.mjs: pass.npm run smoke: pass; end-to-end launcher run shows 45 structured log lines on stdout, 0 on stderr.Environment note found during verification
On this machine
~/quicklisp/local-projects/quasar-*.asdsymlinks point at an older checkout; quicklisp's local-projects scan can register that system before the repo scripts' ownsystems/directory wins. This is pre-existing (reproduces onmain), affects only local multi-checkout setups, and is worth a future hardening pass (e.g. registering the repo's ASDFs beforeql:quickloadin the launch scripts).One intermittent pre-existing failure was observed twice in
melissa-tests(pending-stop timing race,check (null success)); it reproduces on basemainand is unrelated to logging.Will open a PR referencing this issue; not merging without approval.
Implementation status
Branch
feat/durable-loggingimplements the design described in this issue on top of log4cl.What landed
control-plane/src/logging.lisp— newquasar.logpackage:log-eventstructured API, text layout (historical[quasar] ISO8601Z LEVEL subsystem event key=valueformat), JSON-lines layout for the durable file sink,apply-configwith fail-closed validation,flush-logs/shutdown-logging.quasar.configgains*log-sink*(:stdoutdefault),*log-file-path*(nil →$XDG_DATA_HOME/quasar/logs/quasar.log),*log-level*(nil → env precedence),*log-file-format*(:json),*log-immediate-flush*(t).quasar.app:mainapplies logging config right after init-file load;quasar.app:stopflushes and closes the sink;app start/start-failed/stoplifecycle events added.formatwrites (control-plane dispatch, subscriber failure, websocket server) now route throughquasar.logwith structured events;diagnostic-logremains as a compatibility alias.quasar-control.asd+ all three launcher scripts);check-control-plane-deps.mjsguards it.control-plane/tests/logging-tests.lisp— 14 focused tests (stdout default, file override, level filtering, structured fields text+JSON, concurrency line-completeness, restart persistence, flush/shutdown, sink failure containment, invalid config fail-closed, event vocabulary compatibility).scripts/bench-logging/npm run bench:logging.Reference numbers (20,000 events/scenario, SBCL 2.6.6): stdout text 4.4 µs/event ~544 B; file JSON immediate-flush 6.7 µs/event ~1244 B; file JSON interval flush 4.5 µs/event ~860 B; level-filtered event 0.07 µs/event ~96 B.
Test results
quasar-tests, all subsystem test runners): pass, 0 failures.node scripts/check-control-plane-deps.mjs: pass.npm run smoke: pass; end-to-end launcher run shows 45 structured log lines on stdout, 0 on stderr.Environment note found during verification
On this machine
~/quicklisp/local-projects/quasar-*.asdsymlinks point at an older checkout; quicklisp's local-projects scan can register that system before the repo scripts' ownsystems/directory wins. This is pre-existing (reproduces onmain), affects only local multi-checkout setups, and is worth a future hardening pass (e.g. registering the repo's ASDFs beforeql:quickloadin the launch scripts).One intermittent pre-existing failure was observed twice in
melissa-tests(pending-stop timing race,check (null success)); it reproduces on basemainand is unrelated to logging.Will open a PR referencing this issue; not merging without approval.