feat(host,web): host log ring + GET /api/v1/logs + console Logs page

Remote debugging without shell access: a tracing layer tees every
event at DEBUG-and-up — independent of the RUST_LOG filter gating
stderr/host.log, so console-side debugging never needs a restart —
into a bounded in-memory ring (log_capture.rs, 4096 newest entries,
OnceLock singleton like config()), installed at both init sites
(stderr path in main, the Windows service file path). The mgmt API
serves it cursor-paged at GET /api/v1/logs?after=&limit= — bearer-only
and deliberately NOT on the mTLS cert allowlist (log lines can name
client identities and host paths). The web console grows a Logs page
(follow/pause · min-level filter · text search · eviction-gap badge);
polling self-paces: a non-empty page advances the after-cursor (new
query key → immediate refetch, drains backlogs), an empty page idles
at the 2s interval. OpenAPI regenerated; ring pagination/eviction,
layer wiring, and the authed route are unit-tested; Storybook story
included.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 16:33:16 +00:00
parent 7ced80c4e3
commit 8af1a15aa6
13 changed files with 816 additions and 12 deletions
+22 -7
View File
@@ -130,23 +130,38 @@ fn host_log_path() -> PathBuf {
/// Initialise tracing to the service log file (the SCM gives the service no console/stderr). Falls
/// back to stderr if the file can't be opened. Called from `main()` only for `service run`.
/// Also tees into the in-memory log ring (`log_capture`), like the stderr path in `main()` — the
/// supervisor serves no mgmt API itself, but the layer is harmless and keeps both inits uniform.
pub fn init_file_logging(filter: tracing_subscriber::EnvFilter) {
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::Layer;
let ring =
crate::log_capture::RingLayer.with_filter(tracing_subscriber::filter::LevelFilter::DEBUG);
match std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(service_log_path())
{
Ok(file) => {
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_ansi(false)
.with_writer(move || file.try_clone().expect("clone service log handle"))
tracing_subscriber::registry()
.with(ring)
.with(
tracing_subscriber::fmt::layer()
.with_ansi(false)
.with_writer(move || file.try_clone().expect("clone service log handle"))
.with_filter(filter),
)
.init();
}
Err(_) => {
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
tracing_subscriber::registry()
.with(ring)
.with(
tracing_subscriber::fmt::layer()
.with_writer(std::io::stderr)
.with_filter(filter),
)
.init();
}
}