feat: show runtime diagnostics in management summary
This commit is contained in:
@@ -65,6 +65,11 @@ def create_handler(ctx: ManageContext) -> type[BaseHTTPRequestHandler]:
|
||||
limit = bounded_int(query.get("limit", ["200"])[0], 1, MAX_EVENT_LINES)
|
||||
self._send_json({"items": load_events(ctx, limit), "limit": limit})
|
||||
return
|
||||
if parsed.path == "/api/manage/diagnostics":
|
||||
query = parse_qs(parsed.query)
|
||||
limit = bounded_int(query.get("limit", ["50"])[0], 1, MAX_EVENT_LINES)
|
||||
self._send_json({"items": load_diagnostics(ctx, limit), "limit": limit})
|
||||
return
|
||||
self.send_error(HTTPStatus.NOT_FOUND)
|
||||
|
||||
def do_PUT(self) -> None:
|
||||
@@ -234,6 +239,7 @@ def config_payload(ctx: ManageContext) -> dict[str, Any]:
|
||||
|
||||
def build_summary(ctx: ManageContext) -> dict[str, Any]:
|
||||
events = load_events(ctx, MAX_EVENT_LINES)
|
||||
diagnostics = load_diagnostics(ctx, MAX_EVENT_LINES)
|
||||
counts: dict[str, int] = {}
|
||||
last_event_time = ""
|
||||
latest_alert = ""
|
||||
@@ -261,22 +267,35 @@ def build_summary(ctx: ManageContext) -> dict[str, Any]:
|
||||
"violation_count": active_alert_count,
|
||||
"latest_alert_time": latest_alert,
|
||||
"events_path": str(event_sink_path(ctx)),
|
||||
"diagnostics_path": str(diagnostics_path(ctx)),
|
||||
"diagnostics_count": len(diagnostics),
|
||||
"latest_zone_counts": latest_zone_counts(diagnostics),
|
||||
"baseline_ready": latest_baseline_ready(diagnostics),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def load_events(ctx: ManageContext, limit: int) -> list[dict[str, Any]]:
|
||||
path = event_sink_path(ctx)
|
||||
return load_jsonl_tail(path, limit)
|
||||
|
||||
|
||||
def load_diagnostics(ctx: ManageContext, limit: int) -> list[dict[str, Any]]:
|
||||
path = diagnostics_path(ctx)
|
||||
return load_jsonl_tail(path, limit)
|
||||
|
||||
|
||||
def load_jsonl_tail(path: Path, limit: int) -> list[dict[str, Any]]:
|
||||
lines = tail_lines(path, limit)
|
||||
events: list[dict[str, Any]] = []
|
||||
items: list[dict[str, Any]] = []
|
||||
for line in lines:
|
||||
try:
|
||||
payload = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(payload, dict):
|
||||
events.append(payload)
|
||||
return events
|
||||
items.append(payload)
|
||||
return items
|
||||
|
||||
|
||||
def event_sink_path(ctx: ManageContext, data: dict[str, Any] | None = None) -> Path:
|
||||
@@ -289,6 +308,32 @@ def event_sink_path(ctx: ManageContext, data: dict[str, Any] | None = None) -> P
|
||||
return path.resolve()
|
||||
|
||||
|
||||
def diagnostics_path(ctx: ManageContext, data: dict[str, Any] | None = None) -> Path:
|
||||
if data is None:
|
||||
data = load_config_document(ctx.config_path)
|
||||
raw_path = str(data.get("runtime", {}).get("diagnostics_path", "logs/runtime_diagnostics.jsonl"))
|
||||
path = Path(raw_path).expanduser()
|
||||
if not path.is_absolute():
|
||||
path = ctx.project_root / path
|
||||
return path.resolve()
|
||||
|
||||
|
||||
def latest_zone_counts(diagnostics: list[dict[str, Any]]) -> dict[str, int]:
|
||||
for item in reversed(diagnostics):
|
||||
zone_counts = item.get("zone_counts")
|
||||
if isinstance(zone_counts, dict):
|
||||
return {str(key): int(value) for key, value in zone_counts.items()}
|
||||
return {}
|
||||
|
||||
|
||||
def latest_baseline_ready(diagnostics: list[dict[str, Any]]) -> bool:
|
||||
for item in reversed(diagnostics):
|
||||
diagnostics_payload = item.get("diagnostics")
|
||||
if isinstance(diagnostics_payload, dict):
|
||||
return bool(diagnostics_payload.get("baseline_ready", False))
|
||||
return False
|
||||
|
||||
|
||||
def tail_lines(path: Path, limit: int) -> list[str]:
|
||||
if not path.exists():
|
||||
return []
|
||||
|
||||
@@ -77,6 +77,37 @@ class ManageApiTests(unittest.TestCase):
|
||||
self.assertEqual(summary["metrics"]["event_count"], 2)
|
||||
self.assertEqual(summary["metrics"]["violation_count"], 1)
|
||||
|
||||
def test_summary_reads_runtime_diagnostics(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
root = Path(tmpdir)
|
||||
config_path = root / "config" / "local.toml"
|
||||
save_config_document(
|
||||
config_path,
|
||||
{
|
||||
"runtime": {"diagnostics_path": "logs/runtime_diagnostics.jsonl"},
|
||||
"event_sink": {"path": "logs/events.jsonl"},
|
||||
"layout": {"rows": 1, "cols": 1, "zone_ids": ["r1c1"]},
|
||||
},
|
||||
)
|
||||
diagnostics_path = root / "logs" / "runtime_diagnostics.jsonl"
|
||||
diagnostics_path.parent.mkdir()
|
||||
diagnostics_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"ts": "2026-04-28T10:00:00+08:00",
|
||||
"zone_counts": {"r1c1": 1},
|
||||
"diagnostics": {"baseline_ready": True},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
summary = build_summary(ManageContext(config_path=config_path, project_root=root))
|
||||
|
||||
self.assertEqual(summary["metrics"]["diagnostics_count"], 1)
|
||||
self.assertEqual(summary["metrics"]["latest_zone_counts"], {"r1c1": 1})
|
||||
self.assertTrue(summary["metrics"]["baseline_ready"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -484,10 +484,18 @@ function renderMetrics() {
|
||||
const cards = [
|
||||
["事件总数", metrics.event_count ?? 0],
|
||||
["违规事件", metrics.violation_count ?? 0],
|
||||
["诊断帧数", metrics.diagnostics_count ?? 0],
|
||||
["基线状态", metrics.baseline_ready ? "ready" : "learning"],
|
||||
["最新报警", metrics.latest_alert_time || "-"],
|
||||
["事件文件", metrics.events_path || "-"],
|
||||
];
|
||||
els.metrics.innerHTML = cards.map(([label, value]) => `<div class="metric"><span>${label}</span><strong>${value}</strong></div>`).join("");
|
||||
const zoneCounts = metrics.latest_zone_counts || {};
|
||||
const zoneSummary = Object.keys(zoneCounts).length
|
||||
? `<div class="metric wide"><span>最新区域状态</span><strong>${Object.entries(zoneCounts)
|
||||
.map(([zoneId, count]) => `${zoneId}:${count}`)
|
||||
.join(" ")}</strong></div>`
|
||||
: "";
|
||||
els.metrics.innerHTML = cards.map(([label, value]) => `<div class="metric"><span>${label}</span><strong>${value}</strong></div>`).join("") + zoneSummary;
|
||||
}
|
||||
|
||||
function renderEvents() {
|
||||
|
||||
@@ -226,6 +226,10 @@ canvas {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.metric.wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.events-table {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user