Durable logging subsystem on log4cl: stdout default, init-file sinks, structured events #77

Closed
opened 2026-09-08 07:37:21 +00:00 by nsaspy · 2 comments
Owner

Summary

Quasar needs one coherent logging facility built on log4cl instead of the current mix of diagnostic-log, ad-hoc format to 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.lisp defines diagnostic-log, the de-facto logging API. It writes to *error-output* (stderr), always finish-outputs, with format [quasar] ISO8601Z LEVEL subsystem event key=value ... where fields are printed with ~S.
  • Levels: parse-log-level/log-level-rank (:debug :info :warn :error :off), configured from env var QUASAR_LOG_LEVEL only (debug by default, info when CI is set). The init-file config system (quasar.config) has no logging surface at all.
  • Structured event vocabulary already exists and must be preserved: 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.
  • Scattered, unstructured writes bypass diagnostic-log:
    • control-plane.lisp:69 and control-plane.lisp:451 and actors/melissa/async-control-plane.lisp:62 print [control-plane] subscriber failed/unexpected error with no timestamp or level.
    • websocket-server.lisp has its own log-websocket printing [websocket] ... to stderr with no timestamp or level.
  • No log files anywhere; no flush policy concept; no crash/fatal handling; shutdown does not flush any sink.
  • debug-logging.lisp is loaded last in systems/quasar-control.asd so its dispatch-message wraps the final async implementation; it also carries workspace diagnostic-context helpers used by commit-operations.

Problems / gaps

  1. Default sink is stderr, so operational stdout expectations (systemd/container StandardOutput) do not hold; there is no way to select a sink.
  2. No durable option: an abrupt kill can lose everything only because nothing is persisted, and there is no append-only file mode, rotation, or clean shutdown flush.
  3. Logging config lives only in env vars; the executable init file (quasar.config, documented in docs/CONFIGURATION.md) cannot express logging at all.
  4. Three unrelated output formats/streams make filtering and ingestion unreliable.
  5. diagnostic-log evaluates 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.
  6. No tests cover sink selection, file persistence, concurrency, or failure handling.

Proposed architecture (log4cl-based)

Layered so the four concerns stay separate:

  1. API/event representation — quasar.log package: log-event level subsystem event &rest fields plus convenience log-debug/log-info/log-warn/log-error/log-fatal. Internally calls log4cl (log:log macros) under a dedicated logger category. diagnostic-log remains as a deprecated alias for log-event so existing call sites (workspace diagnostics, dispatch instrumentation) keep working unchanged.
  2. Formatting/encoding — a log4cl pattern layout rendering ISO8601Z LEVEL subsystem event key=value... (human text for stdout/stderr). A quasar.json-lines-layout renders one JSON object per line for the file sink so durable records are machine-parseable and non-interleaved (single write-sequence/write-string per record).
  3. Sink — log4cl appenders selected by config: :stdout (console appender on *standard-output*, default), :stderr, :file (log4cl file-appender, opens :if-exists :append), :off. Daily rotation via log4cl daily-file-appender is possible later without API change.
  4. Persistence/flush policy — durable file sink sets log4cl :immediate-flush t (finish-output per record); stdout keeps log4cl's default flush behavior; quasar.log:flush-logs / quasar.log:shutdown-logging flush and close, called from quasar.app:stop so 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 to quasar.config:

;; defaults
(setf *log-sink* :stdout           ; :stdout | :stderr | :file | :off
      *log-file-path* nil          ; required when *log-sink* is :file; nil => $XDG_DATA_HOME/quasar/logs/quasar.log
      *log-level* nil              ; nil => env QUASAR_LOG_LEVEL => CI=>info => debug (unchanged precedence)
      *log-file-format* :json      ; :json | :text for the file sink
      *log-immediate-flush* t)     ; flush policy for the durable sink

Invalid values (unknown sink, unknown level, unknown format, :file without a path) fail closed at startup, like the rest of init-file handling: quasar.log:apply-config signals a clear error during main, aborting startup rather than silently falling back.

Stdout-default semantics

  • With no init-file logging statements, logs go to stdout: npm run dev keeps 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.
  • Quasar must not create log files merely because it started; files appear only when *log-sink* is :file.

Durability semantics

  • File sink is append-only (:if-exists :append), so restarts preserve prior logs; directories are created on demand.
  • :immediate-flush t by default: each record is flushed to the OS before the logging call returns, so an abrupt SIGKILL loses at most the record being written. flush-interval can relax this for throughput.
  • Every append serializes on log4cl's appender lock: no interleaved/corrupt lines even with concurrent actors; JSON-lines layout emits one complete record per write.
  • Logging failures must not recurse: sink stream errors are caught, reported once on stderr, and the sink deactivates (log4cl temp-appender semantics) 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.
  • Rotation: log4cl daily-file-appender is 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 new app fatal / app shutdown / app start lifecycle 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"} with null when absent.

Testing strategy

New control-plane/tests/logging-tests.lisp (registered in quasar-tests.asd) using the existing quasar.tests check framework, plus a capture-appender harness:

  • default config => stdout sink; init-file-style override => file sink
  • level filtering (env and config), unknown level fails closed
  • structured fields rendered (text + JSON)
  • concurrent writes from multiple threads produce N complete, un-interleaved records
  • error logging (condition details) and fatal event
  • flush/close semantics; restart/persistence across two logging sessions on one file
  • invalid configuration => startup error with clear message
  • sink failure => logged-to-nowhere degrades without crashing caller
  • existing event vocabulary compatibility (dispatch/workspace events unchanged)
  • a small benchmark harness timing N events per sink and reporting per-event microseconds, so allocation/lock regressions are visible (npm run bench:logging or an ASDF-performable script)

Full real suites must pass: npm run test:lisp (SBCL, quasar-tests) and frontend suites; npm run smoke for startup behavior.

Acceptance criteria

  • Default run logs only to stdout; no files created without explicit config.
  • Init file can select stderr/file/off, level, file path, file format; invalid config aborts startup with a clear error.
  • Durable file sink: append-only across restarts, line-complete records under concurrency, immediate-flush default, clean flush/close on shutdown.
  • Sink failure never crashes Quasar; at most a one-time stderr notice.
  • control-plane command.received, control-plane command.ok, workspace operation.begin, workspace operation.applied and the rest of the event vocabulary unchanged in output.
  • Scattered format writes in control-plane/websocket-server route through the new facility.
  • log4cl added to quasar-control deps, test-lisp/run-production quickload lists, and dependency-check script contract.
  • docs/CONFIGURATION.md documents sinks, levels, defaults, file locations, and systemd/container recommendations.
  • npm run test:lisp and frontend tests pass; benchmark numbers recorded in the PR.

Migration / backward compatibility

  • diagnostic-log stays as an alias; debug-logging.lisp remains the last-loaded instrumentation file; call sites keep their current shape.
  • stderr is no longer the default sink: log scrapers tailing stderr must switch to stdout or configure :stderr. stdout content changes format slightly for previously-unstructured lines (they gain timestamps/levels), which is the point of the issue.
  • QUASAR_LOG_LEVEL precedence and values keep working.
  • No client-protocol, store, or actor-behavior changes; pure observability layer.
## Summary Quasar needs one coherent logging facility built on **log4cl** instead of the current mix of `diagnostic-log`, ad-hoc `format` to 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.lisp` defines `diagnostic-log`, the de-facto logging API. It writes to `*error-output*` (stderr), always `finish-output`s, with format `[quasar] ISO8601Z LEVEL subsystem event key=value ...` where fields are printed with `~S`. - Levels: `parse-log-level`/`log-level-rank` (`:debug :info :warn :error :off`), configured from env var `QUASAR_LOG_LEVEL` only (`debug` by default, `info` when `CI` is set). The init-file config system (`quasar.config`) has no logging surface at all. - Structured event vocabulary already exists and must be preserved: `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`. - Scattered, unstructured writes bypass `diagnostic-log`: - `control-plane.lisp:69` and `control-plane.lisp:451` and `actors/melissa/async-control-plane.lisp:62` print `[control-plane] subscriber failed/unexpected error` with no timestamp or level. - `websocket-server.lisp` has its own `log-websocket` printing `[websocket] ...` to stderr with no timestamp or level. - No log files anywhere; no flush policy concept; no crash/fatal handling; shutdown does not flush any sink. - `debug-logging.lisp` is loaded last in `systems/quasar-control.asd` so its `dispatch-message` wraps the final async implementation; it also carries workspace diagnostic-context helpers used by `commit-operations`. ## Problems / gaps 1. Default sink is stderr, so operational stdout expectations (systemd/container `StandardOutput`) do not hold; there is no way to select a sink. 2. No durable option: an abrupt kill can lose everything only because nothing is persisted, and there is no append-only file mode, rotation, or clean shutdown flush. 3. Logging config lives only in env vars; the executable init file (`quasar.config`, documented in `docs/CONFIGURATION.md`) cannot express logging at all. 4. Three unrelated output formats/streams make filtering and ingestion unreliable. 5. `diagnostic-log` evaluates 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. 6. No tests cover sink selection, file persistence, concurrency, or failure handling. ## Proposed architecture (log4cl-based) Layered so the four concerns stay separate: 1. **API/event representation** — `quasar.log` package: `log-event level subsystem event &rest fields` plus convenience `log-debug/log-info/log-warn/log-error/log-fatal`. Internally calls log4cl (`log:log` macros) under a dedicated logger category. `diagnostic-log` remains as a deprecated alias for `log-event` so existing call sites (workspace diagnostics, dispatch instrumentation) keep working unchanged. 2. **Formatting/encoding** — a log4cl pattern layout rendering `ISO8601Z LEVEL subsystem event key=value...` (human text for stdout/stderr). A `quasar.json-lines-layout` renders one JSON object per line for the file sink so durable records are machine-parseable and non-interleaved (single `write-sequence`/`write-string` per record). 3. **Sink** — log4cl appenders selected by config: `:stdout` (console appender on `*standard-output*`, default), `:stderr`, `:file` (log4cl `file-appender`, opens `:if-exists :append`), `:off`. Daily rotation via log4cl `daily-file-appender` is possible later without API change. 4. **Persistence/flush policy** — durable file sink sets log4cl `:immediate-flush t` (finish-output per record); stdout keeps log4cl's default flush behavior; `quasar.log:flush-logs` / `quasar.log:shutdown-logging` flush and close, called from `quasar.app:stop` so SIGINT/SIGTERM unwinds cleanly. Serialization: log4cl stream appenders are `serialized-appender`s (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 to `quasar.config`: ```lisp ;; defaults (setf *log-sink* :stdout ; :stdout | :stderr | :file | :off *log-file-path* nil ; required when *log-sink* is :file; nil => $XDG_DATA_HOME/quasar/logs/quasar.log *log-level* nil ; nil => env QUASAR_LOG_LEVEL => CI=>info => debug (unchanged precedence) *log-file-format* :json ; :json | :text for the file sink *log-immediate-flush* t) ; flush policy for the durable sink ``` Invalid values (unknown sink, unknown level, unknown format, `:file` without a path) fail closed at startup, like the rest of init-file handling: `quasar.log:apply-config` signals a clear error during `main`, aborting startup rather than silently falling back. ## Stdout-default semantics - With no init-file logging statements, logs go to **stdout**: `npm run dev` keeps 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. - Quasar must not create log files merely because it started; files appear only when `*log-sink*` is `:file`. ## Durability semantics - File sink is append-only (`:if-exists :append`), so restarts preserve prior logs; directories are created on demand. - `:immediate-flush t` by default: each record is flushed to the OS before the logging call returns, so an abrupt `SIGKILL` loses at most the record being written. `flush-interval` can relax this for throughput. - Every append serializes on log4cl's appender lock: no interleaved/corrupt lines even with concurrent actors; JSON-lines layout emits one complete record per write. - Logging failures must not recurse: sink stream errors are caught, reported once on stderr, and the sink deactivates (log4cl `temp-appender` semantics) 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. - Rotation: log4cl `daily-file-appender` is 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 new `app fatal` / `app shutdown` / `app start` lifecycle 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"}` with `null` when absent. ## Testing strategy New `control-plane/tests/logging-tests.lisp` (registered in `quasar-tests.asd`) using the existing `quasar.tests` `check` framework, plus a capture-appender harness: - default config => stdout sink; init-file-style override => file sink - level filtering (env and config), unknown level fails closed - structured fields rendered (text + JSON) - concurrent writes from multiple threads produce N complete, un-interleaved records - error logging (condition details) and fatal event - flush/close semantics; restart/persistence across two logging sessions on one file - invalid configuration => startup error with clear message - sink failure => logged-to-nowhere degrades without crashing caller - existing event vocabulary compatibility (dispatch/workspace events unchanged) - a small benchmark harness timing N events per sink and reporting per-event microseconds, so allocation/lock regressions are visible (`npm run bench:logging` or an ASDF-performable script) Full real suites must pass: `npm run test:lisp` (SBCL, quasar-tests) and frontend suites; `npm run smoke` for startup behavior. ## Acceptance criteria - [ ] Default run logs only to stdout; no files created without explicit config. - [ ] Init file can select stderr/file/off, level, file path, file format; invalid config aborts startup with a clear error. - [ ] Durable file sink: append-only across restarts, line-complete records under concurrency, immediate-flush default, clean flush/close on shutdown. - [ ] Sink failure never crashes Quasar; at most a one-time stderr notice. - [ ] `control-plane command.received`, `control-plane command.ok`, `workspace operation.begin`, `workspace operation.applied` and the rest of the event vocabulary unchanged in output. - [ ] Scattered `format` writes in control-plane/websocket-server route through the new facility. - [ ] log4cl added to `quasar-control` deps, test-lisp/run-production quickload lists, and dependency-check script contract. - [ ] `docs/CONFIGURATION.md` documents sinks, levels, defaults, file locations, and systemd/container recommendations. - [ ] `npm run test:lisp` and frontend tests pass; benchmark numbers recorded in the PR. ## Migration / backward compatibility - `diagnostic-log` stays as an alias; `debug-logging.lisp` remains the last-loaded instrumentation file; call sites keep their current shape. - stderr is no longer the default sink: log scrapers tailing stderr must switch to stdout or configure `:stderr`. stdout content changes format slightly for previously-unstructured lines (they gain timestamps/levels), which is the point of the issue. - `QUASAR_LOG_LEVEL` precedence and values keep working. - No client-protocol, store, or actor-behavior changes; pure observability layer.
Author
Owner

Implementation status

Branch feat/durable-logging implements the design described in this issue on top of log4cl.

What landed

  • control-plane/src/logging.lisp — new quasar.log package: log-event structured API, text layout (historical [quasar] ISO8601Z LEVEL subsystem event key=value format), JSON-lines layout for the durable file sink, apply-config with fail-closed validation, flush-logs/shutdown-logging.
  • quasar.config gains *log-sink* (:stdout default), *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:main applies logging config right after init-file load; quasar.app:stop flushes and closes the sink; app start/start-failed/stop lifecycle events added.
  • All scattered stderr format writes (control-plane dispatch, subscriber failure, websocket server) now route through quasar.log with structured events; diagnostic-log remains as a compatibility alias.
  • log4cl added to the dependency contract (quasar-control.asd + all three launcher scripts); check-control-plane-deps.mjs guards it.
  • Tests: 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).
  • Benchmark: 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

  • Worktree-verified full Lisp suite (quasar-tests, all subsystem test runners): pass, 0 failures.
  • Focused logging tests: 0 failures.
  • node scripts/check-control-plane-deps.mjs: pass.
  • Frontend: unit 73 files / 360 tests pass; integration 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-*.asd symlinks point at an older checkout; quicklisp's local-projects scan can register that system before the repo scripts' own systems/ directory wins. This is pre-existing (reproduces on main), affects only local multi-checkout setups, and is worth a future hardening pass (e.g. registering the repo's ASDFs before ql:quickload in the launch scripts).

One intermittent pre-existing failure was observed twice in melissa-tests (pending-stop timing race, check (null success)); it reproduces on base main and is unrelated to logging.

Will open a PR referencing this issue; not merging without approval.

## Implementation status Branch `feat/durable-logging` implements the design described in this issue on top of log4cl. ### What landed - `control-plane/src/logging.lisp` — new `quasar.log` package: `log-event` structured API, text layout (historical `[quasar] ISO8601Z LEVEL subsystem event key=value` format), JSON-lines layout for the durable file sink, `apply-config` with fail-closed validation, `flush-logs`/`shutdown-logging`. - `quasar.config` gains `*log-sink*` (`:stdout` default), `*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:main` applies logging config right after init-file load; `quasar.app:stop` flushes and closes the sink; `app start/start-failed/stop` lifecycle events added. - All scattered stderr `format` writes (control-plane dispatch, subscriber failure, websocket server) now route through `quasar.log` with structured events; `diagnostic-log` remains as a compatibility alias. - log4cl added to the dependency contract (`quasar-control.asd` + all three launcher scripts); `check-control-plane-deps.mjs` guards it. - Tests: `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). - Benchmark: `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 - Worktree-verified full Lisp suite (`quasar-tests`, all subsystem test runners): pass, 0 failures. - Focused logging tests: 0 failures. - `node scripts/check-control-plane-deps.mjs`: pass. - Frontend: unit 73 files / 360 tests pass; integration 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-*.asd` symlinks point at an older checkout; quicklisp's local-projects scan can register that system before the repo scripts' own `systems/` directory wins. This is pre-existing (reproduces on `main`), affects only local multi-checkout setups, and is worth a future hardening pass (e.g. registering the repo's ASDFs before `ql:quickload` in the launch scripts). One intermittent pre-existing failure was observed twice in `melissa-tests` (pending-stop timing race, `check (null success)`); it reproduces on base `main` and is unrelated to logging. Will open a PR referencing this issue; not merging without approval.
Author
Owner

Implementation status

Branch feat/durable-logging implements the design described in this issue on top of log4cl.

What landed

  • control-plane/src/logging.lisp — new quasar.log package: log-event structured API, text layout (historical [quasar] ISO8601Z LEVEL subsystem event key=value format), JSON-lines layout for the durable file sink, apply-config with fail-closed validation, flush-logs/shutdown-logging.
  • quasar.config gains *log-sink* (:stdout default), *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:main applies logging config right after init-file load; quasar.app:stop flushes and closes the sink; app start/start-failed/stop lifecycle events added.
  • All scattered stderr format writes (control-plane dispatch, subscriber failure, websocket server) now route through quasar.log with structured events; diagnostic-log remains as a compatibility alias.
  • log4cl added to the dependency contract (quasar-control.asd + all three launcher scripts); check-control-plane-deps.mjs guards it.
  • Tests: 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).
  • Benchmark: 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

  • Worktree-verified full Lisp suite (quasar-tests, all subsystem test runners): pass, 0 failures.
  • Focused logging tests: 0 failures.
  • node scripts/check-control-plane-deps.mjs: pass.
  • Frontend: unit 73 files / 360 tests pass; integration 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-*.asd symlinks point at an older checkout; quicklisp's local-projects scan can register that system before the repo scripts' own systems/ directory wins. This is pre-existing (reproduces on main), affects only local multi-checkout setups, and is worth a future hardening pass (e.g. registering the repo's ASDFs before ql:quickload in the launch scripts).

One intermittent pre-existing failure was observed twice in melissa-tests (pending-stop timing race, check (null success)); it reproduces on base main and is unrelated to logging.

Will open a PR referencing this issue; not merging without approval.

## Implementation status Branch `feat/durable-logging` implements the design described in this issue on top of log4cl. ### What landed - `control-plane/src/logging.lisp` — new `quasar.log` package: `log-event` structured API, text layout (historical `[quasar] ISO8601Z LEVEL subsystem event key=value` format), JSON-lines layout for the durable file sink, `apply-config` with fail-closed validation, `flush-logs`/`shutdown-logging`. - `quasar.config` gains `*log-sink*` (`:stdout` default), `*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:main` applies logging config right after init-file load; `quasar.app:stop` flushes and closes the sink; `app start/start-failed/stop` lifecycle events added. - All scattered stderr `format` writes (control-plane dispatch, subscriber failure, websocket server) now route through `quasar.log` with structured events; `diagnostic-log` remains as a compatibility alias. - log4cl added to the dependency contract (`quasar-control.asd` + all three launcher scripts); `check-control-plane-deps.mjs` guards it. - Tests: `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). - Benchmark: `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 - Worktree-verified full Lisp suite (`quasar-tests`, all subsystem test runners): pass, 0 failures. - Focused logging tests: 0 failures. - `node scripts/check-control-plane-deps.mjs`: pass. - Frontend: unit 73 files / 360 tests pass; integration 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-*.asd` symlinks point at an older checkout; quicklisp's local-projects scan can register that system before the repo scripts' own `systems/` directory wins. This is pre-existing (reproduces on `main`), affects only local multi-checkout setups, and is worth a future hardening pass (e.g. registering the repo's ASDFs before `ql:quickload` in the launch scripts). One intermittent pre-existing failure was observed twice in `melissa-tests` (pending-stop timing race, `check (null success)`); it reproduces on base `main` and is unrelated to logging. Will open a PR referencing this issue; not merging without approval.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
nsaspy/quasar#77
No description provided.