UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

414 lines 13.4 kB
/** * Exporter Registry * Manages multiple observability exporters with circuit breaker protection */ import { logger } from "../utils/logger.js"; import { AlwaysSampler } from "./sampling/samplers.js"; /** Default timeout for exporter API calls (30 seconds) */ const DEFAULT_EXPORT_TIMEOUT_MS = 30_000; /** * Wrap a promise with a timeout. Rejects with a descriptive error if the * promise does not settle within `timeoutMs` milliseconds. */ function withExportTimeout(promise, timeoutMs, label) { return new Promise((resolve, reject) => { const timer = setTimeout(() => { reject(new Error(`Export to '${label}' timed out after ${timeoutMs}ms`)); }, timeoutMs); promise.then((v) => { clearTimeout(timer); resolve(v); }, (e) => { clearTimeout(timer); reject(e); }); }); } /** * Registry for managing multiple observability exporters * Includes circuit breaker protection to prevent cascading failures */ export class ExporterRegistry { exporters = new Map(); defaultExporter = null; sampler = new AlwaysSampler(); circuitBreakers = new Map(); circuitBreakerConfig = { failureThreshold: 5, resetTimeout: 30000, // 30 seconds }; /** * Register an exporter */ register(exporter) { this.exporters.set(exporter.getName(), exporter); } /** * Unregister an exporter */ unregister(name) { return this.exporters.delete(name); } /** * Get an exporter by name */ get(name) { return this.exporters.get(name); } /** * Get all registered exporter names */ getNames() { return Array.from(this.exporters.keys()); } /** * Get total exporter count */ getCount() { return this.exporters.size; } /** * Set the default exporter */ setDefault(name) { if (!this.exporters.has(name)) { throw new Error(`Exporter '${name}' not registered`); } this.defaultExporter = name; } /** * Get the default exporter */ getDefault() { if (!this.defaultExporter) { return undefined; } return this.exporters.get(this.defaultExporter); } /** * Set the sampler for the registry */ setSampler(sampler) { this.sampler = sampler; } /** * Get the current sampler */ getSampler() { return this.sampler; } /** * Configure the circuit breaker settings * @param config - Partial circuit breaker configuration */ configureCircuitBreaker(config) { Object.assign(this.circuitBreakerConfig, config); } /** * Check if circuit is open for an exporter * @param exporterName - Name of the exporter * @returns true if circuit is open (exporter should be skipped) */ isCircuitOpen(exporterName) { const breaker = this.circuitBreakers.get(exporterName); if (!breaker) { return false; } if (breaker.state === "open") { // Check if we should try half-open if (Date.now() - breaker.lastFailure > this.circuitBreakerConfig.resetTimeout) { breaker.state = "half-open"; logger.info(`[ExporterRegistry] Circuit half-open for ${exporterName}, attempting recovery`); return false; } return true; } return false; } /** * Record a failure for an exporter's circuit breaker * @param exporterName - Name of the exporter */ recordFailure(exporterName) { let breaker = this.circuitBreakers.get(exporterName); if (!breaker) { breaker = { failures: 0, lastFailure: 0, state: "closed" }; this.circuitBreakers.set(exporterName, breaker); } breaker.failures++; breaker.lastFailure = Date.now(); if (breaker.failures >= this.circuitBreakerConfig.failureThreshold) { breaker.state = "open"; logger.warn(`[ExporterRegistry] Circuit opened for ${exporterName} after ${breaker.failures} failures`); } } /** * Record a success for an exporter's circuit breaker * Resets the circuit to closed state * @param exporterName - Name of the exporter */ recordSuccess(exporterName) { const breaker = this.circuitBreakers.get(exporterName); if (breaker) { if (breaker.state === "half-open") { logger.info(`[ExporterRegistry] Circuit closed for ${exporterName} after successful recovery`); } breaker.failures = 0; breaker.state = "closed"; } } /** * Get circuit breaker status for an exporter * @param exporterName - Name of the exporter * @returns Circuit breaker state or undefined if not tracked */ getCircuitBreakerStatus(exporterName) { return this.circuitBreakers.get(exporterName); } /** * Reset circuit breaker for an exporter * @param exporterName - Name of the exporter */ resetCircuitBreaker(exporterName) { this.circuitBreakers.delete(exporterName); } /** * Export span to all registered exporters * Applies sampling and circuit breaker protection before export */ async exportToAll(span) { const results = new Map(); // Apply sampling if (!this.sampler.shouldSample(span)) { // Return empty results if not sampled return results; } const exportPromises = Array.from(this.exporters.entries()).map(async ([name, exporter]) => { // Check circuit breaker before attempting export if (this.isCircuitOpen(name)) { results.set(name, { success: false, exportedCount: 0, failedCount: 1, errors: [ { spanId: span.spanId, error: "Circuit breaker open - exporter temporarily disabled", retryable: true, }, ], durationMs: 0, }); return; } if (exporter.isInitialized()) { try { const result = await withExportTimeout(exporter.exportSpan(span), DEFAULT_EXPORT_TIMEOUT_MS, name); results.set(name, result); // Record success or failure based on result if (result.success) { this.recordSuccess(name); } else { this.recordFailure(name); } } catch (error) { this.recordFailure(name); results.set(name, { success: false, exportedCount: 0, failedCount: 1, errors: [ { spanId: span.spanId, error: error instanceof Error ? error.message : String(error), retryable: true, }, ], durationMs: 0, }); } } else { logger.debug(`[ExporterRegistry] Skipping uninitialized exporter '${name}' for span ${span.spanId}`); } }); await Promise.all(exportPromises); return results; } /** * Export span to a specific exporter * Applies sampling and circuit breaker protection */ async exportTo(name, span) { const exporter = this.exporters.get(name); if (!exporter) { return null; } if (!exporter.isInitialized()) { logger.debug(`[ExporterRegistry] Skipping uninitialized exporter '${name}' for span ${span.spanId}`); return null; } // Check circuit breaker before attempting export if (this.isCircuitOpen(name)) { return { success: false, exportedCount: 0, failedCount: 1, errors: [ { spanId: span.spanId, error: "Circuit breaker open - exporter temporarily disabled", retryable: true, }, ], durationMs: 0, }; } // Apply sampling if (!this.sampler.shouldSample(span)) { return { success: true, exportedCount: 0, failedCount: 0, durationMs: 0, }; } try { const result = await withExportTimeout(exporter.exportSpan(span), DEFAULT_EXPORT_TIMEOUT_MS, name); if (result.success) { this.recordSuccess(name); } else { this.recordFailure(name); } return result; } catch (error) { this.recordFailure(name); return { success: false, exportedCount: 0, failedCount: 1, errors: [ { spanId: span.spanId, error: error instanceof Error ? error.message : String(error), retryable: true, }, ], durationMs: 0, }; } } /** * Initialize all exporters */ async initializeAll() { const results = await Promise.allSettled(Array.from(this.exporters.entries()).map(([name, e]) => e.initialize().catch((err) => { logger.error(`[ExporterRegistry] Failed to initialize exporter '${name}':`, err); throw err; }))); for (const result of results) { if (result.status === "rejected") { logger.warn(`[ExporterRegistry] One or more exporters failed to initialize`); break; } } } /** * Shutdown all exporters */ async shutdownAll() { const results = await Promise.allSettled(Array.from(this.exporters.entries()).map(([name, e]) => e.shutdown().catch((err) => { logger.error(`[ExporterRegistry] Failed to shutdown exporter '${name}':`, err); throw err; }))); for (const result of results) { if (result.status === "rejected") { logger.warn(`[ExporterRegistry] One or more exporters failed to shutdown`); break; } } } /** * Flush all exporters */ async flushAll() { const results = await Promise.allSettled(Array.from(this.exporters.entries()).map(([name, e]) => e.flush().catch((err) => { logger.error(`[ExporterRegistry] Failed to flush exporter '${name}':`, err); throw err; }))); for (const result of results) { if (result.status === "rejected") { logger.warn(`[ExporterRegistry] One or more exporters failed to flush`); break; } } } /** * Get health status of all exporters */ async healthCheckAll() { const results = new Map(); const healthPromises = Array.from(this.exporters.entries()).map(async ([name, exporter]) => { const status = await exporter.healthCheck(); results.set(name, status); }); await Promise.all(healthPromises); return results; } /** * Check if all exporters are healthy */ async isHealthy() { const statuses = await this.healthCheckAll(); return Array.from(statuses.values()).every((s) => s.healthy); } /** * Get total pending spans across all exporters */ getTotalPendingSpans() { let total = 0; const exporterArray = Array.from(this.exporters.values()); for (const exporter of exporterArray) { total += exporter.getPendingCount(); } return total; } /** * Clear all registered exporters and reset state * (For testing and cleanup) */ clear() { this.exporters.clear(); this.defaultExporter = null; this.circuitBreakers.clear(); this.sampler = new AlwaysSampler(); } } /** * Global exporter registry instance (singleton pattern from main) */ let globalRegistry = null; /** * Get the global exporter registry instance */ export function getExporterRegistry() { if (!globalRegistry) { globalRegistry = new ExporterRegistry(); } return globalRegistry; } /** * Reset the global exporter registry (for testing) */ export function resetExporterRegistry() { if (globalRegistry) { globalRegistry.clear(); } globalRegistry = null; } //# sourceMappingURL=exporterRegistry.js.map