import type { Context, Span, Tracer } from "@opentelemetry/api"; import { context, propagation, SpanKind, SpanStatusCode, trace, } from "@opentelemetry/api"; import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; import { resourceFromAttributes } from "@opentelemetry/resources"; import { BatchSpanProcessor, ConsoleSpanExporter, ParentBasedSampler, SimpleSpanProcessor, TraceIdRatioBasedSampler, } from "@opentelemetry/sdk-trace-base"; import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node"; import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, } from "@opentelemetry/semantic-conventions"; import serverConfig from "@karakeep/shared/config"; import logger from "@karakeep/shared/logger"; import type { TracingAttributes } from "./tracingTypes"; export type { TracingAttributeKey, TracingAttributes } from "./tracingTypes"; let tracerProvider: NodeTracerProvider | null = null; let isInitialized = false; /** * Initialize the OpenTelemetry tracing infrastructure. * Should be called once at application startup. */ export function initTracing(serviceSuffix?: string): void { if (isInitialized) { logger.debug("Tracing already initialized, skipping"); return; } if (!serverConfig.tracing.enabled) { logger.info("Tracing is disabled"); isInitialized = true; return; } const serviceName = serviceSuffix ? `${serverConfig.tracing.serviceName}-${serviceSuffix}` : serverConfig.tracing.serviceName; logger.info(`Initializing OpenTelemetry tracing for service: ${serviceName}`); const resource = resourceFromAttributes({ [ATTR_SERVICE_NAME]: serviceName, [ATTR_SERVICE_VERSION]: serverConfig.serverVersion ?? "unknown", }); // Configure span processors const spanProcessors = []; if (serverConfig.tracing.otlpEndpoint) { // OTLP exporter (Jaeger, Zipkin, etc.) const otlpExporter = new OTLPTraceExporter({ url: serverConfig.tracing.otlpEndpoint, }); spanProcessors.push(new BatchSpanProcessor(otlpExporter)); logger.info( `OTLP exporter configured: ${serverConfig.tracing.otlpEndpoint}`, ); } else { // Fallback to console exporter for development spanProcessors.push(new SimpleSpanProcessor(new ConsoleSpanExporter())); logger.info("Console span exporter configured (no OTLP endpoint set)"); } tracerProvider = new NodeTracerProvider({ resource, sampler: new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(serverConfig.tracing.sampleRate), }), spanProcessors, }); // Register the provider globally tracerProvider.register(); isInitialized = true; logger.info("OpenTelemetry tracing initialized successfully"); } /** * Shutdown the tracing infrastructure gracefully. * Should be called on application shutdown. */ export async function shutdownTracing(): Promise { if (tracerProvider) { await tracerProvider.shutdown(); logger.info("OpenTelemetry tracing shut down"); } } /** * Get a tracer instance for creating spans. * @param name - The name of the tracer (typically the module/component name) */ export function getTracer(name: string): Tracer { return trace.getTracer(name); } /** * Get the currently active span, if any. */ export function getActiveSpan(): Span | undefined { return trace.getActiveSpan(); } /** * Get the current trace context. */ export function getActiveContext(): Context { return context.active(); } /** * Execute a function within a new span. * Automatically handles error recording and span status. */ export async function withSpan( tracer: Tracer, spanName: string, options: { kind?: SpanKind; attributes?: TracingAttributes; }, fn: (span: Span) => Promise, ): Promise { return tracer.startActiveSpan( spanName, { kind: options.kind ?? SpanKind.INTERNAL, attributes: options.attributes, }, async (span) => { try { const result = await fn(span); span.setStatus({ code: SpanStatusCode.OK }); return result; } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error), }); span.recordException( error instanceof Error ? error : new Error(String(error)), ); throw error; } finally { span.end(); } }, ); } /** * Execute a synchronous function within a new span. */ export function withSpanSync( tracer: Tracer, spanName: string, options: { kind?: SpanKind; attributes?: TracingAttributes; }, fn: (span: Span) => T, ): T { const span = tracer.startSpan(spanName, { kind: options.kind ?? SpanKind.INTERNAL, attributes: options.attributes, }); try { const result = context.with(trace.setSpan(context.active(), span), () => fn(span), ); span.setStatus({ code: SpanStatusCode.OK }); return result; } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error), }); span.recordException( error instanceof Error ? error : new Error(String(error)), ); throw error; } finally { span.end(); } } /** * Add an event to the current active span. */ export function addSpanEvent( name: string, attributes?: Record, ): void { const span = getActiveSpan(); if (span) { span.addEvent(name, attributes); } } /** * Set attributes on the current active span. */ export function setSpanAttributes(attributes: TracingAttributes): void { const span = getActiveSpan(); if (span) { span.setAttributes(attributes); } } /** * Record an error on the current active span. */ export function recordSpanError(error: Error): void { const span = getActiveSpan(); if (span) { span.recordException(error); span.setStatus({ code: SpanStatusCode.ERROR, message: error.message, }); } } /** * Extract trace context from HTTP headers (for distributed tracing). */ export function extractTraceContext( headers: Record, ): Context { const normalizedHeaders: Record = {}; for (const [key, value] of Object.entries(headers)) { if (value) { normalizedHeaders[key] = Array.isArray(value) ? value[0] : value; } } return propagation.extract(context.active(), normalizedHeaders); } /** * Inject trace context into HTTP headers (for distributed tracing). */ export function injectTraceContext( headers: Record, ): Record { propagation.inject(context.active(), headers); return headers; } /** * Run a function within a specific context. */ export function runWithContext(ctx: Context, fn: () => T): T { return context.with(ctx, fn); } // Re-export commonly used types and constants export { SpanKind, SpanStatusCode } from "@opentelemetry/api";