v2.20.0: Crash-safe logs everywhere, structured log web UI, and safer concurrent start/stop
This release completes the out-of-process log capture story so ready_output and on_output daemons now survive a supervisor crash, brings structured log rendering with filters to the web UI, and fixes two production race conditions in concurrent daemon start/stop. It also adds several supervisor and config quality-of-life improvements around user-aware path expansion, JSON status output, and boot resiliency.
Highlights
- Log capture now survives a supervisor crash for every non-PTY daemon. Following the sink model introduced in 2.19.0, both
ready_outputandon_outputdaemons hand their streams to the dedicatedlog-sinksidecar, so killing the supervisor no longer breaks logging, readiness detection, or output hooks (#667, #668) — @jdx. - Safer daemon orchestration. Concurrent start/stop of many daemons is now deterministic, and
stopwaits for the whole process group to exit before reporting success (#606) — @disintegrator.
Added
-
Structured log display with filters in the web UI (#630) — @gaojunran. The web UI now renders logs like the CLI, with level badges, logger names, and
key=valuefield formatting. A new filter bar supports level, logger (populated from a new loggers endpoint), full-text search with regex and case-sensitivity toggles, jq expressions with autocomplete, and since/until time ranges. Log streams are served as newline-delimited JSON, and invalid regex or jq expressions surface as errors. -
ready_outputdaemons use the out-of-process sink (#667) — @jdx. The sink now matches the readiness pattern itself, persists and flushes the matching line, then reports it to the supervisor. Readiness,on_ready, active-port detection, and deadline logic are all unchanged, but log capture no longer dies with the supervisor. -
on_outputhooks are served by the sink (#668) — @jdx. The sink applies the hook'sfilter/regexand debounce and reports qualifying lines, so a daemon's hook keeps firing through a supervisor crash. Debouncing moves into the process that sees every line, cutting per-line IPC chatter. PTY daemons (pty = true) remain the only in-process, crash-vulnerable path. -
--jsonflag forsupervisor status(#601) — @disintegrator. Emits{"status": "up"}/{"status": "down"}(exit code 0 for pipe-friendly use withjq), reportsunknownwith an error when the process is alive but IPC fails, and includes the web UI's actual bound URL when the server is running.$ pitchfork supervisor status --json { "status": "up", "web_ui": "..." }
-
Disable client auto-start of the supervisor (#678) — @risu729. A new
settings.supervisor.auto_start(defaulttrue, also settable viaPITCHFORK_SUPERVISOR_AUTO_START) prevents client commands from spawning an unmanaged supervisor that could win ownership over a systemd- or launchd-managed instance. Explicitsupervisor start/runcommands still work, and disabled auto-start now yields actionable connection errors. -
Home-relative path expansion in config (#675) — @risu729. Leading
~/~/...components in daemon, slug, and namespace directory paths (and path-valued env overrides) are now expanded to the user's home instead of being treated as literal relative paths, using pitchfork's existing home resolution so elevated invocations retain the original user's home. -
~expands using the daemon's effective user (#708) — @gaojunran. When a daemon sets bothdir = "~/data"anduser = "postgres",~now resolves to that user's home (looked up by username or numeric UID), matching Unix semantics. Falls back to the supervisor's home when no user is configured. -
Auto-heal stale boot registration on startup (#707) — @gaojunran. When a package-manager upgrade changes the binary path, the launchd plist or systemd unit could still point at the old version and fail on next boot. The supervisor now detects a stale registered path and re-registers with the current binary, best-effort and off the startup critical path. Resolves #544.
-
Commands declare their effect on the system (#666) — @jdx. All 47 commands now carry usage
effect=metadata (read/write/destructive), sopitchfork usageand generated docs indicate whether a command inspects, changes, or destroys state. Daemon-launch commands whose effect is really the user's command are deliberately left unclassified.
Fixed
-
Deterministic concurrent start/stop and whole-group stop wait (#606) — @disintegrator. Fixes two production race conditions. Cold-starting many daemons at once could misattribute IPC responses (a "phantom" start failure aborting dependents) because the wire protocol lacks request IDs; each parallel start/stop task now uses its own dedicated IPC connection, making attribution structural rather than timing-dependent. Separately,
stoppreviously waited only for the group leader to die and returned while children (e.g.docker composecontainers) were still shutting down, letting a force-restart attach to a dying group; the wait now requires whole-group emptiness with the existing SIGTERM →stop_timeout→ SIGKILL escalation, plus per-daemon stop locks and background orphan cleanup so the wider stop window doesn't cause duplicate instances or delayed boot. -
Include registries in the config schema (#671) — @risu729.
slugs,namespaces, andgroupswere skipped from the generated JSON schema even though the parser and docs accept them, causing strict validators to reject valid global config. Editors using the published pitchfork schema now recognize these sections.
Full Changelog: v2.19.0...v2.20.0
💚 Sponsor pitchfork
pitchfork is maintained by @jdx, an open source developer for entire.io, the title sponsor of the jdx.dev open source tools including mise, aube, hk, and more. Development is sustained by sponsorships.
If pitchfork has a place in your dev workflow, please consider sponsoring at jdx.dev. Individual and company sponsorships are what keep the project healthy and moving forward.