autotel
Version:
Write Once, Observe Anywhere
1 lines • 114 kB
Source Map (JSON)
{"version":3,"file":"init-BXiuPK6j.cjs","names":["safeRequire","propagation","otelContext","requireModule","REDACTOR_PRESETS","AlwaysOnSampler","AlwaysOffSampler","TraceIdRatioBasedSampler","ParentBasedSampler","SamplingDecision","requireModule","OTLPTraceExporterHTTP","OTLPMetricExporterHTTP","OTLPLogExporterHTTP","safeRequire","loadYamlConfig","normalizeAttributeRedactorConfig","ATTR_SERVICE_NAME","ATTR_SERVICE_VERSION","TailSamplingSpanProcessor","BatchSpanProcessor","SimpleSpanProcessor","PrettyConsoleExporter","ConsoleSpanExporter","CanonicalLogLineProcessor","AttributeRedactingProcessor","SpanNameNormalizingProcessor","FilteringSpanProcessor","PeriodicExportingMetricReader","BatchLogRecordProcessor","resolveSamplingPreset","samplingPresets","NodeSDK"],"sources":["../src/posthog-logs.ts","../src/baggage-span-processor.ts","../src/redact-values.ts","../src/env-config.ts","../src/devtools.ts","../src/init.ts"],"sourcesContent":["import type { LogRecordProcessor } from '@opentelemetry/sdk-logs';\nimport { safeRequire } from './node-require';\nimport type { StringRedactor } from './redact-values';\n\nexport class RedactingLogRecordProcessor implements LogRecordProcessor {\n constructor(\n private wrapped: LogRecordProcessor,\n private redact: StringRedactor,\n ) {}\n\n onEmit(logRecord: any, context?: any): void {\n if (logRecord.body && typeof logRecord.body === 'string') {\n logRecord.body = this.redact(logRecord.body);\n }\n if (logRecord.attributes) {\n for (const [key, value] of Object.entries(logRecord.attributes)) {\n if (typeof value === 'string') {\n logRecord.attributes[key] = this.redact(value);\n } else if (Array.isArray(value)) {\n logRecord.attributes[key] = value.map((item: unknown) =>\n typeof item === 'string' ? this.redact(item) : item,\n );\n }\n }\n }\n this.wrapped.onEmit(logRecord, context);\n }\n\n shutdown(): Promise<void> {\n return this.wrapped.shutdown();\n }\n\n forceFlush(): Promise<void> {\n return this.wrapped.forceFlush();\n }\n}\n\nexport interface PostHogConfig {\n /** OTLP logs endpoint URL (e.g., https://us.i.posthog.com/i/v1/logs?token=phc_xxx) */\n url: string;\n}\n\n/**\n * Build log record processors for PostHog OTLP logs integration.\n *\n * Resolution order:\n * 1. config.url if provided\n * 2. POSTHOG_LOGS_URL env var\n * 3. Empty array (disabled)\n */\nexport function buildPostHogLogProcessors(\n config: PostHogConfig | undefined,\n stringRedactor?: StringRedactor | null,\n): LogRecordProcessor[] {\n const url = config?.url || process.env.POSTHOG_LOGS_URL;\n if (!url) return [];\n\n const sdkLogs = safeRequire<{\n BatchLogRecordProcessor: new (exporter: unknown) => LogRecordProcessor;\n }>('@opentelemetry/sdk-logs');\n\n const exporterModule = safeRequire<{\n OTLPLogExporter: new (config: { url: string }) => unknown;\n }>('@opentelemetry/exporter-logs-otlp-http');\n\n if (!sdkLogs || !exporterModule) return [];\n\n const exporter = new exporterModule.OTLPLogExporter({ url });\n let processor: LogRecordProcessor = new sdkLogs.BatchLogRecordProcessor(\n exporter,\n );\n if (stringRedactor) {\n processor = new RedactingLogRecordProcessor(processor, stringRedactor);\n }\n\n return [processor];\n}\n","/**\n * Span processor that copies baggage entries to span attributes\n *\n * This makes baggage visible in trace UIs without manual attribute setting.\n * Enabled via init({ baggage: true }) or init({ baggage: 'custom-prefix' })\n */\n\nimport type { Span, Context } from '@opentelemetry/api';\nimport { propagation, context as otelContext } from '@opentelemetry/api';\nimport type { SpanProcessor } from '@opentelemetry/sdk-trace-base';\nimport type { ReadableSpan } from '@opentelemetry/sdk-trace-base';\nimport { requireModule } from './node-require';\n\nexport interface BaggageSpanProcessorOptions {\n /**\n * Prefix for baggage attributes\n * @default 'baggage.'\n */\n prefix?: string;\n}\n\n/**\n * Span processor that automatically copies baggage entries to span attributes\n *\n * This makes baggage visible in trace UIs (Jaeger, Grafana, DataDog, etc.)\n * without manually calling ctx.setAttribute() for each baggage entry.\n *\n * @example Enable in init()\n * ```typescript\n * init({\n * service: 'my-app',\n * baggage: true // Uses default 'baggage.' prefix\n * });\n *\n * // Now baggage automatically appears as span attributes\n * await withBaggage({\n * baggage: { 'tenant.id': 't1', 'user.id': 'u1' },\n * fn: async () => {\n * // Span has baggage.tenant.id and baggage.user.id attributes!\n * }\n * });\n * ```\n *\n * @example Custom prefix\n * ```typescript\n * init({\n * service: 'my-app',\n * baggage: 'ctx' // Uses 'ctx.' prefix\n * });\n * // Creates attributes: ctx.tenant.id, ctx.user.id\n * ```\n */\nexport class BaggageSpanProcessor implements SpanProcessor {\n private readonly prefix: string;\n\n constructor(options: BaggageSpanProcessorOptions = {}) {\n this.prefix = options.prefix ?? 'baggage.';\n }\n\n onStart(span: Span, parentContext: Context): void {\n // Read baggage from parentContext first (spans created with explicit context)\n // Then fall back to active context (spans created without explicit context)\n // Also check getActiveContextWithBaggage() to see baggage set via ctx.setBaggage()\n let baggage = propagation.getBaggage(parentContext);\n if (!baggage) {\n baggage = propagation.getBaggage(otelContext.active());\n }\n // Check stored context from ctx.setBaggage() if still no baggage\n if (!baggage) {\n try {\n const { getActiveContextWithBaggage } = requireModule<{\n getActiveContextWithBaggage: () => Context;\n }>('./trace-context');\n const storedContext = getActiveContextWithBaggage();\n baggage = propagation.getBaggage(storedContext);\n } catch {\n // Fallback if trace-context isn't available\n }\n }\n if (!baggage) return;\n\n // Copy all baggage entries to span attributes\n for (const [key, entry] of baggage.getAllEntries()) {\n span.setAttribute(`${this.prefix}${key}`, entry.value);\n }\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n onEnd(_span: ReadableSpan): void {\n // No-op - required by SpanProcessor interface\n }\n\n async shutdown(): Promise<void> {\n // No-op\n }\n\n async forceFlush(): Promise<void> {\n // No-op\n }\n}\n","/** Standalone string redaction for use outside the span processor pipeline. */\n\nimport {\n REDACTOR_PRESETS,\n type AttributeRedactorConfig,\n type AttributeRedactorPreset,\n type ValuePatternConfig,\n} from './attribute-redacting-processor';\n\nexport type StringRedactor = (value: string) => string;\nexport function createStringRedactor(\n config: AttributeRedactorConfig | AttributeRedactorPreset,\n): StringRedactor {\n const resolved =\n typeof config === 'string' ? REDACTOR_PRESETS[config] : config;\n const valuePatterns: ValuePatternConfig[] = resolved.valuePatterns ?? [];\n const defaultReplacement = resolved.replacement ?? '[REDACTED]';\n\n return (value: string): string => {\n let result = value;\n for (const { pattern, replacement, mask } of valuePatterns) {\n pattern.lastIndex = 0;\n // Smart masks (e.g. email → a***@***.com) take precedence over the\n // static replacement so callers see the same output as the\n // span-attribute redactor does.\n if (mask) {\n result = result.replaceAll(pattern, (match) => mask(match));\n } else {\n result = result.replaceAll(pattern, replacement ?? defaultReplacement);\n }\n }\n return result;\n };\n}\n","/**\n * Standard OpenTelemetry environment variables\n */\nimport type { Sampler as OtelSampler } from '@opentelemetry/sdk-trace-base';\nimport {\n AlwaysOffSampler,\n AlwaysOnSampler,\n ParentBasedSampler,\n TraceIdRatioBasedSampler,\n} from '@opentelemetry/sdk-trace-base';\n\nexport interface OtelEnvVars {\n OTEL_SERVICE_NAME?: string;\n OTEL_EXPORTER_OTLP_ENDPOINT?: string;\n OTEL_EXPORTER_OTLP_HEADERS?: string;\n OTEL_RESOURCE_ATTRIBUTES?: string;\n OTEL_EXPORTER_OTLP_PROTOCOL?: 'http' | 'grpc';\n OTEL_TRACES_SAMPLER?: string;\n OTEL_TRACES_SAMPLER_ARG?: string;\n}\n\n/**\n * Parsed resource attributes as key-value pairs\n */\nexport interface ResourceAttributes {\n [key: string]: string;\n}\n\n/**\n * Parsed OTLP headers as key-value pairs\n */\nexport interface OtlpHeaders {\n [key: string]: string;\n}\n\n/**\n * Environment-resolved configuration (subset of AutotelConfig)\n * Defined locally to avoid circular dependency with init.ts\n */\nexport interface EnvConfig {\n service?: string;\n endpoint?: string;\n protocol?: 'http' | 'grpc';\n headers?: Record<string, string>;\n resourceAttributes?: Record<string, string>;\n otelSampler?: OtelSampler;\n}\n\n/**\n * Validate URL format\n */\nfunction isValidUrl(urlString: string): boolean {\n try {\n const url = new URL(urlString);\n return url.protocol === 'http:' || url.protocol === 'https:';\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve OpenTelemetry environment variables from process.env\n */\nexport function resolveOtelEnv(): OtelEnvVars {\n const env: OtelEnvVars = {};\n\n // OTEL_SERVICE_NAME - optional string\n if (process.env.OTEL_SERVICE_NAME) {\n const value = process.env.OTEL_SERVICE_NAME.trim();\n if (value) {\n env.OTEL_SERVICE_NAME = value;\n }\n }\n\n // OTEL_EXPORTER_OTLP_ENDPOINT - optional URL\n if (process.env.OTEL_EXPORTER_OTLP_ENDPOINT) {\n const value = process.env.OTEL_EXPORTER_OTLP_ENDPOINT.trim();\n if (value && isValidUrl(value)) {\n env.OTEL_EXPORTER_OTLP_ENDPOINT = value;\n }\n }\n\n // OTEL_EXPORTER_OTLP_HEADERS - optional string\n if (process.env.OTEL_EXPORTER_OTLP_HEADERS) {\n const value = process.env.OTEL_EXPORTER_OTLP_HEADERS.trim();\n if (value) {\n env.OTEL_EXPORTER_OTLP_HEADERS = value;\n }\n }\n\n // OTEL_RESOURCE_ATTRIBUTES - optional string\n if (process.env.OTEL_RESOURCE_ATTRIBUTES) {\n const value = process.env.OTEL_RESOURCE_ATTRIBUTES.trim();\n if (value) {\n env.OTEL_RESOURCE_ATTRIBUTES = value;\n }\n }\n\n // OTEL_EXPORTER_OTLP_PROTOCOL - optional enum ('http' | 'grpc')\n if (process.env.OTEL_EXPORTER_OTLP_PROTOCOL) {\n const value = process.env.OTEL_EXPORTER_OTLP_PROTOCOL.trim().toLowerCase();\n if (value === 'http' || value === 'grpc') {\n env.OTEL_EXPORTER_OTLP_PROTOCOL = value;\n }\n }\n\n if (process.env.OTEL_TRACES_SAMPLER) {\n const value = process.env.OTEL_TRACES_SAMPLER.trim();\n if (value) {\n env.OTEL_TRACES_SAMPLER = value;\n }\n }\n\n if (process.env.OTEL_TRACES_SAMPLER_ARG) {\n const value = process.env.OTEL_TRACES_SAMPLER_ARG.trim();\n if (value) {\n env.OTEL_TRACES_SAMPLER_ARG = value;\n }\n }\n\n return env;\n}\n\nfunction parseRatioSamplerArg(\n samplerName: string,\n samplerArg: string | undefined,\n): number {\n if (samplerArg === undefined) {\n return 1.0;\n }\n\n const ratio = Number(samplerArg);\n if (!Number.isFinite(ratio) || ratio < 0 || ratio > 1) {\n console.error(\n `[autotel] Invalid OTEL_TRACES_SAMPLER_ARG=\"${samplerArg}\" for ${samplerName}. Expected a number in [0..1]. Falling back to 1.0.`,\n );\n return 1.0;\n }\n\n return ratio;\n}\n\nfunction warnOnUnusedSamplerArg(\n samplerName: string,\n samplerArg: string | undefined,\n): void {\n if (samplerArg !== undefined) {\n console.error(\n `[autotel] OTEL_TRACES_SAMPLER_ARG is not used by OTEL_TRACES_SAMPLER=\"${samplerName}\". Ignoring value \"${samplerArg}\".`,\n );\n }\n}\n\nexport function createSamplerFromEnv(\n env: Pick<OtelEnvVars, 'OTEL_TRACES_SAMPLER' | 'OTEL_TRACES_SAMPLER_ARG'>,\n): OtelSampler | undefined {\n const samplerName = env.OTEL_TRACES_SAMPLER;\n if (!samplerName) {\n return undefined;\n }\n\n switch (samplerName) {\n case 'always_on':\n warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);\n return new AlwaysOnSampler();\n case 'always_off':\n warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);\n return new AlwaysOffSampler();\n case 'traceidratio':\n return new TraceIdRatioBasedSampler(\n parseRatioSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG),\n );\n case 'parentbased_always_on':\n warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);\n return new ParentBasedSampler({ root: new AlwaysOnSampler() });\n case 'parentbased_always_off':\n warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);\n return new ParentBasedSampler({ root: new AlwaysOffSampler() });\n case 'parentbased_traceidratio':\n return new ParentBasedSampler({\n root: new TraceIdRatioBasedSampler(\n parseRatioSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG),\n ),\n });\n case 'jaeger_remote':\n case 'parentbased_jaeger_remote':\n case 'xray':\n console.error(\n `[autotel] OTEL_TRACES_SAMPLER=\"${samplerName}\" is not supported yet by autotel. Falling back to the next sampler source.`,\n );\n return undefined;\n default:\n console.error(\n `[autotel] Unknown OTEL_TRACES_SAMPLER=\"${samplerName}\". Falling back to the next sampler source.`,\n );\n return undefined;\n }\n}\n\n/**\n * Parse OTEL_RESOURCE_ATTRIBUTES from comma-separated key=value pairs\n * Example: \"service.version=1.0.0,deployment.environment=production\"\n */\nexport function parseResourceAttributes(\n input: string | undefined,\n): ResourceAttributes {\n if (!input || input.trim() === '') {\n return {};\n }\n\n const attributes: ResourceAttributes = {};\n const pairs = input.split(',');\n\n for (const pair of pairs) {\n const trimmedPair = pair.trim();\n if (!trimmedPair) continue;\n\n const equalIndex = trimmedPair.indexOf('=');\n if (equalIndex === -1) {\n // Invalid format, skip this pair\n continue;\n }\n\n const key = trimmedPair.slice(0, equalIndex).trim();\n const value = trimmedPair.slice(equalIndex + 1).trim();\n\n if (key && value) {\n attributes[key] = value;\n }\n }\n\n return attributes;\n}\n\n/**\n * Parse OTEL_EXPORTER_OTLP_HEADERS from comma-separated key=value pairs\n * Example: \"api-key=secret123,x-custom-header=value\"\n */\nexport function parseOtlpHeaders(input: string | undefined): OtlpHeaders {\n if (!input || input.trim() === '') {\n return {};\n }\n\n const headers: OtlpHeaders = {};\n const pairs = input.split(',');\n\n for (const pair of pairs) {\n const trimmedPair = pair.trim();\n if (!trimmedPair) continue;\n\n const equalIndex = trimmedPair.indexOf('=');\n if (equalIndex === -1) {\n // Invalid format, skip this pair\n continue;\n }\n\n const key = trimmedPair.slice(0, equalIndex).trim();\n const value = trimmedPair.slice(equalIndex + 1).trim();\n\n if (key && value) {\n headers[key] = value;\n }\n }\n\n return headers;\n}\n\n/**\n * Convert resolved environment variables to config\n */\nexport function envToConfig(env: OtelEnvVars): EnvConfig {\n const config: EnvConfig = {};\n\n if (env.OTEL_SERVICE_NAME) {\n config.service = env.OTEL_SERVICE_NAME;\n }\n\n if (env.OTEL_EXPORTER_OTLP_ENDPOINT) {\n config.endpoint = env.OTEL_EXPORTER_OTLP_ENDPOINT;\n }\n\n if (env.OTEL_EXPORTER_OTLP_PROTOCOL) {\n config.protocol = env.OTEL_EXPORTER_OTLP_PROTOCOL;\n }\n\n if (env.OTEL_EXPORTER_OTLP_HEADERS) {\n config.headers = parseOtlpHeaders(env.OTEL_EXPORTER_OTLP_HEADERS);\n }\n\n const resourceAttrs = parseResourceAttributes(env.OTEL_RESOURCE_ATTRIBUTES);\n if (Object.keys(resourceAttrs).length > 0) {\n config.resourceAttributes = resourceAttrs;\n }\n\n const sampler = createSamplerFromEnv(env);\n if (sampler) {\n config.otelSampler = sampler;\n }\n\n return config;\n}\n\n/**\n * Main function to resolve config from environment variables\n */\nexport function resolveConfigFromEnv(): EnvConfig {\n const env = resolveOtelEnv();\n return envToConfig(env);\n}\n","export interface AutotelDevtoolsConfig {\n enabled?: boolean;\n endpoint?: string;\n embedded?: boolean;\n host?: string;\n port?: number;\n verbose?: boolean;\n}\n\nexport interface ResolvedAutotelDevtoolsConfig {\n enabled: boolean;\n endpoint?: string;\n embedded: boolean;\n host: string;\n port: number;\n verbose: boolean;\n}\n\nconst defaultHost = '127.0.0.1';\nconst defaultPort = 4318;\n\nexport function resolveDevtoolsConfig(\n config: boolean | AutotelDevtoolsConfig | undefined,\n): ResolvedAutotelDevtoolsConfig {\n if (!config) {\n return {\n enabled: false,\n endpoint: undefined,\n embedded: false,\n host: defaultHost,\n port: defaultPort,\n verbose: false,\n };\n }\n\n if (config === true) {\n return {\n enabled: true,\n endpoint: `http://${defaultHost}:${defaultPort}`,\n embedded: false,\n host: defaultHost,\n port: defaultPort,\n verbose: false,\n };\n }\n\n const enabled = config.enabled ?? true;\n const host = config.host ?? defaultHost;\n const port = config.port ?? defaultPort;\n const endpoint = config.endpoint ?? `http://${host}:${port}`;\n\n return {\n enabled,\n endpoint: enabled ? endpoint : undefined,\n embedded: enabled && (config.embedded ?? false),\n host,\n port,\n verbose: config.verbose ?? false,\n };\n}\n","/**\n * Simplified initialization for autotel\n *\n * Single init() function with sensible defaults.\n * Replaces initInstrumentation() and separate events config.\n */\n\nimport { NodeSDK } from '@opentelemetry/sdk-node';\nimport type { NodeSDKConfiguration } from '@opentelemetry/sdk-node';\nimport {\n BatchSpanProcessor,\n type SpanProcessor,\n SimpleSpanProcessor,\n ConsoleSpanExporter,\n SamplingDecision,\n type SpanExporter,\n type Sampler as OtelSampler,\n type SamplingResult,\n} from '@opentelemetry/sdk-trace-base';\nimport {\n resourceFromAttributes,\n type Resource,\n} from '@opentelemetry/resources';\nimport {\n ATTR_SERVICE_NAME,\n ATTR_SERVICE_VERSION,\n} from '@opentelemetry/semantic-conventions';\nimport type { Sampler, SamplingPreset } from './sampling';\nimport { samplingPresets, resolveSamplingPreset } from './sampling';\nimport type { EventSubscriber } from './event-subscriber';\nimport type { Logger } from './logger';\nimport type { Attributes, Context, SpanKind, Link } from '@opentelemetry/api';\nimport type { ValidationConfig } from './validation';\nimport {\n PeriodicExportingMetricReader,\n type MetricReader,\n} from '@opentelemetry/sdk-metrics';\nimport { OTLPMetricExporter as OTLPMetricExporterHTTP } from '@opentelemetry/exporter-metrics-otlp-http';\nimport { OTLPTraceExporter as OTLPTraceExporterHTTP } from '@opentelemetry/exporter-trace-otlp-http';\nimport { OTLPLogExporter as OTLPLogExporterHTTP } from '@opentelemetry/exporter-logs-otlp-http';\nimport type { PushMetricExporter } from '@opentelemetry/sdk-metrics';\nimport {\n BatchLogRecordProcessor,\n type LogRecordExporter,\n type LogRecordProcessor,\n} from '@opentelemetry/sdk-logs';\nimport {\n buildPostHogLogProcessors,\n RedactingLogRecordProcessor,\n} from './posthog-logs';\nimport { TailSamplingSpanProcessor } from './tail-sampling-processor';\nimport { BaggageSpanProcessor } from './baggage-span-processor';\nimport {\n FilteringSpanProcessor,\n type SpanFilterPredicate,\n} from './filtering-span-processor';\nimport {\n SpanNameNormalizingProcessor,\n type SpanNameNormalizerConfig,\n} from './span-name-normalizer';\nimport {\n AttributeRedactingProcessor,\n normalizeAttributeRedactorConfig,\n type AttributeRedactorConfig,\n type AttributeRedactorPreset,\n} from './attribute-redacting-processor';\nimport { createStringRedactor, type StringRedactor } from './redact-values';\nimport { PrettyConsoleExporter } from './pretty-console-exporter';\nimport { resolveConfigFromEnv } from './env-config';\nimport { loadYamlConfig } from './yaml-config';\nimport { requireModule, safeRequire } from './node-require';\nimport {\n CanonicalLogLineProcessor,\n type CanonicalLogLineOptions,\n} from './processors/canonical-log-line-processor';\nimport type { EventsConfig } from './events-config';\nimport { resolveDevtoolsConfig, type AutotelDevtoolsConfig } from './devtools';\n\n/**\n * Silent logger (no-op) - used as default when user doesn't provide one.\n * Internal autotel logs are silent by default to avoid spam.\n * Users can import { autotelLogger } from 'autotel/logger' to create their own.\n */\nconst silentLogger: Logger = {\n info: () => {},\n warn: () => {},\n error: () => {},\n debug: () => {},\n};\n\n/**\n * Adapts an Autotel Sampler to the OTel SDK Sampler interface.\n */\nfunction toOtelSampler(sampler: Sampler): OtelSampler {\n return {\n shouldSample(\n _context: Context,\n _traceId: string,\n spanName: string,\n _spanKind: SpanKind,\n _attributes: Attributes,\n links: Link[],\n ): SamplingResult {\n const shouldTrace = sampler.shouldSample({\n operationName: spanName,\n args: [],\n links,\n });\n return {\n decision: shouldTrace\n ? SamplingDecision.RECORD_AND_SAMPLED\n : SamplingDecision.NOT_RECORD,\n };\n },\n toString(): string {\n return `AutotelSamplerAdapter`;\n },\n };\n}\n\n// Type imports for exporters\ntype OTLPExporterConfig = {\n url?: string;\n headers?: Record<string, string>;\n timeoutMillis?: number;\n concurrencyLimit?: number;\n};\n\nexport type OtlpSignal = 'traces' | 'metrics' | 'logs';\n\nexport interface OtlpDestinationConfig {\n /**\n * Base OTLP endpoint for this destination.\n * HTTP destinations may omit `/v1/{signal}`; autotel appends it automatically.\n * gRPC destinations should point at the collector host:port.\n */\n endpoint: string;\n\n /**\n * Headers for this destination. Falls back to top-level `headers`.\n */\n headers?: Record<string, string> | string;\n\n /**\n * Protocol for this destination. Falls back to top-level `protocol`.\n */\n protocol?: 'http' | 'grpc';\n\n /**\n * Signals to send to this destination.\n * Defaults to all signals supported by the current init() config.\n */\n signals?: OtlpSignal[];\n}\n\n// Lazy-load gRPC exporters (optional peer dependencies)\nlet OTLPTraceExporterGRPC:\n | (new (config: OTLPExporterConfig) => SpanExporter)\n | undefined;\nlet OTLPMetricExporterGRPC:\n | (new (config: OTLPExporterConfig) => PushMetricExporter)\n | undefined;\nlet OTLPLogExporterGRPC:\n | (new (config: OTLPExporterConfig) => LogRecordExporter)\n | undefined;\n\n/**\n * Helper: Lazy-load gRPC trace exporter\n */\nfunction loadGRPCTraceExporter(): new (\n config: OTLPExporterConfig,\n) => SpanExporter {\n if (OTLPTraceExporterGRPC) return OTLPTraceExporterGRPC;\n\n try {\n // Dynamic import for optional peer dependency\n const grpcModule = requireModule<{\n OTLPTraceExporter: new (config: OTLPExporterConfig) => SpanExporter;\n }>('@opentelemetry/exporter-trace-otlp-grpc');\n OTLPTraceExporterGRPC = grpcModule.OTLPTraceExporter;\n return OTLPTraceExporterGRPC;\n } catch {\n throw new Error(\n 'gRPC trace exporter not found. Install @opentelemetry/exporter-trace-otlp-grpc',\n );\n }\n}\n\n/**\n * Helper: Lazy-load gRPC metric exporter\n */\nfunction loadGRPCMetricExporter(): new (\n config: OTLPExporterConfig,\n) => PushMetricExporter {\n if (OTLPMetricExporterGRPC) return OTLPMetricExporterGRPC;\n\n try {\n // Dynamic import for optional peer dependency\n const grpcModule = requireModule<{\n OTLPMetricExporter: new (\n config: OTLPExporterConfig,\n ) => PushMetricExporter;\n }>('@opentelemetry/exporter-metrics-otlp-grpc');\n OTLPMetricExporterGRPC = grpcModule.OTLPMetricExporter;\n return OTLPMetricExporterGRPC;\n } catch {\n throw new Error(\n 'gRPC metric exporter not found. Install @opentelemetry/exporter-metrics-otlp-grpc',\n );\n }\n}\n\n/**\n * Helper: Create trace exporter based on protocol\n */\nfunction createTraceExporter(\n protocol: 'http' | 'grpc',\n config: OTLPExporterConfig,\n): SpanExporter {\n if (protocol === 'grpc') {\n const Exporter = loadGRPCTraceExporter();\n return new Exporter(config);\n }\n\n // Default: HTTP\n return new OTLPTraceExporterHTTP(config);\n}\n\n/**\n * Helper: Create metric exporter based on protocol\n */\nfunction createMetricExporter(\n protocol: 'http' | 'grpc',\n config: OTLPExporterConfig,\n): PushMetricExporter {\n if (protocol === 'grpc') {\n const Exporter = loadGRPCMetricExporter();\n return new Exporter(config);\n }\n\n // Default: HTTP\n return new OTLPMetricExporterHTTP(config);\n}\n\n/**\n * Helper: Lazy-load gRPC log exporter\n */\nfunction loadGRPCLogExporter(): new (\n config: OTLPExporterConfig,\n) => LogRecordExporter {\n if (OTLPLogExporterGRPC) return OTLPLogExporterGRPC;\n\n try {\n const grpcModule = requireModule<{\n OTLPLogExporter: new (config: OTLPExporterConfig) => LogRecordExporter;\n }>('@opentelemetry/exporter-logs-otlp-grpc');\n OTLPLogExporterGRPC = grpcModule.OTLPLogExporter;\n return OTLPLogExporterGRPC;\n } catch {\n throw new Error(\n 'gRPC log exporter not found. Install @opentelemetry/exporter-logs-otlp-grpc',\n );\n }\n}\n\n/**\n * Helper: Create log exporter based on protocol\n */\nfunction createLogExporter(\n protocol: 'http' | 'grpc',\n config: OTLPExporterConfig,\n): LogRecordExporter {\n if (protocol === 'grpc') {\n const Exporter = loadGRPCLogExporter();\n return new Exporter(config);\n }\n\n // Default: HTTP\n return new OTLPLogExporterHTTP(config);\n}\n\n/**\n * Helper: Resolve protocol from config and environment\n */\nfunction resolveProtocol(configProtocol?: 'http' | 'grpc'): 'http' | 'grpc' {\n // 1. Check config parameter (highest priority)\n if (configProtocol === 'grpc' || configProtocol === 'http') {\n return configProtocol;\n }\n\n // 2. Check OTEL_EXPORTER_OTLP_PROTOCOL env var\n const envProtocol = process.env.OTEL_EXPORTER_OTLP_PROTOCOL;\n if (envProtocol === 'grpc') return 'grpc';\n if (envProtocol === 'http/protobuf' || envProtocol === 'http') return 'http';\n\n // 3. Default to HTTP\n return 'http';\n}\n\n/**\n * Helper: Adjust endpoint URL for protocol\n * gRPC exporters don't need the /v1/traces or /v1/metrics path\n * HTTP exporters need the full path\n */\nfunction formatEndpointUrl(\n endpoint: string,\n signal: 'traces' | 'metrics' | 'logs',\n protocol: 'http' | 'grpc',\n): string {\n if (protocol === 'grpc') {\n // gRPC: strip any paths, return base endpoint\n return endpoint.replace(/\\/(v1\\/)?(traces|metrics|logs)$/, '');\n }\n\n // HTTP: append signal path if not present\n if (!endpoint.endsWith(`/v1/${signal}`)) {\n return `${endpoint}/v1/${signal}`;\n }\n\n return endpoint;\n}\n\n// Built-in logger is created dynamically in init() with service name\n\nexport interface AutotelConfig {\n /** Service name (required) */\n service: string;\n\n /**\n * Local developer UX for autotel-devtools.\n *\n * - `true`: send traces, metrics, and logs to `http://127.0.0.1:4318`\n * - `{ embedded: true }`: attempt to start `autotel-devtools` automatically\n *\n * When enabled:\n * - `endpoint` defaults to the local devtools URL\n * - `logs` default to `true` unless explicitly set\n *\n * This keeps production config unchanged while making local debugging\n * effectively zero-config.\n */\n devtools?: boolean | AutotelDevtoolsConfig;\n\n /** Event subscribers - bring your own (PostHog, Mixpanel, etc.) */\n subscribers?: EventSubscriber[];\n\n /**\n * Additional OpenTelemetry instrumentations to register (raw OTel classes).\n * Useful when you need custom instrumentation configs or instrumentations\n * not covered by autoInstrumentations.\n *\n * **Important:** If you need custom instrumentation configs (like `requireParentSpan: false`),\n * use EITHER manual instrumentations OR autoInstrumentations, not both for the same library.\n * Manual instrumentations always take precedence over auto-instrumentations.\n *\n * @example Manual instrumentations with custom config\n * ```typescript\n * import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb'\n *\n * init({\n * service: 'my-app',\n * autoInstrumentations: false, // Disable auto-instrumentations\n * instrumentations: [\n * new MongoDBInstrumentation({\n * requireParentSpan: false // Custom config\n * })\n * ]\n * })\n * ```\n *\n * @example Mix auto + manual (auto for most, manual for specific configs)\n * ```typescript\n * import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb'\n *\n * init({\n * service: 'my-app',\n * autoInstrumentations: ['http', 'express'], // Auto for these\n * instrumentations: [\n * new MongoDBInstrumentation({\n * requireParentSpan: false // Manual config for MongoDB\n * })\n * ]\n * })\n * ```\n */\n instrumentations?: NodeSDKConfiguration['instrumentations'];\n\n /**\n * Simple names for auto-instrumentation.\n * Uses @opentelemetry/auto-instrumentations-node (peer dependency).\n *\n * **Important:** If you provide manual instrumentations for the same library,\n * the manual config takes precedence and auto-instrumentation for that library is disabled.\n *\n * @example Enable all auto-instrumentations (simple approach)\n * ```typescript\n * init({\n * service: 'my-app',\n * autoInstrumentations: true // Enable all with defaults\n * })\n * ```\n *\n * @example Enable specific auto-instrumentations\n * ```typescript\n * init({\n * service: 'my-app',\n * autoInstrumentations: ['express', 'pino', 'http']\n * })\n * ```\n *\n * @example Configure specific auto-instrumentations\n * ```typescript\n * init({\n * service: 'my-app',\n * autoInstrumentations: {\n * express: { enabled: true },\n * pino: { enabled: true },\n * http: { enabled: false }\n * }\n * })\n * ```\n *\n * @example Manual config when you need custom settings\n * ```typescript\n * import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb'\n *\n * init({\n * service: 'my-app',\n * autoInstrumentations: false, // Use manual control\n * instrumentations: [\n * new MongoDBInstrumentation({\n * requireParentSpan: false // Custom config not available with auto\n * })\n * ]\n * })\n * ```\n */\n autoInstrumentations?:\n | string[]\n | boolean\n | Record<string, { enabled?: boolean }>;\n\n /**\n * OTLP endpoint for traces/metrics/logs\n * Single-destination shorthand. For multi-backend OTLP fan-out, use\n * `destinations` instead.\n * Only used if you don't provide custom exporters/processors\n * @default process.env.OTLP_ENDPOINT || 'http://localhost:4318'\n */\n endpoint?: string;\n\n /**\n * Declarative OTLP multi-destination config.\n * Each destination can override endpoint, headers, protocol, and signals.\n *\n * This is the high-level alternative to wiring `spanExporters`,\n * `spanProcessors`, `metricReaders`, and `logRecordProcessors` manually when\n * you want to fan telemetry out to multiple OTLP backends.\n *\n * When provided, `destinations` takes precedence over the single `endpoint`\n * shorthand for built-in OTLP exporters/readers/processors.\n *\n * @example Grafana + Honeycomb for traces, Grafana only for metrics/logs\n * ```typescript\n * init({\n * service: 'my-app',\n * logs: true,\n * destinations: [\n * {\n * endpoint: 'https://otlp-gateway-prod-eu-west-2.grafana.net/otlp',\n * headers: { Authorization: 'Basic ...' },\n * },\n * {\n * endpoint: 'https://api.honeycomb.io',\n * headers: { 'x-honeycomb-team': '...' },\n * signals: ['traces'],\n * },\n * ],\n * })\n * ```\n */\n destinations?: OtlpDestinationConfig[];\n\n /**\n * Custom span processors for traces (supports multiple processors)\n * Allows you to use any backend: Jaeger, Zipkin, Datadog, New Relic, etc.\n * If not provided, defaults to OTLP with tail sampling\n *\n * @example Multiple processors\n * ```typescript\n * import { JaegerExporter } from '@opentelemetry/exporter-jaeger'\n * import { BatchSpanProcessor, SimpleSpanProcessor, ConsoleSpanExporter } from '@opentelemetry/sdk-trace-base'\n *\n * init({\n * service: 'my-app',\n * spanProcessors: [\n * new BatchSpanProcessor(new JaegerExporter()),\n * new SimpleSpanProcessor(new ConsoleSpanExporter()) // Debug alongside production\n * ]\n * })\n * ```\n *\n * @example Single processor\n * ```typescript\n * import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'\n *\n * init({\n * service: 'my-app',\n * spanProcessors: [new SimpleSpanProcessor(new ConsoleSpanExporter())]\n * })\n * ```\n */\n spanProcessors?: SpanProcessor[];\n\n /**\n * Custom span processor for traces (single-item alias of spanProcessors)\n * @deprecated Use spanProcessors for consistency with other multi-value config fields\n */\n spanProcessor?: SpanProcessor;\n\n /**\n * Custom span exporters for traces (alternative to spanProcessors, supports multiple exporters)\n * Provide either spanProcessors OR spanExporters, not both\n * Each exporter will be wrapped in TailSamplingSpanProcessor + BatchSpanProcessor\n *\n * @example Multiple exporters\n * ```typescript\n * import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'\n * import { JaegerExporter } from '@opentelemetry/exporter-jaeger'\n *\n * init({\n * service: 'my-app',\n * spanExporters: [\n * new ZipkinExporter({ url: 'http://localhost:9411/api/v2/spans' }),\n * new JaegerExporter() // Send to multiple backends simultaneously\n * ]\n * })\n * ```\n *\n * @example Single exporter\n * ```typescript\n * import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'\n *\n * init({\n * service: 'my-app',\n * spanExporters: [new ZipkinExporter({ url: 'http://localhost:9411/api/v2/spans' })]\n * })\n * ```\n */\n spanExporters?: SpanExporter[];\n\n /**\n * Custom span exporter for traces (single-item alias of spanExporters)\n * @deprecated Use spanExporters for consistency with other multi-value config fields\n */\n spanExporter?: SpanExporter;\n\n /**\n * Custom metric readers (supports multiple readers)\n * Allows sending metrics to multiple backends: OTLP, Prometheus, custom readers\n * Defaults to OTLP metrics exporter when metrics are enabled.\n *\n * @example Multiple metric readers\n * ```typescript\n * import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'\n * import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'\n * import { PrometheusExporter } from '@opentelemetry/exporter-prometheus'\n *\n * init({\n * service: 'my-app',\n * metricReaders: [\n * new PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter() }),\n * new PrometheusExporter() // Export to multiple backends\n * ]\n * })\n * ```\n */\n metricReaders?: MetricReader[];\n\n /**\n * Custom metric reader (single-item alias of metricReaders)\n * @deprecated Use metricReaders for consistency with other multi-value config fields\n */\n metricReader?: MetricReader;\n\n /**\n * Custom log record processors. When omitted, logs are not configured.\n */\n logRecordProcessors?: LogRecordProcessor[];\n\n /**\n * Custom log record processor (single-item alias of logRecordProcessors)\n * @deprecated Use logRecordProcessors for consistency with other multi-value config fields\n */\n logRecordProcessor?: LogRecordProcessor;\n\n /**\n * PostHog integration - auto-configures OTLP log exporter.\n *\n * @example\n * ```typescript\n * init({\n * service: 'my-app',\n * posthog: { url: 'https://us.i.posthog.com/i/v1/logs?token=phc_xxx' }\n * });\n * ```\n *\n * Also reads from POSTHOG_LOGS_URL environment variable as fallback.\n */\n posthog?: { url: string };\n\n /** Additional resource attributes to merge with defaults. */\n resourceAttributes?: Attributes;\n\n /** Provide a fully custom Resource to merge (advanced use case). */\n resource?: Resource;\n\n /**\n * Headers for OTLP exporters. Accepts either an object map or\n * a \"key=value\" comma separated string.\n *\n * @example\n * ```typescript\n * init({\n * service: 'my-app',\n * endpoint: 'https://api.honeycomb.io',\n * headers: { 'x-honeycomb-team': 'YOUR_API_KEY' }\n * })\n * ```\n */\n headers?: Record<string, string> | string;\n\n /**\n * OTLP protocol to use for traces, metrics, and logs\n * - 'http': HTTP/protobuf (default, uses port 4318)\n * - 'grpc': gRPC (uses port 4317)\n *\n * Can be overridden with OTEL_EXPORTER_OTLP_PROTOCOL env var.\n *\n * Note: gRPC exporters are optional peer dependencies. Install them with:\n * ```bash\n * pnpm add @opentelemetry/exporter-trace-otlp-grpc @opentelemetry/exporter-metrics-otlp-grpc\n * ```\n *\n * @example HTTP (default)\n * ```typescript\n * init({\n * service: 'my-app',\n * protocol: 'http', // or omit (defaults to http)\n * endpoint: 'http://localhost:4318'\n * })\n * ```\n *\n * @example gRPC\n * ```typescript\n * init({\n * service: 'my-app',\n * protocol: 'grpc',\n * endpoint: 'grpc://localhost:4317'\n * })\n * ```\n *\n * @default 'http'\n */\n protocol?: 'http' | 'grpc';\n\n /**\n * Optional factory to build a customised NodeSDK instance from our defaults.\n */\n sdkFactory?: (defaults: Partial<NodeSDKConfiguration>) => NodeSDK;\n\n /**\n * Infrastructure metrics configuration\n * - true: always enabled (default)\n * - false: always disabled\n * - 'auto': always enabled (same as true)\n *\n * Can be overridden with AUTOTEL_METRICS=on|off env var\n */\n metrics?: boolean | 'auto';\n\n /**\n * OTLP logs configuration\n * - true: auto-configure OTLP log exporter from endpoint\n * - false: disabled (default)\n * - 'auto': same as false (opt-in only)\n *\n * When enabled and an endpoint is configured, autotel will automatically\n * create a BatchLogRecordProcessor with an OTLPLogExporter - no manual\n * imports needed. Works alongside logRecordProcessors (additive).\n *\n * Requires @opentelemetry/sdk-logs and @opentelemetry/exporter-logs-otlp-http\n * (or -grpc) as peer dependencies.\n *\n * Can be overridden with AUTOTEL_LOGS=on|off env var.\n *\n * @example\n * ```typescript\n * init({\n * service: 'my-app',\n * endpoint: 'http://localhost:4318',\n * logs: true,\n * });\n * ```\n */\n logs?: boolean | 'auto';\n\n /** Sampling strategy - takes precedence over `sampling` preset */\n sampler?: Sampler;\n\n /**\n * Sampling preset shorthand — resolves to a pre-configured sampler.\n * If both `sampler` and `sampling` are provided, `sampler` takes precedence.\n *\n * @default 'production' (10% baseline + always-on for errors/slow)\n *\n * **Footgun for one-shot scripts:** the default samples ~90% of spans away,\n * which means a fixture-capture script that emits a single normal span\n * almost always sees zero output. For local debugging and capture use:\n *\n * ```typescript\n * init({\n * service: 'fixture-capture',\n * spanProcessors: [new SimpleSpanProcessor(new InMemorySpanExporter())],\n * sampling: 'development', // capture EVERY span\n * });\n * ```\n *\n * Read `exporter.getFinishedSpans()` **before** calling `shutdown()` —\n * `InMemorySpanExporter.shutdown()` resets state.\n */\n sampling?: SamplingPreset;\n\n /** Service version (default: auto-detect from package.json or '1.0.0') */\n version?: string;\n\n /** Environment (default: process.env.NODE_ENV || 'development') */\n environment?: string;\n\n /**\n * Logger instance for internal autotel diagnostic messages\n *\n * This logger is used by autotel internally to log initialization, warnings,\n * and debug information. Any logger with info/warn/error/debug methods works.\n *\n * **For OTel instrumentation of your application logs**, use the `autoInstrumentations` option:\n * - `autoInstrumentations: ['pino']` - Injects traceId/spanId into Pino logs\n * - `autoInstrumentations: ['winston']` - Injects traceId/spanId into Winston logs\n *\n * Default: silent logger (no-op)\n *\n * @example Pino with OTel instrumentation\n * ```typescript\n * import pino from 'pino'\n * import { init } from 'autotel'\n *\n * const logger = pino({ level: 'info' })\n * init({\n * service: 'my-app',\n * logger, // For autotel's internal logs\n * autoInstrumentations: ['pino'] // For OTel trace context in YOUR logs\n * })\n * ```\n *\n * @example Custom logger for autotel diagnostics\n * ```typescript\n * const logger = {\n * info: (msg, extra) => console.log(msg, extra),\n * warn: (msg, extra) => console.warn(msg, extra),\n * error: (msg, err, extra) => console.error(msg, err, extra),\n * debug: (msg, extra) => console.debug(msg, extra),\n * }\n * init({ service: 'my-app', logger })\n * ```\n */\n logger?: Logger;\n\n /**\n * Flush events queue when root spans end\n * - true: Flush on root span completion (default)\n * - false: Use batching (events flush every 10 seconds automatically)\n *\n * Only flushes on root spans to avoid excessive network calls.\n * Default is true for serverless/short-lived processes. Set to false\n * for long-running services where batching is more efficient.\n */\n flushOnRootSpanEnd?: boolean;\n\n /**\n * Force-flush OpenTelemetry spans on shutdown (default: false)\n *\n * When enabled, spans are force-flushed along with events on root\n * span completion. This is useful for serverless/short-lived processes where\n * spans may not export before the process ends.\n *\n * - true: Force-flush spans on root span completion (~50-200ms latency)\n * - false: Spans export via normal batch processor (default behavior)\n *\n * Only applies when flushOnRootSpanEnd is also enabled.\n *\n * Note: For edge runtimes (Cloudflare Workers, Vercel Edge), use the\n * 'autotel-edge' package instead, which handles this automatically.\n *\n * @example Serverless with force-flush\n * ```typescript\n * init({\n * service: 'my-lambda',\n * flushOnRootSpanEnd: true,\n * forceFlushOnShutdown: true, // Force-flush spans\n * });\n * ```\n */\n forceFlushOnShutdown?: boolean;\n\n /**\n * Automatically copy baggage entries to span attributes\n *\n * When enabled, all baggage entries are automatically added as span attributes,\n * making them visible in trace UIs (Jaeger, Grafana, DataDog, etc.) without\n * manually calling ctx.setAttribute() for each entry.\n *\n * - `true`: adds baggage with 'baggage.' prefix (e.g. baggage.tenant.id)\n * - `string`: uses custom prefix (e.g. 'ctx' → ctx.tenant.id, '' → tenant.id)\n * - `false` or omit: disabled (default)\n *\n * @default false\n *\n * @example Enable with default prefix\n * ```typescript\n * init({\n * service: 'my-app',\n * baggage: true\n * });\n *\n * // Now baggage automatically appears as span attributes\n * await withBaggage({\n * baggage: { 'tenant.id': 't1', 'user.id': 'u1' },\n * fn: async () => {\n * // Span has baggage.tenant.id and baggage.user.id attributes!\n * }\n * });\n * ```\n *\n * @example Custom prefix\n * ```typescript\n * init({\n * service: 'my-app',\n * baggage: 'ctx' // Uses 'ctx.' prefix\n * });\n * // Creates attributes: ctx.tenant.id, ctx.user.id\n * ```\n *\n * @example No prefix\n * ```typescript\n * init({\n * service: 'my-app',\n * baggage: '' // No prefix\n * });\n * // Creates attributes: tenant.id, user.id\n * ```\n */\n baggage?: boolean | string;\n\n /**\n * Validation configuration for events events\n * - Override default sensitive field patterns for redaction\n * - Customize max lengths, nesting depth, etc.\n *\n * @example Disable redaction for development\n * ```typescript\n * init({\n * service: 'my-app',\n * validation: {\n * sensitivePatterns: [] // Disable all redaction\n * }\n * })\n * ```\n *\n * @example Add custom patterns\n * ```typescript\n * init({\n * service: 'my-app',\n * validation: {\n * sensitivePatterns: [\n * /password/i,\n * /apiKey/i,\n * /customSecret/i // Your custom pattern\n * ]\n * }\n * })\n * ```\n */\n validation?: Partial<ValidationConfig>;\n\n /**\n * Events configuration for trace context, correlation IDs, and enrichment\n *\n * Controls how product events integrate with distributed tracing:\n * - `includeTraceContext`: Automatically include trace context in events\n * - `includeLinkedTraceIds`: Include full array of linked trace IDs (for batch/fan-in)\n * - `traceUrl`: Generate clickable trace URLs in events\n * - `enrichFromBaggage`: Auto-enrich events from baggage with guardrails\n *\n * @example Basic trace context\n * ```typescript\n * init({\n * service: 'my-app',\n * events: {\n * includeTraceContext: true\n * }\n * });\n * // Events now include autotel.trace_id, autotel.span_id, autotel.correlation_id\n * ```\n *\n * @example With clickable trace URLs\n * ```typescript\n * init({\n * service: 'my-app',\n * events: {\n * includeTraceContext: true,\n * traceUrl: (ctx) => `https://grafana.internal/explore?traceId=${ctx.traceId}`\n * }\n * });\n * ```\n *\n * @example With baggage enrichment\n * ```typescript\n * init({\n * service: 'my-app',\n * events: {\n * includeTraceContext: true,\n * enrichFromBaggage: {\n * allow: ['tenant.id', 'user.id'],\n * prefix: 'ctx.',\n * maxKeys: 10,\n * maxBytes: 1024\n * }\n * }\n * });\n * ```\n */\n events?: EventsConfig;\n\n /**\n * Debug mode for local span inspection.\n * Enables console output to help you see spans as they're created.\n *\n * - `true`: Raw JSON output (ConsoleSpanExporter)\n * - `'pretty'`: Colorized, hierarchical output (PrettyConsoleExporter)\n * - `false`/undefined: No console output (default)\n *\n * When enabled: Outputs spans to console AND sends to backend (if endpoint/exporter configured)\n *\n * Perfect for progressive development:\n * - Start with debug: 'pretty' (no endpoint) → see traces immediately with nice formatting\n * - Add endpoint later → console + backend, verify before choosing provider\n * - Remove debug in production → backend only, clean production config\n *\n * Can be overridden with AUTOTEL_DEBUG environment variable.\n *\n * @example Pretty debug output (recommended for development)\n * ```typescript\n * init({\n * service: 'my-app',\n * debug: 'pretty' // Colorized, hierarchical output\n * })\n * ```\n *\n * @example Raw JSON output (verbose)\n * ```typescript\n * init({\n * service: 'my-app',\n * debug: true // Raw ConsoleSpanExporter output\n * })\n * ```\n *\n * @example Environment variable\n * ```bash\n * AUTOTEL_DEBUG=pretty node server.js\n * AUTOTEL_DEBUG=true node server.js\n * ```\n */\n debug?: boolean | 'pretty';\n\n /**\n * Filter predicate to drop unwanted spans before processing.\n *\n * Useful for filtering out noisy spans from specific instrumentations\n * (e.g., Next.js internal spans, health check endpoints).\n *\n * The filter runs on completed spans (onEnd), so you have access to:\n * - `span.name` - Span name\n * - `span.attributes` - All span attributes\n * - `span.instrumentationScope` - `{ name, version }` of the instrumentation\n * - `span.status` - Span status code and message\n * - `span.duration` - Span duration as `[seconds, nanoseconds]`\n *\n * Return `true` to keep the span, `false` to drop it.\n *\n * @example Filter out Next.js instrumentation spans\n * ```typescript\n * init({\n * service: 'my-app',\n * spanFilter: (span) => span.instrumentationScope.name !== 'next.js'\n * })\n * ```\n *\n * @example Filter out health check spans\n * ```typescript\n * init({\n * service: 'my-app',\n * spanFilter: (span) => !span.name.includes('/health')\n * })\n * ```\n *\n * @example Complex filtering (multiple conditions)\n * ```typescript\n * init({\n * service: 'my-app',\n * spanFilter: (span) => {\n * // Drop Next.js internal spans\n * if (span.instrumentationScope.name === 'next.js') return false;\n * // Drop health checks\n * if (span.name.includes('/health')) return false;\n * // Drop very short spans (less than 1ms)\n * const [secs, nanos] = span.duration;\n * if (secs === 0 && nanos < 1_000_000) return false;\n * return true;\n * }\n * })\n * ```\n */\n spanFilter?: SpanFilterPredicate;\n\n /**\n * Norm