refactor(nostr): centralize metrics state (#105408)

This commit is contained in:
Vincent Koc
2026-07-12 16:22:02 +02:00
committed by GitHub
parent 0eb9298f3a
commit 79ca96cd66
+70 -119
View File
@@ -150,51 +150,60 @@ export interface NostrMetrics {
reset: () => void;
}
type MetricsState = Omit<MetricsSnapshot, "relays" | "snapshotAt"> & {
relays: Map<string, RelayMetrics>;
};
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<string, RelayMetrics>();
// 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: () => {},