Skip to content

v2.20.0: Crash-safe logs everywhere, structured log web UI, and safer concurrent start/stop

Choose a tag to compare

@jdx jdx released this 02 Aug 18:00
· 60 commits to main since this release
Immutable release. Only release title and notes can be modified.
0a97663

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_output and on_output daemons hand their streams to the dedicated log-sink sidecar, 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 stop waits 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=value field 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_output daemons 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_output hooks are served by the sink (#668) — @jdx. The sink applies the hook's filter/regex and 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.

  • --json flag for supervisor status (#601) — @disintegrator. Emits {"status": "up"} / {"status": "down"} (exit code 0 for pipe-friendly use with jq), reports unknown with 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 (default true, also settable via PITCHFORK_SUPERVISOR_AUTO_START) prevents client commands from spawning an unmanaged supervisor that could win ownership over a systemd- or launchd-managed instance. Explicit supervisor start/run commands 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 both dir = "~/data" and user = "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), so pitchfork usage and 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, stop previously waited only for the group leader to die and returned while children (e.g. docker compose containers) 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, and groups were 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.