/** * Span attributes */ export type SpanAttributes = Record; /** * Span status */ export enum SpanStatus { OK = 'OK', ERROR = 'ERROR', UNSET = 'UNSET', } /** * Span context for distributed tracing */ export interface SpanContext { traceId: string; spanId: string; traceFlags: number; traceState?: string; } /** * Span interface */ export interface Span { /** Span name */ name: string; /** Span start time */ startTime: Date; /** Span end time (if ended) */ endTime?: Date; /** Span status */ status: SpanStatus; /** Span attributes */ attributes: SpanAttributes; /** Span context */ context: SpanContext; /** Parent span ID */ parentSpanId?: string; /** Set an attribute */ setAttribute(key: string, value: string | number | boolean): void; /** Set multiple attributes */ setAttributes(attributes: SpanAttributes): void; /** Set span status */ setStatus(status: SpanStatus, message?: string): void; /** Add an event to the span */ addEvent(name: string, attributes?: SpanAttributes): void; /** End the span */ end(): void; /** Record an exception */ recordException(exception: Error): void; } /** * Tracer interface */ export interface Tracer { /** Start a new span */ startSpan(name: string, attributes?: SpanAttributes): Span; /** Get the active span */ getActiveSpan(): Span | null; /** Execute function within span context */ withSpan(name: string, fn: (span: Span) => Promise): Promise; } /** * Generate a random trace ID */ function generateTraceId(): string { return Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16)).join(''); } /** * Generate a random span ID */ function generateSpanId(): string { return Array.from({ length: 16 }, () => Math.floor(Math.random() * 16).toString(16)).join(''); } /** * Basic in-memory span implementation */ export class InMemorySpan implements Span { public name: string; public startTime: Date; public endTime?: Date; public status: SpanStatus = SpanStatus.UNSET; public attributes: SpanAttributes; public context: SpanContext; public parentSpanId?: string; public events: Array<{ name: string; timestamp: Date; attributes?: SpanAttributes }> = []; constructor( name: string, attributes: SpanAttributes = {}, parentContext?: SpanContext ) { this.name = name; this.startTime = new Date(); this.attributes = { ...attributes }; if (parentContext) { this.context = { traceId: parentContext.traceId, spanId: generateSpanId(), traceFlags: parentContext.traceFlags, traceState: parentContext.traceState, }; this.parentSpanId = parentContext.spanId; } else { this.context = { traceId: generateTraceId(), spanId: generateSpanId(), traceFlags: 1, }; } } setAttribute(key: string, value: string | number | boolean): void { this.attributes[key] = value; } setAttributes(attributes: SpanAttributes): void { Object.assign(this.attributes, attributes); } setStatus(status: SpanStatus, message?: string): void { this.status = status; if (message) { this.setAttribute('status.message', message); } } addEvent(name: string, attributes?: SpanAttributes): void { this.events.push({ name, timestamp: new Date(), attributes, }); } recordException(exception: Error): void { this.setStatus(SpanStatus.ERROR); this.setAttribute('exception.type', exception.name); this.setAttribute('exception.message', exception.message); if (exception.stack) { this.setAttribute('exception.stacktrace', exception.stack); } } end(): void { if (!this.endTime) { this.endTime = new Date(); if (this.status === SpanStatus.UNSET) { this.status = SpanStatus.OK; } } } /** * Get duration in milliseconds */ getDuration(): number | null { if (!this.endTime) return null; return this.endTime.getTime() - this.startTime.getTime(); } /** * Convert span to JSON */ toJSON(): Record { return { name: this.name, traceId: this.context.traceId, spanId: this.context.spanId, parentSpanId: this.parentSpanId, startTime: this.startTime.toISOString(), endTime: this.endTime?.toISOString(), duration: this.getDuration(), status: this.status, attributes: this.attributes, events: this.events, }; } } /** * Span exporter interface */ export interface SpanExporter { export(spans: Span[]): void | Promise; } /** * Console span exporter for debugging */ export class ConsoleSpanExporter implements SpanExporter { export(spans: Span[]): void { for (const span of spans) { if (span instanceof InMemorySpan) { console.log('[TRACE]', JSON.stringify(span.toJSON(), null, 2)); } } } } /** * In-memory tracer implementation */ export class InMemoryTracer implements Tracer { private activeSpan: Span | null = null; private spans: Span[] = []; private exporter: SpanExporter; constructor(exporter: SpanExporter = new ConsoleSpanExporter()) { this.exporter = exporter; } startSpan(name: string, attributes?: SpanAttributes): Span { const parentContext = this.activeSpan?.context; const span = new InMemorySpan(name, attributes, parentContext); this.spans.push(span); return span; } getActiveSpan(): Span | null { return this.activeSpan; } async withSpan(name: string, fn: (span: Span) => Promise): Promise { const span = this.startSpan(name); const previousActiveSpan = this.activeSpan; this.activeSpan = span; try { const result = await fn(span); span.setStatus(SpanStatus.OK); return result; } catch (error) { span.recordException(error as Error); throw error; } finally { span.end(); this.activeSpan = previousActiveSpan; this.exportSpan(span); } } private exportSpan(span: Span): void { try { const result = this.exporter.export([span]); if (result && typeof result.then === 'function') { result.catch((err) => { console.error('Span export error:', err); }); } } catch (err) { console.error('Span export error:', err); } } /** * Get all recorded spans */ getSpans(): Span[] { return [...this.spans]; } /** * Clear all spans */ clear(): void { this.spans = []; } /** * Create context carrier for propagation */ inject(carrier: Record): void { if (this.activeSpan) { const { traceId, spanId, traceFlags } = this.activeSpan.context; carrier['traceparent'] = `00-${traceId}-${spanId}-${traceFlags.toString(16).padStart(2, '0')}`; } } /** * Extract context from carrier */ extract(carrier: Record): SpanContext | null { const traceparent = carrier['traceparent']; if (!traceparent) return null; const parts = traceparent.split('-'); if (parts.length !== 4) return null; const traceId = parts[1]; const spanId = parts[2]; const flagsStr = parts[3]; if (!traceId || !spanId || !flagsStr) return null; return { traceId, spanId, traceFlags: parseInt(flagsStr, 16), }; } } /** * No-op tracer for when tracing is disabled */ class NoOpSpan implements Span { name = ''; startTime = new Date(); status = SpanStatus.UNSET; attributes = {}; context: SpanContext = { traceId: '00000000000000000000000000000000', spanId: '0000000000000000', traceFlags: 0, }; setAttribute(): void {} setAttributes(): void {} setStatus(): void {} addEvent(): void {} end(): void {} recordException(): void {} } class NoOpTracer implements Tracer { private noOpSpan = new NoOpSpan(); startSpan(): Span { return this.noOpSpan; } getActiveSpan(): Span | null { return null; } async withSpan(_name: string, fn: (span: Span) => Promise): Promise { return fn(this.noOpSpan); } } /** * Default tracer instance */ export const defaultTracer: Tracer = new InMemoryTracer(); /** * No-op tracer instance (for performance-sensitive scenarios) */ export const noOpTracer: Tracer = new NoOpTracer(); /** * Tracing provider configuration */ export interface TracingConfig { enabled: boolean; exporter?: SpanExporter; serviceName?: string; serviceVersion?: string; } /** * Tracing provider */ export class TracingProvider { private tracer: Tracer; private config: TracingConfig; constructor(config: Partial = {}) { this.config = { enabled: config.enabled !== false, exporter: config.exporter, serviceName: config.serviceName || 'elasticsearch-client', serviceVersion: config.serviceVersion, }; this.tracer = this.config.enabled ? new InMemoryTracer(this.config.exporter) : noOpTracer; } getTracer(): Tracer { return this.tracer; } isEnabled(): boolean { return this.config.enabled; } createSpan(name: string, attributes?: SpanAttributes): Span { const span = this.tracer.startSpan(name, { ...attributes, 'service.name': this.config.serviceName, ...(this.config.serviceVersion && { 'service.version': this.config.serviceVersion }), }); return span; } async withSpan(name: string, fn: (span: Span) => Promise): Promise { return this.tracer.withSpan(name, fn); } propagateContext(carrier: Record): void { if (this.tracer instanceof InMemoryTracer) { this.tracer.inject(carrier); } } extractContext(carrier: Record): SpanContext | null { if (this.tracer instanceof InMemoryTracer) { return this.tracer.extract(carrier); } return null; } } /** * Default tracing provider */ export const defaultTracingProvider = new TracingProvider();