@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
JavaScript
/**
* 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