UNPKG

nostr-websocket-utils

Version:

Robust WebSocket utilities for Nostr applications with automatic reconnection, supporting both ESM and CommonJS. Features channel-based messaging, heartbeat monitoring, message queueing, and comprehensive error handling with type-safe handlers.

212 lines 7.87 kB
/** * @file Metrics tracking for Nostr WebSocket connections * @module metrics */ import { EventEmitter } from 'events'; import { getLogger } from './logger.js'; const logger = getLogger('RelayMetrics'); export class RelayMetricsTracker extends EventEmitter { constructor() { super(); this.metrics = new Map(); this.startTime = Date.now(); // Periodically calculate scores setInterval(() => this.updateScores(), 60000); // Every minute } /** * Get metrics for a specific relay */ getRelayMetrics(relayUrl) { if (!this.metrics.has(relayUrl)) { this.initializeMetrics(relayUrl); } return this.metrics.get(relayUrl); } /** * Get metrics for all relays */ getAllMetrics() { return this.metrics; } /** * Initialize metrics for a new relay */ initializeMetrics(relayUrl) { this.metrics.set(relayUrl, { totalConnections: 0, activeConnections: 0, connectionErrors: 0, messagesReceived: 0, messagesSent: 0, bytesReceived: 0, bytesSent: 0, averageLatency: 0, maxLatency: 0, minLatency: Infinity, totalErrors: 0, eventsReceived: 0, eventsSent: 0, subscriptions: 0, uptime: 0, reliability: 1, lastSeen: Date.now(), score: 100 }); } /** * Update connection metrics */ trackConnection(relayUrl, connected) { const metrics = this.getRelayMetrics(relayUrl); if (connected) { metrics.totalConnections++; metrics.activeConnections++; metrics.lastSeen = Date.now(); } else { metrics.activeConnections = Math.max(0, metrics.activeConnections - 1); } this.emit('metrics.update', relayUrl, metrics); } /** * Track message metrics */ trackMessage(relayUrl, sent, bytes) { const metrics = this.getRelayMetrics(relayUrl); if (sent) { metrics.messagesSent++; metrics.bytesSent += bytes; } else { metrics.messagesReceived++; metrics.bytesReceived += bytes; } metrics.lastSeen = Date.now(); this.emit('metrics.update', relayUrl, metrics); } /** * Track latency */ trackLatency(relayUrl, latencyMs) { const metrics = this.getRelayMetrics(relayUrl); metrics.maxLatency = Math.max(metrics.maxLatency, latencyMs); metrics.minLatency = Math.min(metrics.minLatency, latencyMs); // Exponential moving average for smoothing metrics.averageLatency = metrics.averageLatency * 0.9 + latencyMs * 0.1; this.emit('metrics.update', relayUrl, metrics); } /** * Track errors */ trackError(relayUrl, error) { const metrics = this.getRelayMetrics(relayUrl); metrics.totalErrors++; metrics.lastError = error.message; metrics.connectionErrors++; this.emit('metrics.update', relayUrl, metrics); } /** * Track protocol-specific events */ trackProtocolEvent(relayUrl, type, sent) { const metrics = this.getRelayMetrics(relayUrl); if (type === 'event') { if (sent) { metrics.eventsSent++; } else { metrics.eventsReceived++; } } else if (type === 'subscription') { metrics.subscriptions += sent ? 1 : -1; metrics.subscriptions = Math.max(0, metrics.subscriptions); } this.emit('metrics.update', relayUrl, metrics); } /** * Calculate relay score based on metrics */ calculateScore(metrics) { const now = Date.now(); // Calculate weighted scores for different aspects const latencyScore = Math.max(0, 100 - (metrics.averageLatency / 10)); // Lower is better const reliabilityScore = metrics.reliability * 100; const uptimeScore = Math.min(100, (metrics.uptime / (60 * 60 * 24)) * 100); // Score based on 24h uptime const errorScore = Math.max(0, 100 - (metrics.connectionErrors * 5)); // Each error reduces score by 5 const activityScore = metrics.lastSeen ? Math.max(0, 100 - ((now - metrics.lastSeen) / (60 * 1000))) : 0; // Activity in last hour // Weighted average return Math.round((latencyScore * 0.2) + (reliabilityScore * 0.3) + (uptimeScore * 0.2) + (errorScore * 0.2) + (activityScore * 0.1)); } /** * Update scores for all relays */ updateScores() { const now = Date.now(); for (const [url, metrics] of this.metrics.entries()) { // Update uptime metrics.uptime = (now - this.startTime) / 1000; // Update reliability based on successful vs total connections metrics.reliability = metrics.totalConnections > 0 ? 1 - (metrics.connectionErrors / metrics.totalConnections) : 1; // Calculate composite score metrics.score = this.calculateScore(metrics); this.emit('metrics.score', url, metrics.score); logger.info({ url, score: metrics.score }, 'Updated relay score'); } } /** * Get high-value relays (score > threshold) */ getHighValueRelays(threshold = 70) { return Array.from(this.metrics.entries()) .filter(([_, metrics]) => metrics.score >= threshold) .map(([url]) => url); } /** * Export metrics in Prometheus format */ getPrometheusMetrics() { const lines = []; // Helper to format metric line const formatMetric = (name, value, labels = {}) => { const labelStr = Object.entries(labels) .map(([k, v]) => `${k}="${v}"`) .join(','); lines.push(`nostr_${name}${labelStr ? `{${labelStr}}` : ''} ${value}`); }; for (const [url, metrics] of this.metrics.entries()) { const labels = { relay: url }; // Connection metrics formatMetric('connections_total', metrics.totalConnections, labels); formatMetric('connections_active', metrics.activeConnections, labels); formatMetric('connection_errors_total', metrics.connectionErrors, labels); // Message metrics formatMetric('messages_received_total', metrics.messagesReceived, labels); formatMetric('messages_sent_total', metrics.messagesSent, labels); formatMetric('bytes_received_total', metrics.bytesReceived, labels); formatMetric('bytes_sent_total', metrics.bytesSent, labels); // Performance metrics formatMetric('latency_average', metrics.averageLatency, labels); formatMetric('latency_max', metrics.maxLatency, labels); formatMetric('latency_min', metrics.minLatency, labels); // Protocol metrics formatMetric('events_received_total', metrics.eventsReceived, labels); formatMetric('events_sent_total', metrics.eventsSent, labels); formatMetric('subscriptions_active', metrics.subscriptions, labels); // Scoring metrics formatMetric('uptime_seconds', metrics.uptime, labels); formatMetric('reliability_score', metrics.reliability, labels); formatMetric('composite_score', metrics.score, labels); } return lines.join('\n') + '\n'; } } // Export singleton instance export const metricsTracker = new RelayMetricsTracker(); //# sourceMappingURL=metrics.js.map