feat: add structured backend logging
CI / Static checks (push) Successful in 37s
CI / Build and push image (push) Skipped

Collector failures, service-API errors, WS connection lifecycle, and
config reload/discovery events were previously invisible outside the
browser (or, for a few config/discovery cases, logged with an
inconsistent ad-hoc console.error). Add a small scoped logger
(lib/logger.ts, level via LOG_LEVEL) and wire it through server.ts,
the scheduler, service collector, config loader/effective store, and
the WS server so operators can see failures via `docker logs`.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 09:39:30 +02:00
co-authored by Claude Sonnet 5
parent 94ffa9e930
commit 7727ca8e97
7 changed files with 159 additions and 18 deletions
+40
View File
@@ -7,6 +7,9 @@ import { collectSystem } from "./system";
import { collectHttp } from "./http";
import { collectService } from "./service";
import type { WidgetResult } from "@/lib/types/widget-result";
import { createLogger } from "@/lib/logger";
const log = createLogger("scheduler");
const httpLimit = pLimit(8);
@@ -122,6 +125,12 @@ class CollectorScheduler {
if (existing) clearInterval(existing.timer);
log.debug(existing ? "job replaced" : "job added", {
widgetId: instance.id,
type: instance.widget.type,
intervalMs,
});
const run = () => this.runJob(instance.id);
const timer = setInterval(run, intervalMs);
this.jobs.set(instance.id, { widget: instance.widget, intervalMs, timer, consecutiveFailures: 0 });
@@ -130,16 +139,20 @@ class CollectorScheduler {
for (const [id, job] of this.jobs) {
if (!seen.has(id)) {
log.debug("job removed", { widgetId: id, type: job.widget.type });
clearInterval(job.timer);
this.jobs.delete(id);
this.lastResults.delete(id);
}
}
log.info("reconciled", { jobs: this.jobs.size });
}
stop(): void {
for (const job of this.jobs.values()) clearInterval(job.timer);
this.jobs.clear();
log.info("scheduler stopped");
}
private async runJob(id: string): Promise<void> {
@@ -150,10 +163,37 @@ class CollectorScheduler {
try {
result = await collect(job.widget);
if (result.type === "http") {
const wasFailing = job.consecutiveFailures > 0;
job.consecutiveFailures = result.data.up ? 0 : job.consecutiveFailures + 1;
result = { type: "http", data: { ...result.data, consecutiveFailures: job.consecutiveFailures } };
if (!result.data.up) {
log.warn("http check failed", {
widgetId: id,
url: (job.widget as { url?: string }).url,
statusCode: result.data.statusCode,
attempt: job.consecutiveFailures,
});
} else if (wasFailing) {
log.info("http check recovered", { widgetId: id, url: (job.widget as { url?: string }).url });
}
} else {
if (job.consecutiveFailures > 0) {
log.info("collector recovered", {
widgetId: id,
type: job.widget.type,
afterFailures: job.consecutiveFailures,
});
}
job.consecutiveFailures = 0;
}
} catch (err) {
job.consecutiveFailures += 1;
log.error("collector failed", {
widgetId: id,
type: job.widget.type,
attempt: job.consecutiveFailures,
error: (err as Error).message,
});
result = { type: "error", message: (err as Error).message };
}