Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
27 kB · 825 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826import { DiagLogLevel, diag, trace } from '@opentelemetry/api'import { logs } from '@opentelemetry/api-logs'// OTLP/Prometheus exporters are dynamically imported inside the protocol// switch statements below. A process uses at most one protocol variant per// signal, but static imports would load all 6 (~1.2MB) on every startup.import { envDetector, hostDetector, osDetector, resourceFromAttributes,} from '@opentelemetry/resources'import { BatchLogRecordProcessor, ConsoleLogRecordExporter, LoggerProvider,} from '@opentelemetry/sdk-logs'import { ConsoleMetricExporter, MeterProvider, PeriodicExportingMetricReader,} from '@opentelemetry/sdk-metrics'import { BasicTracerProvider, BatchSpanProcessor, ConsoleSpanExporter,} from '@opentelemetry/sdk-trace-base'import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, SEMRESATTRS_HOST_ARCH,} from '@opentelemetry/semantic-conventions'import { HttpsProxyAgent } from 'https-proxy-agent'import { getLoggerProvider, getMeterProvider, getTracerProvider, setEventLogger, setLoggerProvider, setMeterProvider, setTracerProvider,} from 'src/bootstrap/state.js'import { getOtelHeadersFromHelper, getSubscriptionType, is1PApiCustomer, isClaudeAISubscriber,} from 'src/utils/auth.js'import { getPlatform, getWslVersion } from 'src/utils/platform.js'
import { getCACertificates } from '../caCerts.js'import { registerCleanup } from '../cleanupRegistry.js'import { getHasFormattedOutput, logForDebugging } from '../debug.js'import { isEnvTruthy } from '../envUtils.js'import { errorMessage } from '../errors.js'import { getMTLSConfig } from '../mtls.js'import { getProxyUrl, shouldBypassProxy } from '../proxy.js'import { getSettings_DEPRECATED } from '../settings/settings.js'import { jsonStringify } from '../slowOperations.js'import { profileCheckpoint } from '../startupProfiler.js'import { isBetaTracingEnabled } from './betaSessionTracing.js'import { BigQueryMetricsExporter } from './bigqueryExporter.js'import { ClaudeCodeDiagLogger } from './logger.js'import { initializePerfettoTracing } from './perfettoTracing.js'import { endInteractionSpan, isEnhancedTelemetryEnabled,} from './sessionTracing.js'
const DEFAULT_METRICS_EXPORT_INTERVAL_MS = 60000const DEFAULT_LOGS_EXPORT_INTERVAL_MS = 5000const DEFAULT_TRACES_EXPORT_INTERVAL_MS = 5000
class TelemetryTimeoutError extends Error {}
function telemetryTimeout(ms: number, message: string): Promise<never> { return new Promise((_, reject) => { setTimeout( (rej: (e: Error) => void, msg: string) => rej(new TelemetryTimeoutError(msg)), ms, reject, message, ).unref() })}
export function bootstrapTelemetry() { if (process.env.USER_TYPE === 'ant') { // Read from ANT_ prefixed variables that are defined at build time if (process.env.ANT_OTEL_METRICS_EXPORTER) { process.env.OTEL_METRICS_EXPORTER = process.env.ANT_OTEL_METRICS_EXPORTER } if (process.env.ANT_OTEL_LOGS_EXPORTER) { process.env.OTEL_LOGS_EXPORTER = process.env.ANT_OTEL_LOGS_EXPORTER } if (process.env.ANT_OTEL_TRACES_EXPORTER) { process.env.OTEL_TRACES_EXPORTER = process.env.ANT_OTEL_TRACES_EXPORTER } if (process.env.ANT_OTEL_EXPORTER_OTLP_PROTOCOL) { process.env.OTEL_EXPORTER_OTLP_PROTOCOL = process.env.ANT_OTEL_EXPORTER_OTLP_PROTOCOL } if (process.env.ANT_OTEL_EXPORTER_OTLP_ENDPOINT) { process.env.OTEL_EXPORTER_OTLP_ENDPOINT = process.env.ANT_OTEL_EXPORTER_OTLP_ENDPOINT } if (process.env.ANT_OTEL_EXPORTER_OTLP_HEADERS) { process.env.OTEL_EXPORTER_OTLP_HEADERS = process.env.ANT_OTEL_EXPORTER_OTLP_HEADERS } }
// Set default tempoality to 'delta' because it's the more sane default if (!process.env.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE) { process.env.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = 'delta' }}
// Per OTEL spec, "none" means "no automatically configured exporter for this signal".// https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#exporter-selectionexport function parseExporterTypes(value: string | undefined): string[] { return (value || '') .trim() .split(',') .filter(Boolean) .map(t => t.trim()) .filter(t => t !== 'none')}
async function getOtlpReaders() { const exporterTypes = parseExporterTypes(process.env.OTEL_METRICS_EXPORTER) const exportInterval = parseInt( process.env.OTEL_METRIC_EXPORT_INTERVAL || DEFAULT_METRICS_EXPORT_INTERVAL_MS.toString(), )
const exporters = [] for (const exporterType of exporterTypes) { if (exporterType === 'console') { // Custom console exporter that shows resource attributes const consoleExporter = new ConsoleMetricExporter() const originalExport = consoleExporter.export.bind(consoleExporter)
consoleExporter.export = (metrics, callback) => { // Log resource attributes once at the start if (metrics.resource && metrics.resource.attributes) { // The console exporter is for debugging, so console output is intentional here
logForDebugging('\n=== Resource Attributes ===') logForDebugging(jsonStringify(metrics.resource.attributes)) logForDebugging('===========================\n') }
return originalExport(metrics, callback) }
exporters.push(consoleExporter) } else if (exporterType === 'otlp') { const protocol = process.env.OTEL_EXPORTER_OTLP_METRICS_PROTOCOL?.trim() || process.env.OTEL_EXPORTER_OTLP_PROTOCOL?.trim()
const httpConfig = getOTLPExporterConfig()
switch (protocol) { case 'grpc': { // Lazy-import to keep @grpc/grpc-js (~700KB) out of the telemetry chunk // when the protocol is http/protobuf (ant default) or http/json. const { OTLPMetricExporter } = await import( '@opentelemetry/exporter-metrics-otlp-grpc' ) exporters.push(new OTLPMetricExporter()) break } case 'http/json': { const { OTLPMetricExporter } = await import( '@opentelemetry/exporter-metrics-otlp-http' ) exporters.push(new OTLPMetricExporter(httpConfig)) break } case 'http/protobuf': { const { OTLPMetricExporter } = await import( '@opentelemetry/exporter-metrics-otlp-proto' ) exporters.push(new OTLPMetricExporter(httpConfig)) break } default: throw new Error( `Unknown protocol set in OTEL_EXPORTER_OTLP_METRICS_PROTOCOL or OTEL_EXPORTER_OTLP_PROTOCOL env var: ${protocol}`, ) } } else if (exporterType === 'prometheus') { const { PrometheusExporter } = await import( '@opentelemetry/exporter-prometheus' ) exporters.push(new PrometheusExporter()) } else { throw new Error( `Unknown exporter type set in OTEL_EXPORTER_OTLP_METRICS_PROTOCOL or OTEL_EXPORTER_OTLP_PROTOCOL env var: ${exporterType}`, ) } }
return exporters.map(exporter => { if ('export' in exporter) { return new PeriodicExportingMetricReader({ exporter, exportIntervalMillis: exportInterval, }) } return exporter })}
async function getOtlpLogExporters() { const exporterTypes = parseExporterTypes(process.env.OTEL_LOGS_EXPORTER)
const protocol = process.env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL?.trim() || process.env.OTEL_EXPORTER_OTLP_PROTOCOL?.trim() const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT
logForDebugging( `[3P telemetry] getOtlpLogExporters: types=${jsonStringify(exporterTypes)}, protocol=${protocol}, endpoint=${endpoint}`, )
const exporters = [] for (const exporterType of exporterTypes) { if (exporterType === 'console') { exporters.push(new ConsoleLogRecordExporter()) } else if (exporterType === 'otlp') { const httpConfig = getOTLPExporterConfig()
switch (protocol) { case 'grpc': { const { OTLPLogExporter } = await import( '@opentelemetry/exporter-logs-otlp-grpc' ) exporters.push(new OTLPLogExporter()) break } case 'http/json': { const { OTLPLogExporter } = await import( '@opentelemetry/exporter-logs-otlp-http' ) exporters.push(new OTLPLogExporter(httpConfig)) break } case 'http/protobuf': { const { OTLPLogExporter } = await import( '@opentelemetry/exporter-logs-otlp-proto' ) exporters.push(new OTLPLogExporter(httpConfig)) break } default: throw new Error( `Unknown protocol set in OTEL_EXPORTER_OTLP_LOGS_PROTOCOL or OTEL_EXPORTER_OTLP_PROTOCOL env var: ${protocol}`, ) } } else { throw new Error( `Unknown exporter type set in OTEL_LOGS_EXPORTER env var: ${exporterType}`, ) } }
return exporters}
async function getOtlpTraceExporters() { const exporterTypes = parseExporterTypes(process.env.OTEL_TRACES_EXPORTER)
const exporters = [] for (const exporterType of exporterTypes) { if (exporterType === 'console') { exporters.push(new ConsoleSpanExporter()) } else if (exporterType === 'otlp') { const protocol = process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL?.trim() || process.env.OTEL_EXPORTER_OTLP_PROTOCOL?.trim()
const httpConfig = getOTLPExporterConfig()
switch (protocol) { case 'grpc': { const { OTLPTraceExporter } = await import( '@opentelemetry/exporter-trace-otlp-grpc' ) exporters.push(new OTLPTraceExporter()) break } case 'http/json': { const { OTLPTraceExporter } = await import( '@opentelemetry/exporter-trace-otlp-http' ) exporters.push(new OTLPTraceExporter(httpConfig)) break } case 'http/protobuf': { const { OTLPTraceExporter } = await import( '@opentelemetry/exporter-trace-otlp-proto' ) exporters.push(new OTLPTraceExporter(httpConfig)) break } default: throw new Error( `Unknown protocol set in OTEL_EXPORTER_OTLP_TRACES_PROTOCOL or OTEL_EXPORTER_OTLP_PROTOCOL env var: ${protocol}`, ) } } else { throw new Error( `Unknown exporter type set in OTEL_TRACES_EXPORTER env var: ${exporterType}`, ) } }
return exporters}
export function isTelemetryEnabled() { return isEnvTruthy(process.env.CLAUDE_CODE_ENABLE_TELEMETRY)}
function getBigQueryExportingReader() { const bigqueryExporter = new BigQueryMetricsExporter() return new PeriodicExportingMetricReader({ exporter: bigqueryExporter, exportIntervalMillis: 5 * 60 * 1000, // 5mins for BigQuery metrics exporter to reduce load })}
function isBigQueryMetricsEnabled() { // BigQuery metrics are enabled for: // 1. API customers (excluding Claude.ai subscribers and Bedrock/Vertex) // 2. Claude for Enterprise (C4E) users // 3. Claude for Teams users const subscriptionType = getSubscriptionType() const isC4EOrTeamUser = isClaudeAISubscriber() && (subscriptionType === 'enterprise' || subscriptionType === 'team')
return is1PApiCustomer() || isC4EOrTeamUser}
/** * Initialize beta tracing - a separate code path for detailed debugging. * Uses BETA_TRACING_ENDPOINT instead of OTEL_EXPORTER_OTLP_ENDPOINT. */async function initializeBetaTracing( resource: ReturnType<typeof resourceFromAttributes>,): Promise<void> { const endpoint = process.env.BETA_TRACING_ENDPOINT if (!endpoint) { return }
const [{ OTLPTraceExporter }, { OTLPLogExporter }] = await Promise.all([ import('@opentelemetry/exporter-trace-otlp-http'), import('@opentelemetry/exporter-logs-otlp-http'), ])
const httpConfig = { url: `${endpoint}/v1/traces`, }
const logHttpConfig = { url: `${endpoint}/v1/logs`, }
// Initialize trace exporter const traceExporter = new OTLPTraceExporter(httpConfig) const spanProcessor = new BatchSpanProcessor(traceExporter, { scheduledDelayMillis: DEFAULT_TRACES_EXPORT_INTERVAL_MS, })
const tracerProvider = new BasicTracerProvider({ resource, spanProcessors: [spanProcessor], })
trace.setGlobalTracerProvider(tracerProvider) setTracerProvider(tracerProvider)
// Initialize log exporter const logExporter = new OTLPLogExporter(logHttpConfig) const loggerProvider = new LoggerProvider({ resource, processors: [ new BatchLogRecordProcessor(logExporter, { scheduledDelayMillis: DEFAULT_LOGS_EXPORT_INTERVAL_MS, }), ], })
logs.setGlobalLoggerProvider(loggerProvider) setLoggerProvider(loggerProvider)
// Initialize event logger const eventLogger = logs.getLogger( 'com.anthropic.claude_code.events', MACRO.VERSION, ) setEventLogger(eventLogger)
// Setup flush handlers - flush both logs AND traces process.on('beforeExit', async () => { await loggerProvider?.forceFlush() await tracerProvider?.forceFlush() })
process.on('exit', () => { void loggerProvider?.forceFlush() void tracerProvider?.forceFlush() })}
export async function initializeTelemetry() { profileCheckpoint('telemetry_init_start') bootstrapTelemetry()
// Console exporters call console.dir on a timer (5s logs/traces, 60s // metrics), writing pretty-printed objects to stdout. In stream-json // mode stdout is the SDK message channel; the first line (`{`) breaks // the SDK's line reader. Stripped here (not main.tsx) because init.ts // re-runs applyConfigEnvironmentVariables() inside initializeTelemetry- // AfterTrust for remote-managed-settings users, and bootstrapTelemetry // above copies ANT_OTEL_* for ant users — both would undo an earlier strip. if (getHasFormattedOutput()) { for (const key of [ 'OTEL_METRICS_EXPORTER', 'OTEL_LOGS_EXPORTER', 'OTEL_TRACES_EXPORTER', ] as const) { const v = process.env[key] if (v?.includes('console')) { process.env[key] = v .split(',') .map(s => s.trim()) .filter(s => s !== 'console') .join(',') } } }
diag.setLogger(new ClaudeCodeDiagLogger(), DiagLogLevel.ERROR)
// Initialize Perfetto tracing (independent of OTEL) // Enable via CLAUDE_CODE_PERFETTO_TRACE=1 or CLAUDE_CODE_PERFETTO_TRACE=<path> initializePerfettoTracing()
const readers = []
// Add customer exporters (if enabled) const telemetryEnabled = isTelemetryEnabled() logForDebugging( `[3P telemetry] isTelemetryEnabled=${telemetryEnabled} (CLAUDE_CODE_ENABLE_TELEMETRY=${process.env.CLAUDE_CODE_ENABLE_TELEMETRY})`, ) if (telemetryEnabled) { readers.push(...(await getOtlpReaders())) }
// Add BigQuery exporter (for API customers, C4E users, and internal users) if (isBigQueryMetricsEnabled()) { readers.push(getBigQueryExportingReader()) }
// Create base resource with service attributes const platform = getPlatform() const baseAttributes: Record<string, string> = { [ATTR_SERVICE_NAME]: 'claude-code', [ATTR_SERVICE_VERSION]: MACRO.VERSION, }
// Add WSL-specific attributes if running on WSL if (platform === 'wsl') { const wslVersion = getWslVersion() if (wslVersion) { baseAttributes['wsl.version'] = wslVersion } }
const baseResource = resourceFromAttributes(baseAttributes)
// Use OpenTelemetry detectors const osResource = resourceFromAttributes( osDetector.detect().attributes || {}, )
// Extract only host.arch from hostDetector const hostDetected = hostDetector.detect() const hostArchAttributes = hostDetected.attributes?.[SEMRESATTRS_HOST_ARCH] ? { [SEMRESATTRS_HOST_ARCH]: hostDetected.attributes[SEMRESATTRS_HOST_ARCH], } : {} const hostArchResource = resourceFromAttributes(hostArchAttributes)
const envResource = resourceFromAttributes( envDetector.detect().attributes || {}, )
// Merge resources - later resources take precedence const resource = baseResource .merge(osResource) .merge(hostArchResource) .merge(envResource)
// Check if beta tracing is enabled - this is a separate code path // Available to all users who set ENABLE_BETA_TRACING_DETAILED=1 and BETA_TRACING_ENDPOINT if (isBetaTracingEnabled()) { void initializeBetaTracing(resource).catch(e => logForDebugging(`Beta tracing init failed: ${e}`, { level: 'error' }), ) // Still set up meter provider for metrics (but skip regular logs/traces setup) const meterProvider = new MeterProvider({ resource, views: [], readers, }) setMeterProvider(meterProvider)
// Register shutdown for beta tracing const shutdownTelemetry = async () => { const timeoutMs = parseInt( process.env.CLAUDE_CODE_OTEL_SHUTDOWN_TIMEOUT_MS || '2000', ) try { endInteractionSpan()
// Force flush + shutdown together inside the timeout. Previously forceFlush // was awaited unbounded BEFORE the race, blocking exit on slow OTLP endpoints. // Each provider's flush→shutdown is chained independently so a slow logger // flush doesn't delay meterProvider/tracerProvider shutdown (no waterfall). const loggerProvider = getLoggerProvider() const tracerProvider = getTracerProvider()
const chains: Promise<void>[] = [meterProvider.shutdown()] if (loggerProvider) { chains.push( loggerProvider.forceFlush().then(() => loggerProvider.shutdown()), ) } if (tracerProvider) { chains.push( tracerProvider.forceFlush().then(() => tracerProvider.shutdown()), ) }
await Promise.race([ Promise.all(chains), telemetryTimeout(timeoutMs, 'OpenTelemetry shutdown timeout'), ]) } catch { // Ignore shutdown errors } } registerCleanup(shutdownTelemetry)
return meterProvider.getMeter('com.anthropic.claude_code', MACRO.VERSION) }
const meterProvider = new MeterProvider({ resource, views: [], readers, })
// Store reference in state for flushing setMeterProvider(meterProvider)
// Initialize logs if telemetry is enabled if (telemetryEnabled) { const logExporters = await getOtlpLogExporters() logForDebugging( `[3P telemetry] Created ${logExporters.length} log exporter(s)`, )
if (logExporters.length > 0) { const loggerProvider = new LoggerProvider({ resource, // Add batch processors for each exporter processors: logExporters.map( exporter => new BatchLogRecordProcessor(exporter, { scheduledDelayMillis: parseInt( process.env.OTEL_LOGS_EXPORT_INTERVAL || DEFAULT_LOGS_EXPORT_INTERVAL_MS.toString(), ), }), ), })
// Register the logger provider globally logs.setGlobalLoggerProvider(loggerProvider) setLoggerProvider(loggerProvider)
// Initialize event logger const eventLogger = logs.getLogger( 'com.anthropic.claude_code.events', MACRO.VERSION, ) setEventLogger(eventLogger) logForDebugging('[3P telemetry] Event logger set successfully')
// 'beforeExit' is emitted when Node.js empties its event loop and has no additional work to schedule. // Unlike 'exit', it allows us to perform async operations, so it works well for letting // network requests complete before the process exits naturally. process.on('beforeExit', async () => { await loggerProvider?.forceFlush() // Also flush traces - they use BatchSpanProcessor which needs explicit flush const tracerProvider = getTracerProvider() await tracerProvider?.forceFlush() })
process.on('exit', () => { // Final attempt to flush logs and traces void loggerProvider?.forceFlush() void getTracerProvider()?.forceFlush() }) } }
// Initialize tracing if enhanced telemetry is enabled (BETA) if (telemetryEnabled && isEnhancedTelemetryEnabled()) { const traceExporters = await getOtlpTraceExporters() if (traceExporters.length > 0) { // Create span processors for each exporter const spanProcessors = traceExporters.map( exporter => new BatchSpanProcessor(exporter, { scheduledDelayMillis: parseInt( process.env.OTEL_TRACES_EXPORT_INTERVAL || DEFAULT_TRACES_EXPORT_INTERVAL_MS.toString(), ), }), )
const tracerProvider = new BasicTracerProvider({ resource, spanProcessors, })
// Register the tracer provider globally trace.setGlobalTracerProvider(tracerProvider) setTracerProvider(tracerProvider) } }
// Shutdown metrics and logs on exit (flushes and closes exporters) const shutdownTelemetry = async () => { const timeoutMs = parseInt( process.env.CLAUDE_CODE_OTEL_SHUTDOWN_TIMEOUT_MS || '2000', )
try { // End any active interaction span before shutdown endInteractionSpan()
const shutdownPromises = [meterProvider.shutdown()] const loggerProvider = getLoggerProvider() if (loggerProvider) { shutdownPromises.push(loggerProvider.shutdown()) } const tracerProvider = getTracerProvider() if (tracerProvider) { shutdownPromises.push(tracerProvider.shutdown()) }
await Promise.race([ Promise.all(shutdownPromises), telemetryTimeout(timeoutMs, 'OpenTelemetry shutdown timeout'), ]) } catch (error) { if (error instanceof Error && error.message.includes('timeout')) { logForDebugging( `OpenTelemetry telemetry flush timed out after ${timeoutMs}ms
To resolve this issue, you can:1. Increase the timeout by setting CLAUDE_CODE_OTEL_SHUTDOWN_TIMEOUT_MS env var (e.g., 5000 for 5 seconds)2. Check if your OpenTelemetry backend is experiencing scalability issues3. Disable OpenTelemetry by unsetting CLAUDE_CODE_ENABLE_TELEMETRY env var
Current timeout: ${timeoutMs}ms`, { level: 'error' }, ) } throw error } }
// Always register shutdown (internal metrics are always enabled) registerCleanup(shutdownTelemetry)
return meterProvider.getMeter('com.anthropic.claude_code', MACRO.VERSION)}
/** * Flush all pending telemetry data immediately. * This should be called before logout or org switching to prevent data leakage. */export async function flushTelemetry(): Promise<void> { const meterProvider = getMeterProvider() if (!meterProvider) { return }
const timeoutMs = parseInt( process.env.CLAUDE_CODE_OTEL_FLUSH_TIMEOUT_MS || '5000', )
try { const flushPromises = [meterProvider.forceFlush()] const loggerProvider = getLoggerProvider() if (loggerProvider) { flushPromises.push(loggerProvider.forceFlush()) } const tracerProvider = getTracerProvider() if (tracerProvider) { flushPromises.push(tracerProvider.forceFlush()) }
await Promise.race([ Promise.all(flushPromises), telemetryTimeout(timeoutMs, 'OpenTelemetry flush timeout'), ])
logForDebugging('Telemetry flushed successfully') } catch (error) { if (error instanceof TelemetryTimeoutError) { logForDebugging( `Telemetry flush timed out after ${timeoutMs}ms. Some metrics may not be exported.`, { level: 'warn' }, ) } else { logForDebugging(`Telemetry flush failed: ${errorMessage(error)}`, { level: 'error', }) } // Don't throw - allow logout to continue even if flush fails }}
function parseOtelHeadersEnvVar(): Record<string, string> { const headers: Record<string, string> = {} const envHeaders = process.env.OTEL_EXPORTER_OTLP_HEADERS if (envHeaders) { for (const pair of envHeaders.split(',')) { const [key, ...valueParts] = pair.split('=') if (key && valueParts.length > 0) { headers[key.trim()] = valueParts.join('=').trim() } } } return headers}
/** * Get configuration for OTLP exporters including: * - HTTP agent options (proxy, mTLS) * - Dynamic headers via otelHeadersHelper or static headers from env var */function getOTLPExporterConfig() { const proxyUrl = getProxyUrl() const mtlsConfig = getMTLSConfig() const settings = getSettings_DEPRECATED()
// Build base config const config: Record<string, unknown> = {}
// Parse static headers from env var once (doesn't change at runtime) const staticHeaders = parseOtelHeadersEnvVar()
// If otelHeadersHelper is configured, use async headers function for dynamic refresh // Otherwise just return static headers if any exist if (settings?.otelHeadersHelper) { config.headers = async (): Promise<Record<string, string>> => { const dynamicHeaders = getOtelHeadersFromHelper() return { ...staticHeaders, ...dynamicHeaders } } } else if (Object.keys(staticHeaders).length > 0) { config.headers = async (): Promise<Record<string, string>> => staticHeaders }
// Check if we should bypass proxy for OTEL endpoint const otelEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT if (!proxyUrl || (otelEndpoint && shouldBypassProxy(otelEndpoint))) { // No proxy configured or OTEL endpoint should bypass proxy const caCerts = getCACertificates() if (mtlsConfig || caCerts) { config.httpAgentOptions = { ...mtlsConfig, ...(caCerts && { ca: caCerts }), } } return config }
// Return an HttpAgentFactory function that creates our proxy agent const caCerts = getCACertificates() const agentFactory = (_protocol: string) => { // Create and return the proxy agent with mTLS and CA cert config const proxyAgent = mtlsConfig || caCerts ? new HttpsProxyAgent(proxyUrl, { ...(mtlsConfig && { cert: mtlsConfig.cert, key: mtlsConfig.key, passphrase: mtlsConfig.passphrase, }), ...(caCerts && { ca: caCerts }), }) : new HttpsProxyAgent(proxyUrl)
return proxyAgent }
config.httpAgentOptions = agentFactory return config}