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:
@@ -578,6 +578,62 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/logs": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"logs"
|
||||
],
|
||||
"summary": "Host logs",
|
||||
"description": "The host's recent log entries — an in-memory ring of the newest few thousand, captured at\nDEBUG and above regardless of `RUST_LOG`. Follow live by polling with `after` set to the last\nresponse's `next` cursor; a `dropped: true` means entries were evicted between polls (the ring\nwrapped). Bearer-only: logs can reference client identities and host paths, so this is part of\nthe loopback-only admin surface, never the LAN-readable mTLS one.",
|
||||
"operationId": "logsGet",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "after",
|
||||
"in": "query",
|
||||
"description": "Return entries with seq greater than this (omitted/0 = oldest retained)",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
"description": "Max entries per response (default and cap 1000)",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Entries after the cursor, oldest first",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/LogPage"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing or invalid bearer token",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/native/clients": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -2027,6 +2083,70 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"LogEntry": {
|
||||
"type": "object",
|
||||
"description": "One captured log event.",
|
||||
"required": [
|
||||
"seq",
|
||||
"ts_ms",
|
||||
"level",
|
||||
"target",
|
||||
"msg"
|
||||
],
|
||||
"properties": {
|
||||
"level": {
|
||||
"type": "string",
|
||||
"description": "`ERROR` | `WARN` | `INFO` | `DEBUG` | `TRACE`."
|
||||
},
|
||||
"msg": {
|
||||
"type": "string",
|
||||
"description": "The formatted message, structured fields appended as `key=value`."
|
||||
},
|
||||
"seq": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "Monotonic sequence number (1-based) — pass the last one back as the `after` cursor.",
|
||||
"minimum": 0
|
||||
},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "The emitting module path (tracing target)."
|
||||
},
|
||||
"ts_ms": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "Unix timestamp in milliseconds.",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"LogPage": {
|
||||
"type": "object",
|
||||
"description": "One poll's worth of log entries.",
|
||||
"required": [
|
||||
"entries",
|
||||
"next",
|
||||
"dropped"
|
||||
],
|
||||
"properties": {
|
||||
"dropped": {
|
||||
"type": "boolean",
|
||||
"description": "True when entries between `after` and the first returned one were already evicted."
|
||||
},
|
||||
"entries": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/LogEntry"
|
||||
}
|
||||
},
|
||||
"next": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "Cursor for the next poll (the last returned seq, or the request's `after` when empty).",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"NativeClient": {
|
||||
"type": "object",
|
||||
"description": "A paired native (punktfunk/1) client.",
|
||||
@@ -2571,6 +2691,10 @@
|
||||
{
|
||||
"name": "stats",
|
||||
"description": "Streaming performance-stats capture: arm/stop a recording, read the live + saved time-series for graphing"
|
||||
},
|
||||
{
|
||||
"name": "logs",
|
||||
"description": "Host log stream: the newest in-memory log entries, cursor-paged for live following"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user