Keystone of the observability program (OBS-1). Replaces the stderr-only tracing_subscriber::fmt::init() bootstrap in nunciod with a layered Registry subscriber. That old one-liner pinned the level at INFO, ignored RUST_LOG (the env-filter feature wasn't even enabled), had no file sink, no JSON, and no runtime control — the daemon was effectively unfollowable and unauditable.
New module crates/nunciod/src/logging.rs builds three layers:
Reloadable EnvFilter — precedence NUNCIO_LOG > RUST_LOG > default info. Wrapped in a reload::Layer so a future control RPC can change verbosity at runtime; the reload::Handle is returned from init (not wired to an RPC in this story, just exposed). A malformed filter string never panics — it degrades to the default and emits a warn! once the subscriber is live.
Rotating file layer — tracing_appender::rolling::daily into <data_dir>/logs/ (sibling of the SQLite DB, i.e. ~/.nuncio/logs/) through tracing_appender::non_blocking.
JSON toggle:NUNCIO_LOG_FORMAT=json switches the file sink to JSON. The console sink stays human-readable (it's for an operator watching a terminal); the file is the machine-parsed sink, so JSON lands there.
WorkerGuard lifetime:non_blocking returns a WorkerGuard that must be held for the whole process or buffered logs are dropped. init returns it inside LoggingGuards; main binds let _log_guards = ... so it lives until shutdown. Documented in the module.
Fail-safe: logging never crashes the daemon. Unwritable log dir → stderr-only + warn!. Subscriber already installed (e.g. test harness) → left untouched, empty guards returned.
Cargo
Workspace tracing-subscriber gains features env-filter, json (default fmt/ansi/registry retained); added tracing-appender to workspace deps. nunciod now consumes both via workspace = true (previously pinned tracing-subscriber = "0.3" locally — now consistent with the rest of the crate).
Tests
Layer/filter construction is split into pure functions so behavior is unit-tested without ever calling global init() twice:
NUNCIO_LOG beats RUST_LOG beats default (and empty var treated as unset).
Malformed filter falls back to default without panicking.
NUNCIO_LOG_FORMAT=json (case-insensitive) selects JSON; other values stay human.
Log dir resolves to a logs/ sibling of the DB file.
Gate green: cargo fmt --all -- --check, cargo check-all, cargo test-all — 546 passed, 0 failed, 0 ignored. No unsafe, no unwrap/expect/panic in non-test code.
## Summary
Keystone of the observability program (OBS-1). Replaces the stderr-only `tracing_subscriber::fmt::init()` bootstrap in `nunciod` with a layered `Registry` subscriber. That old one-liner pinned the level at INFO, ignored `RUST_LOG` (the `env-filter` feature wasn't even enabled), had no file sink, no JSON, and no runtime control — the daemon was effectively unfollowable and unauditable.
New module `crates/nunciod/src/logging.rs` builds three layers:
1. **Reloadable EnvFilter** — precedence `NUNCIO_LOG` > `RUST_LOG` > default `info`. Wrapped in a `reload::Layer` so a future control RPC can change verbosity at runtime; the `reload::Handle` is returned from `init` (not wired to an RPC in this story, just exposed). A malformed filter string never panics — it degrades to the default and emits a `warn!` once the subscriber is live.
2. **Console layer** — human-readable, stderr, ANSI, target + span context.
3. **Rotating file layer** — `tracing_appender::rolling::daily` into `<data_dir>/logs/` (sibling of the SQLite DB, i.e. `~/.nuncio/logs/`) through `tracing_appender::non_blocking`.
**JSON toggle:** `NUNCIO_LOG_FORMAT=json` switches the *file* sink to JSON. The console sink stays human-readable (it's for an operator watching a terminal); the file is the machine-parsed sink, so JSON lands there.
**WorkerGuard lifetime:** `non_blocking` returns a `WorkerGuard` that must be held for the whole process or buffered logs are dropped. `init` returns it inside `LoggingGuards`; `main` binds `let _log_guards = ...` so it lives until shutdown. Documented in the module.
**Fail-safe:** logging never crashes the daemon. Unwritable log dir → stderr-only + `warn!`. Subscriber already installed (e.g. test harness) → left untouched, empty guards returned.
## Cargo
- Workspace `tracing-subscriber` gains features `env-filter`, `json` (default `fmt`/`ansi`/`registry` retained); added `tracing-appender` to workspace deps. `nunciod` now consumes both via `workspace = true` (previously pinned `tracing-subscriber = "0.3"` locally — now consistent with the rest of the crate).
## Tests
Layer/filter construction is split into pure functions so behavior is unit-tested without ever calling global `init()` twice:
- `NUNCIO_LOG` beats `RUST_LOG` beats default (and empty var treated as unset).
- Malformed filter falls back to default without panicking.
- `NUNCIO_LOG_FORMAT=json` (case-insensitive) selects JSON; other values stay human.
- Log dir resolves to a `logs/` sibling of the DB file.
Gate green: `cargo fmt --all -- --check`, `cargo check-all`, `cargo test-all` — 546 passed, 0 failed, 0 ignored. No `unsafe`, no `unwrap`/`expect`/`panic` in non-test code.
Refs #357
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Keystone of the observability program (OBS-1). Replaces the stderr-only
tracing_subscriber::fmt::init()bootstrap innunciodwith a layeredRegistrysubscriber. That old one-liner pinned the level at INFO, ignoredRUST_LOG(theenv-filterfeature wasn't even enabled), had no file sink, no JSON, and no runtime control — the daemon was effectively unfollowable and unauditable.New module
crates/nunciod/src/logging.rsbuilds three layers:NUNCIO_LOG>RUST_LOG> defaultinfo. Wrapped in areload::Layerso a future control RPC can change verbosity at runtime; thereload::Handleis returned frominit(not wired to an RPC in this story, just exposed). A malformed filter string never panics — it degrades to the default and emits awarn!once the subscriber is live.tracing_appender::rolling::dailyinto<data_dir>/logs/(sibling of the SQLite DB, i.e.~/.nuncio/logs/) throughtracing_appender::non_blocking.JSON toggle:
NUNCIO_LOG_FORMAT=jsonswitches the file sink to JSON. The console sink stays human-readable (it's for an operator watching a terminal); the file is the machine-parsed sink, so JSON lands there.WorkerGuard lifetime:
non_blockingreturns aWorkerGuardthat must be held for the whole process or buffered logs are dropped.initreturns it insideLoggingGuards;mainbindslet _log_guards = ...so it lives until shutdown. Documented in the module.Fail-safe: logging never crashes the daemon. Unwritable log dir → stderr-only +
warn!. Subscriber already installed (e.g. test harness) → left untouched, empty guards returned.Cargo
tracing-subscribergains featuresenv-filter,json(defaultfmt/ansi/registryretained); addedtracing-appenderto workspace deps.nunciodnow consumes both viaworkspace = true(previously pinnedtracing-subscriber = "0.3"locally — now consistent with the rest of the crate).Tests
Layer/filter construction is split into pure functions so behavior is unit-tested without ever calling global
init()twice:NUNCIO_LOGbeatsRUST_LOGbeats default (and empty var treated as unset).NUNCIO_LOG_FORMAT=json(case-insensitive) selects JSON; other values stay human.logs/sibling of the DB file.Gate green:
cargo fmt --all -- --check,cargo check-all,cargo test-all— 546 passed, 0 failed, 0 ignored. Nounsafe, nounwrap/expect/panicin non-test code.Refs #357
🤖 Generated with Claude Code