From e083ee134d2be95d5a4a0c9b72e96c03f0806fa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Thu, 2 Apr 2026 17:02:15 +0100 Subject: [PATCH 1/3] Wire planner metrics gauges to actual session/client counts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit plannerActiveSessions and plannerConnectedClients were registered but never updated — they always reported 0. Now incremented on WebSocket connect and decremented on close. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/planner/app/lib/yjs-server.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/planner/app/lib/yjs-server.ts b/apps/planner/app/lib/yjs-server.ts index ccb22ba..af9e53f 100644 --- a/apps/planner/app/lib/yjs-server.ts +++ b/apps/planner/app/lib/yjs-server.ts @@ -6,6 +6,7 @@ import * as encoding from "lib0/encoding"; import * as decoding from "lib0/decoding"; import type { IncomingMessage, Server } from "node:http"; import { saveSessionState, loadSessionState, touchSession } from "./sessions.ts"; +import { plannerActiveSessions, plannerConnectedClients } from "./metrics.server.ts"; const messageSync = 0; const messageAwareness = 1; @@ -159,10 +160,13 @@ export function setupYjsWebSocket(server: Server): WebSocketServer { }); wss.on("connection", async (ws: WebSocket, _request: IncomingMessage, sessionId: string) => { + const isNewSession = !docs.has(sessionId); const doc = await getOrLoadDoc(sessionId); const awareness = getAwareness(sessionId, doc); conns.set(ws, { sessionId, clientIds: new Set() }); + plannerConnectedClients.inc(); + if (isNewSession) plannerActiveSessions.inc(); // Broadcast doc updates to all connections in this session const onUpdate = (update: Uint8Array, origin: unknown) => { @@ -218,11 +222,13 @@ export function setupYjsWebSocket(server: Server): WebSocketServer { ); } conns.delete(ws); + plannerConnectedClients.dec(); doc.off("update", onUpdate); // Save when last client leaves const hasClients = Array.from(conns.values()).some((m) => m.sessionId === sessionId); if (!hasClients) { + plannerActiveSessions.dec(); saveSessionState(sessionId).catch(() => {}); } }); From a3ad073d96833dde2a93e3818e7fb15d725c382a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Fri, 3 Apr 2026 09:15:15 +0100 Subject: [PATCH 2/3] Guard collectDefaultMetrics against duplicate registration Importing metrics.server.ts from yjs-server.ts caused collectDefaultMetrics() to re-register during HMR, crashing the planner dev server. Check if metrics already exist first. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/planner/app/lib/metrics.server.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/planner/app/lib/metrics.server.ts b/apps/planner/app/lib/metrics.server.ts index 85590c6..059e5ab 100644 --- a/apps/planner/app/lib/metrics.server.ts +++ b/apps/planner/app/lib/metrics.server.ts @@ -1,7 +1,10 @@ import client from "prom-client"; // Collect default Node.js metrics (event loop, heap, GC) -client.collectDefaultMetrics(); +// Guard against duplicate registration during HMR +if (!client.register.getSingleMetric("process_cpu_user_seconds_total")) { + client.collectDefaultMetrics(); +} export const httpRequestDuration = new client.Histogram({ name: "http_request_duration_seconds", From d97992e96798c49e14aa496f0d23af82b3a73d36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Fri, 3 Apr 2026 10:02:21 +0100 Subject: [PATCH 3/3] Guard all metric registrations against re-evaluation Vite's dev server can re-evaluate modules, causing prom-client "already registered" errors for all metrics, not just default ones. Use getOrCreate pattern to reuse existing metrics from the registry. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/planner/app/lib/metrics.server.ts | 54 ++++++++++++++++---------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/apps/planner/app/lib/metrics.server.ts b/apps/planner/app/lib/metrics.server.ts index 059e5ab..46d2060 100644 --- a/apps/planner/app/lib/metrics.server.ts +++ b/apps/planner/app/lib/metrics.server.ts @@ -1,32 +1,44 @@ import client from "prom-client"; -// Collect default Node.js metrics (event loop, heap, GC) -// Guard against duplicate registration during HMR +// Guard all metric registration — Vite's dev server can re-evaluate +// this module, causing "already registered" errors. +function getOrCreate(name: string, create: () => T): T { + return (client.register.getSingleMetric(name) as T) ?? create(); +} + if (!client.register.getSingleMetric("process_cpu_user_seconds_total")) { client.collectDefaultMetrics(); } -export const httpRequestDuration = new client.Histogram({ - name: "http_request_duration_seconds", - help: "Duration of HTTP requests in seconds", - labelNames: ["method", "route", "status"] as const, - buckets: [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5], -}); +export const httpRequestDuration = getOrCreate("http_request_duration_seconds", () => + new client.Histogram({ + name: "http_request_duration_seconds", + help: "Duration of HTTP requests in seconds", + labelNames: ["method", "route", "status"] as const, + buckets: [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5], + }), +); -export const plannerActiveSessions = new client.Gauge({ - name: "planner_active_sessions", - help: "Number of active planner sessions", -}); +export const plannerActiveSessions = getOrCreate("planner_active_sessions", () => + new client.Gauge({ + name: "planner_active_sessions", + help: "Number of active planner sessions", + }), +); -export const plannerConnectedClients = new client.Gauge({ - name: "planner_connected_clients", - help: "Number of connected WebSocket clients", -}); +export const plannerConnectedClients = getOrCreate("planner_connected_clients", () => + new client.Gauge({ + name: "planner_connected_clients", + help: "Number of connected WebSocket clients", + }), +); -export const brouterRequestDuration = new client.Histogram({ - name: "brouter_request_duration_seconds", - help: "Duration of BRouter API requests in seconds", - buckets: [0.1, 0.25, 0.5, 1, 2, 5, 10], -}); +export const brouterRequestDuration = getOrCreate("brouter_request_duration_seconds", () => + new client.Histogram({ + name: "brouter_request_duration_seconds", + help: "Duration of BRouter API requests in seconds", + buckets: [0.1, 0.25, 0.5, 1, 2, 5, 10], + }), +); export const registry = client.register;