diff --git a/observability/otel/config.ts b/observability/otel/config.ts index 161921563..0c34f5752 100644 --- a/observability/otel/config.ts +++ b/observability/otel/config.ts @@ -57,12 +57,14 @@ const loggerName = "deco-logger"; export const OTEL_IS_ENABLED: boolean = Deno.env.has( "OTEL_EXPORTER_OTLP_ENDPOINT", ); + export const logger: Logger = new Logger(loggerName, "INFO", { handlers: [ ...OTEL_IS_ENABLED ? [ new OpenTelemetryHandler("INFO", { resourceAttributes: resource.attributes, + additionalExporters: parseExtraOTLPExporters(), }), ] : [new log.ConsoleHandler("INFO")], @@ -151,3 +153,57 @@ export const tracer = opentelemetry.trace.getTracer( export const tracerIsRecording = () => opentelemetry.trace.getActiveSpan()?.isRecording() ?? false; + +interface ExtraOTLPExporterConfig { + endpoint: string; + headers?: Record; +} + +/** + * Parses the OTEL_EXPORTER_EXTRA_OTLP_HANDLER environment variable + * to create additional OTLP exporter configurations for fan-out. + * + * @returns Array of OTLPExporterNodeConfigBase for additional exporters + */ +function parseExtraOTLPExporters() { + const OTEL_EXPORTER_EXTRA_OTLP_HANDLER = Deno.env.get( + "OTEL_EXPORTER_EXTRA_OTLP_HANDLER", + ); + if (!OTEL_EXPORTER_EXTRA_OTLP_HANDLER) { + return []; + } + + try { + const configs: ExtraOTLPExporterConfig[] = JSON.parse( + OTEL_EXPORTER_EXTRA_OTLP_HANDLER, + ); + + if (!Array.isArray(configs)) { + console.error( + "OTEL_EXPORTER_EXTRA_OTLP_HANDLER must be a JSON array", + ); + return []; + } + + return configs + .filter((config) => { + if (!config.endpoint) { + console.error( + "Each OTEL_EXPORTER_EXTRA_OTLP_HANDLER entry must have an 'endpoint' property", + ); + return false; + } + return true; + }) + .map((config) => ({ + url: config.endpoint, + headers: config.headers || {}, + })); + } catch (error) { + console.error( + "Failed to parse OTEL_EXPORTER_EXTRA_OTLP_HANDLER:", + error, + ); + return []; + } +} diff --git a/observability/otel/logger.ts b/observability/otel/logger.ts index cd69db5e4..c10ea188d 100644 --- a/observability/otel/logger.ts +++ b/observability/otel/logger.ts @@ -60,11 +60,12 @@ interface HandlerOptions extends log.BaseHandlerOptions { processorConfig?: BufferConfig; resourceAttributes?: Attributes; detectResources?: boolean; + additionalExporters?: OTLPExporterNodeConfigBase[]; } export class OpenTelemetryHandler extends log.BaseHandler { protected _logger: Logger | undefined; - protected _processor: BatchLogRecordProcessor | undefined; + protected _processors: BatchLogRecordProcessor[] = []; #unloadCallback = (() => { this.destroy(); @@ -84,23 +85,39 @@ export class OpenTelemetryHandler extends log.BaseHandler { ], }); - const exporter = options.exporterProtocol === "console" + const loggerProvider = new LoggerProvider({ + resource: detectedResource.merge( + new Resource({ ...options.resourceAttributes }), + ), + }); + + // Add the main exporter processor + const mainExporter = options.exporterProtocol === "console" ? new ConsoleLogRecordExporter() : new OTLPLogExporter(options.httpExporterOptions); - const processor = new BatchLogRecordProcessor( + const mainProcessor = new BatchLogRecordProcessor( // @ts-ignore: no idea why this is failing, but it should work - exporter, + mainExporter, options.processorConfig, ); - this._processor = processor; + this._processors.push(mainProcessor); + loggerProvider.addLogRecordProcessor(mainProcessor); + + // Add additional exporter processors for fan-out + if (options.additionalExporters && options.additionalExporters.length > 0) { + options.additionalExporters.forEach((exporterConfig) => { + const additionalExporter = new OTLPLogExporter(exporterConfig); + const additionalProcessor = new BatchLogRecordProcessor( + // @ts-ignore: no idea why this is failing, but it should work + additionalExporter, + options.processorConfig, + ); + this._processors.push(additionalProcessor); + loggerProvider.addLogRecordProcessor(additionalProcessor); + }); + } - const loggerProvider = new LoggerProvider({ - resource: detectedResource.merge( - new Resource({ ...options.resourceAttributes }), - ), - }); - loggerProvider.addLogRecordProcessor(processor); logs.setGlobalLoggerProvider(loggerProvider); const logger = logs.getLogger("deno-logger"); this._logger = logger; @@ -150,7 +167,8 @@ export class OpenTelemetryHandler extends log.BaseHandler { override destroy() { this.flush(); - this._processor?.shutdown(); + // Shutdown all processors + this._processors.forEach((processor) => processor.shutdown()); removeEventListener("unload", this.#unloadCallback); } }