From 79ca96cd661010a3be10db419554fae00658e466 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 12 Jul 2026 16:22:02 +0200 Subject: [PATCH] refactor(nostr): centralize metrics state (#105408) --- extensions/nostr/src/metrics.ts | 189 ++++++++++++-------------------- 1 file changed, 70 insertions(+), 119 deletions(-) diff --git a/extensions/nostr/src/metrics.ts b/extensions/nostr/src/metrics.ts index 7f243d133189..8134061bef88 100644 --- a/extensions/nostr/src/metrics.ts +++ b/extensions/nostr/src/metrics.ts @@ -150,51 +150,60 @@ export interface NostrMetrics { reset: () => void; } +type MetricsState = Omit & { + relays: Map; +}; + +function createZeroMetricsState(): MetricsState { + return { + eventsReceived: 0, + eventsProcessed: 0, + eventsDuplicate: 0, + eventsRejected: { + invalidShape: 0, + wrongKind: 0, + stale: 0, + future: 0, + rateLimited: 0, + invalidSignature: 0, + oversizedCiphertext: 0, + oversizedPlaintext: 0, + decryptFailed: 0, + selfMessage: 0, + }, + relays: new Map(), + rateLimiting: { perSenderHits: 0, globalHits: 0 }, + decrypt: { success: 0, failure: 0 }, + memory: { seenTrackerSize: 0, rateLimiterEntries: 0 }, + }; +} + +function createMetricsSnapshot(state: MetricsState, snapshotAt?: number): MetricsSnapshot { + const relays: MetricsSnapshot["relays"] = {}; + for (const [url, stats] of state.relays) { + relays[url] = { ...stats, messagesReceived: { ...stats.messagesReceived } }; + } + + return { + ...state, + eventsRejected: { ...state.eventsRejected }, + relays, + rateLimiting: { ...state.rateLimiting }, + decrypt: { ...state.decrypt }, + memory: { ...state.memory }, + snapshotAt: snapshotAt ?? Date.now(), + }; +} + /** * Create a metrics collector instance. * Optionally pass an onMetric callback to receive real-time metric events. */ export function createMetrics(onMetric?: OnMetricCallback): NostrMetrics { - // Counters - let eventsReceived = 0; - let eventsProcessed = 0; - let eventsDuplicate = 0; - const eventsRejected = { - invalidShape: 0, - wrongKind: 0, - stale: 0, - future: 0, - rateLimited: 0, - invalidSignature: 0, - oversizedCiphertext: 0, - oversizedPlaintext: 0, - decryptFailed: 0, - selfMessage: 0, - }; - - // Per-relay stats - const relays = new Map(); - - // Rate limiting stats - const rateLimiting = { - perSenderHits: 0, - globalHits: 0, - }; - - // Decrypt stats - const decrypt = { - success: 0, - failure: 0, - }; - - // Memory stats (updated via gauge-style metrics) - const memory = { - seenTrackerSize: 0, - rateLimiterEntries: 0, - }; + let state = createZeroMetricsState(); function getOrCreateRelay(url: string) { - let relay = relays.get(url); + let relay = state.relays.get(url); if (!relay) { relay = { connects: 0, @@ -213,7 +222,7 @@ export function createMetrics(onMetric?: OnMetricCallback): NostrMetrics { circuitBreakerOpens: 0, circuitBreakerCloses: 0, }; - relays.set(url, relay); + state.relays.set(url, relay); } return relay; } @@ -235,43 +244,43 @@ export function createMetrics(onMetric?: OnMetricCallback): NostrMetrics { switch (name) { // Event metrics case "event.received": - eventsReceived += value; + state.eventsReceived += value; break; case "event.processed": - eventsProcessed += value; + state.eventsProcessed += value; break; case "event.duplicate": - eventsDuplicate += value; + state.eventsDuplicate += value; break; case "event.rejected.invalid_shape": - eventsRejected.invalidShape += value; + state.eventsRejected.invalidShape += value; break; case "event.rejected.wrong_kind": - eventsRejected.wrongKind += value; + state.eventsRejected.wrongKind += value; break; case "event.rejected.stale": - eventsRejected.stale += value; + state.eventsRejected.stale += value; break; case "event.rejected.future": - eventsRejected.future += value; + state.eventsRejected.future += value; break; case "event.rejected.rate_limited": - eventsRejected.rateLimited += value; + state.eventsRejected.rateLimited += value; break; case "event.rejected.invalid_signature": - eventsRejected.invalidSignature += value; + state.eventsRejected.invalidSignature += value; break; case "event.rejected.oversized_ciphertext": - eventsRejected.oversizedCiphertext += value; + state.eventsRejected.oversizedCiphertext += value; break; case "event.rejected.oversized_plaintext": - eventsRejected.oversizedPlaintext += value; + state.eventsRejected.oversizedPlaintext += value; break; case "event.rejected.decrypt_failed": - eventsRejected.decryptFailed += value; + state.eventsRejected.decryptFailed += value; break; case "event.rejected.self_message": - eventsRejected.selfMessage += value; + state.eventsRejected.selfMessage += value; break; // Relay metrics @@ -347,73 +356,36 @@ export function createMetrics(onMetric?: OnMetricCallback): NostrMetrics { // Rate limiting case "rate_limit.per_sender": - rateLimiting.perSenderHits += value; + state.rateLimiting.perSenderHits += value; break; case "rate_limit.global": - rateLimiting.globalHits += value; + state.rateLimiting.globalHits += value; break; // Decrypt case "decrypt.success": - decrypt.success += value; + state.decrypt.success += value; break; case "decrypt.failure": - decrypt.failure += value; + state.decrypt.failure += value; break; // Memory (gauge-style - value replaces, not adds) case "memory.seen_tracker_size": - memory.seenTrackerSize = value; + state.memory.seenTrackerSize = value; break; case "memory.rate_limiter_entries": - memory.rateLimiterEntries = value; + state.memory.rateLimiterEntries = value; break; } } function getSnapshot(): MetricsSnapshot { - // Convert relay map to object - const relaysObj: MetricsSnapshot["relays"] = {}; - for (const [url, stats] of relays) { - relaysObj[url] = { ...stats, messagesReceived: { ...stats.messagesReceived } }; - } - - return { - eventsReceived, - eventsProcessed, - eventsDuplicate, - eventsRejected: { ...eventsRejected }, - relays: relaysObj, - rateLimiting: { ...rateLimiting }, - decrypt: { ...decrypt }, - memory: { ...memory }, - snapshotAt: Date.now(), - }; + return createMetricsSnapshot(state); } function reset(): void { - eventsReceived = 0; - eventsProcessed = 0; - eventsDuplicate = 0; - Object.assign(eventsRejected, { - invalidShape: 0, - wrongKind: 0, - stale: 0, - future: 0, - rateLimited: 0, - invalidSignature: 0, - oversizedCiphertext: 0, - oversizedPlaintext: 0, - decryptFailed: 0, - selfMessage: 0, - }); - relays.clear(); - rateLimiting.perSenderHits = 0; - rateLimiting.globalHits = 0; - decrypt.success = 0; - decrypt.failure = 0; - memory.seenTrackerSize = 0; - memory.rateLimiterEntries = 0; + state = createZeroMetricsState(); } return { emit, getSnapshot, reset }; @@ -423,28 +395,7 @@ export function createMetrics(onMetric?: OnMetricCallback): NostrMetrics { * Create a no-op metrics instance (for when metrics are disabled). */ export function createNoopMetrics(): NostrMetrics { - const emptySnapshot: MetricsSnapshot = { - eventsReceived: 0, - eventsProcessed: 0, - eventsDuplicate: 0, - eventsRejected: { - invalidShape: 0, - wrongKind: 0, - stale: 0, - future: 0, - rateLimited: 0, - invalidSignature: 0, - oversizedCiphertext: 0, - oversizedPlaintext: 0, - decryptFailed: 0, - selfMessage: 0, - }, - relays: {}, - rateLimiting: { perSenderHits: 0, globalHits: 0 }, - decrypt: { success: 0, failure: 0 }, - memory: { seenTrackerSize: 0, rateLimiterEntries: 0 }, - snapshotAt: 0, - }; + const emptySnapshot = createMetricsSnapshot(createZeroMetricsState(), 0); return { emit: () => {},