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] 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;