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
+15 -3
View File
@@ -45,6 +45,7 @@ mod install;
#[path = "windows/interactive.rs"]
mod interactive;
mod library;
mod log_capture;
mod mgmt;
mod mgmt_token;
mod native_pairing;
@@ -92,9 +93,20 @@ fn main() {
service::init_file_logging(filter);
} else {
// Logs go to stderr so stdout stays machine-readable (`punktfunk-host openapi > spec.json`).
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
// A second layer tees DEBUG-and-up into the in-memory ring served by GET /api/v1/logs —
// deliberately not gated by RUST_LOG, so console-side debugging never needs a restart.
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::Layer;
tracing_subscriber::registry()
.with(
log_capture::RingLayer.with_filter(tracing_subscriber::filter::LevelFilter::DEBUG),
)
.with(
tracing_subscriber::fmt::layer()
.with_writer(std::io::stderr)
.with_filter(filter),
)
.init();
}