4.4 KiB
4.4 KiB
Worker log → footer auto-route + Log Visualizer overlay
Date: 2026-06-23 Status: approved (design forks resolved with user)
Goal
- Auto-route all Worker WARN/ERROR Serilog events to the footer status strip (today only ~10 hand-curated business events reach it).
- Make the footer log line clickable → opens a Log Visualizer overlay showing the last 30 min of logs at all levels, color-coded.
- Dedupe/rate-limit the footer so repeating warnings (e.g. the current 60s OIDC-discovery failure) don't strobe.
Decisions (locked)
- Overlay source: Worker-side in-memory ring buffer (30-min window, all levels), fetched via a hub call. No log-file parsing.
- Levels: overlay shows INF/WRN/ERR; footer flashes WARN/ERROR only.
- Footer noise: per-message dedupe within a rate-limit window (suppress the footer broadcast for an identical message seen recently; the event is still buffered for the overlay).
Architecture
Worker
LogRingBuffer(singleton,Logging/): thread-safe, time-bounded (TimeSpanwindow, default 30 min) + hard cap (e.g. 5000) ring ofWorkerLogRecord(Message, Level, TimestampUtc). Evicts on append by age + cap.Snapshot()returns newest-last.BroadcastLogSink : Serilog.Core.ILogEventSink(Logging/): for everyLogEvent—- map level: Verbose/Debug/Information→
Info, Warning→Warn, Error/Fatal→Error; - render
msg = evt.RenderMessage()(+": {ex.GetType().Name}: {ex.Message}"first-line ifevt.Exception != null); - append to
LogRingBuffer(all levels); - if
Warn|Errorand not rate-limited: fire-and-forgetHubBroadcaster.WorkerLog(msg, level, evt.Timestamp.UtcDateTime). - Loop guard: wrap the broadcast in try/catch and swallow; skip broadcasting events whose
SourceContextis SignalR/connections plumbing (still buffered). Broadcasting must never itself log. - Dedupe/rate-limit: dict
message → lastBroadcastUtc; suppress footer broadcast ifnow - last < RateLimitWindow(const, 120 s). Periodic prune of the dict.
- map level: Verbose/Debug/Information→
- DI wiring (chicken-egg):
LogRingBuffer+BroadcastLogSinkare created as locals inProgram.csbeforebuilder.Build(), captured intoUseSerilog(... .WriteTo.Sink(broadcastSink)), and registered as singletons.HubBroadcasterdoesn't exist until post-build, so the sink starts detached; afterbuilder.Build()we callbroadcastSink.Attach(app.Services.GetRequiredService<HubBroadcaster>()). Buffering works from process start; broadcasting begins once attached. - Hub:
WorkerHub.GetRecentLogs() -> IReadOnlyList<WorkerLogRecordDto>readsLogRingBuffer.Snapshot(). (Read-only, no auth beyond existing hub.)
UI
- IWorkerClient / WorkerClient: add
Task<IReadOnlyList<WorkerLogEntry>> GetRecentLogsAsync(CancellationToken ct = default). ⚠ Update hand-rolled fakes in both test projects (StubWorkerClient + Worker.Tests UiVm fake). - Footer: wrap the worker-log
TextBlockso it's clickable (Button, transparent) →IslandsShellViewModel.OpenLogVisualizerCommand. ExistingOnWorkerLogReceivedalready routes the (now more numerous)WorkerLogbroadcasts to the strip — no change needed for footer routing itself. LogVisualizerViewModel(Modals/): on open,GetRecentLogsAsync()→ObservableCollection<LogLineViewModel>(msg, level→brush, HH:mm:ss). A level filter (All / Warn+Err) and a Refresh command. MVP = snapshot on open + Refresh; live-tail is a later nicety.LogVisualizerView(Modals/):ModalShell-based dialog (consistent with other modals), shown viaIDialogService.ShowLogVisualizerAsync(vm). Small, scrollable, monospaced, color-coded lines.- Localization: new
vm.logVisualizer(+ any view keys) in en.json + de.json (parity test enforces).
Out of scope / follow-ups
- Live-tail while the overlay is open (snapshot + Refresh for MVP).
- The OIDC-discovery-every-60s failure is a separate bug (Online Inbox enabled,
auth.kuns.devSSL fails). Dedupe tames the footer symptom; the root cause is tracked separately.
Tests
- Worker:
LogRingBufferTests(age + cap eviction, snapshot order),BroadcastLogSinkTests(level mapping; all levels buffered; only Warn/Err broadcast; dedupe suppresses repeat broadcast within window but still buffers; exception rendering; loop-guard source filter). - UI:
LogVisualizerViewModelTests(loads from worker, populates, filter). Footer-click wiring smoke.