BREAKING CHANGE(core): Refactor to v3: introduce modular core/domain architecture, plugin system, observability and strict TypeScript configuration; remove legacy classes

This commit is contained in:
2025-11-29 18:32:00 +00:00
parent 53673e37cb
commit 7e89b6ebf5
68 changed files with 17020 additions and 720 deletions

View File

@@ -0,0 +1,14 @@
/**
* Observability layer for Elasticsearch client
*
* This module provides:
* - Structured logging with context and correlation
* - Prometheus-compatible metrics collection
* - Distributed tracing with OpenTelemetry-compatible API
*
* @packageDocumentation
*/
export * from './logger.js';
export * from './metrics.js';
export * from './tracing.js';

View File

@@ -0,0 +1,281 @@
/**
* Log levels in order of severity
*/
export enum LogLevel {
DEBUG = 'debug',
INFO = 'info',
WARN = 'warn',
ERROR = 'error',
}
/**
* Log level priorities for filtering
*/
const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
[LogLevel.DEBUG]: 0,
[LogLevel.INFO]: 1,
[LogLevel.WARN]: 2,
[LogLevel.ERROR]: 3,
};
/**
* Structured log entry
*/
export interface LogEntry {
level: LogLevel;
message: string;
timestamp: Date;
context?: Record<string, unknown>;
correlationId?: string;
error?: Error;
}
/**
* Log transport interface for custom log handlers
*/
export interface LogTransport {
log(entry: LogEntry): void | Promise<void>;
}
/**
* Console transport with colored output
*/
export class ConsoleTransport implements LogTransport {
private readonly colors = {
debug: '\x1b[36m', // Cyan
info: '\x1b[32m', // Green
warn: '\x1b[33m', // Yellow
error: '\x1b[31m', // Red
reset: '\x1b[0m',
};
log(entry: LogEntry): void {
const color = this.colors[entry.level];
const reset = this.colors.reset;
const timestamp = entry.timestamp.toISOString();
const level = entry.level.toUpperCase().padEnd(5);
let message = `${color}[${timestamp}] ${level}${reset} ${entry.message}`;
if (entry.correlationId) {
message += ` ${color}[correlation: ${entry.correlationId}]${reset}`;
}
if (entry.context && Object.keys(entry.context).length > 0) {
message += `\n Context: ${JSON.stringify(entry.context, null, 2)}`;
}
if (entry.error) {
message += `\n Error: ${entry.error.message}`;
if (entry.error.stack) {
message += `\n${entry.error.stack}`;
}
}
console.log(message);
}
}
/**
* JSON transport for structured logging
*/
export class JsonTransport implements LogTransport {
log(entry: LogEntry): void {
const jsonEntry = {
level: entry.level,
message: entry.message,
timestamp: entry.timestamp.toISOString(),
...(entry.correlationId && { correlationId: entry.correlationId }),
...(entry.context && { context: entry.context }),
...(entry.error && {
error: {
message: entry.error.message,
name: entry.error.name,
stack: entry.error.stack,
},
}),
};
console.log(JSON.stringify(jsonEntry));
}
}
/**
* Logger configuration
*/
export interface LoggerConfig {
/** Minimum log level to output */
level: LogLevel;
/** Log transports */
transports: LogTransport[];
/** Default context to include in all logs */
defaultContext?: Record<string, unknown>;
/** Whether to include timestamp */
includeTimestamp?: boolean;
}
/**
* Structured logger with context and correlation support
*
* @example
* ```typescript
* const logger = new Logger({
* level: LogLevel.INFO,
* transports: [new ConsoleTransport()],
* defaultContext: { service: 'elasticsearch-client' }
* });
*
* logger.info('Connected to Elasticsearch', { node: 'localhost:9200' });
*
* const childLogger = logger.withContext({ operation: 'bulk-index' });
* childLogger.debug('Processing batch', { size: 1000 });
* ```
*/
export class Logger {
private config: LoggerConfig;
private context: Record<string, unknown>;
private correlationId?: string;
constructor(config: Partial<LoggerConfig> = {}) {
this.config = {
level: config.level || LogLevel.INFO,
transports: config.transports || [new ConsoleTransport()],
defaultContext: config.defaultContext || {},
includeTimestamp: config.includeTimestamp !== false,
};
this.context = { ...this.config.defaultContext };
}
/**
* Create a child logger with additional context
*/
withContext(context: Record<string, unknown>): Logger {
const child = new Logger(this.config);
child.context = { ...this.context, ...context };
child.correlationId = this.correlationId;
return child;
}
/**
* Create a child logger with correlation ID
*/
withCorrelation(correlationId: string): Logger {
const child = new Logger(this.config);
child.context = { ...this.context };
child.correlationId = correlationId;
return child;
}
/**
* Create a child logger for a specific namespace
*/
child(namespace: string): Logger {
return this.withContext({ namespace });
}
/**
* Log at DEBUG level
*/
debug(message: string, meta?: Record<string, unknown>): void {
this.log(LogLevel.DEBUG, message, meta);
}
/**
* Log at INFO level
*/
info(message: string, meta?: Record<string, unknown>): void {
this.log(LogLevel.INFO, message, meta);
}
/**
* Log at WARN level
*/
warn(message: string, meta?: Record<string, unknown>): void {
this.log(LogLevel.WARN, message, meta);
}
/**
* Log at ERROR level
*/
error(message: string, error?: Error, meta?: Record<string, unknown>): void {
this.log(LogLevel.ERROR, message, meta, error);
}
/**
* Internal log method
*/
private log(
level: LogLevel,
message: string,
meta?: Record<string, unknown>,
error?: Error
): void {
// Check if we should log this level
if (!this.shouldLog(level)) {
return;
}
const entry: LogEntry = {
level,
message,
timestamp: new Date(),
context: { ...this.context, ...meta },
...(this.correlationId && { correlationId: this.correlationId }),
...(error && { error }),
};
// Send to all transports
for (const transport of this.config.transports) {
try {
const result = transport.log(entry);
// Handle async transports
if (result && typeof result.then === 'function') {
result.catch((err) => {
console.error('Transport error:', err);
});
}
} catch (err) {
console.error('Transport error:', err);
}
}
}
/**
* Check if a log level should be output
*/
private shouldLog(level: LogLevel): boolean {
return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[this.config.level];
}
/**
* Update logger configuration
*/
setLevel(level: LogLevel): void {
this.config.level = level;
}
/**
* Add a transport
*/
addTransport(transport: LogTransport): void {
this.config.transports.push(transport);
}
/**
* Get current log level
*/
getLevel(): LogLevel {
return this.config.level;
}
}
/**
* Default logger instance
*/
export const defaultLogger = new Logger({
level: LogLevel.INFO,
transports: [new ConsoleTransport()],
});

View File

@@ -0,0 +1,543 @@
/**
* Label map for metrics
*/
export type Labels = Record<string, string | number>;
/**
* Metric types
*/
export enum MetricType {
COUNTER = 'counter',
GAUGE = 'gauge',
HISTOGRAM = 'histogram',
}
/**
* Histogram bucket configuration
*/
export interface HistogramBuckets {
buckets: number[];
counts: Map<number, number>;
sum: number;
count: number;
}
/**
* Base metric class
*/
abstract class Metric {
constructor(
public readonly name: string,
public readonly type: MetricType,
public readonly help: string,
public readonly labels: string[] = []
) {}
abstract getValue(labels?: Labels): number | HistogramBuckets;
abstract reset(): void;
}
/**
* Counter metric - monotonically increasing value
*
* @example
* ```typescript
* const requestCounter = new Counter('http_requests_total', 'Total HTTP requests', ['method', 'status']);
* requestCounter.inc({ method: 'GET', status: '200' });
* requestCounter.inc({ method: 'POST', status: '201' }, 5);
* ```
*/
export class Counter extends Metric {
private values: Map<string, number> = new Map();
constructor(name: string, help: string, labels: string[] = []) {
super(name, MetricType.COUNTER, help, labels);
}
/**
* Increment counter
*/
inc(labels: Labels = {}, value: number = 1): void {
if (value < 0) {
throw new Error('Counter can only be incremented with positive values');
}
const key = this.getKey(labels);
const current = this.values.get(key) || 0;
this.values.set(key, current + value);
}
/**
* Get current value
*/
getValue(labels: Labels = {}): number {
const key = this.getKey(labels);
return this.values.get(key) || 0;
}
/**
* Reset counter
*/
reset(): void {
this.values.clear();
}
/**
* Get all values with labels
*/
getAll(): Array<{ labels: Labels; value: number }> {
const results: Array<{ labels: Labels; value: number }> = [];
for (const [key, value] of this.values.entries()) {
const labels = this.parseKey(key);
results.push({ labels, value });
}
return results;
}
private getKey(labels: Labels): string {
const labelStr = this.labels
.map((label) => `${label}=${labels[label] || ''}`)
.join(',');
return labelStr || 'default';
}
private parseKey(key: string): Labels {
if (key === 'default') return {};
const labels: Labels = {};
const pairs = key.split(',');
for (const pair of pairs) {
const [name, value] = pair.split('=');
if (name && value !== undefined) {
labels[name] = value;
}
}
return labels;
}
}
/**
* Gauge metric - value that can go up and down
*
* @example
* ```typescript
* const activeConnections = new Gauge('active_connections', 'Number of active connections');
* activeConnections.set(42);
* activeConnections.inc();
* activeConnections.dec(5);
* ```
*/
export class Gauge extends Metric {
private values: Map<string, number> = new Map();
constructor(name: string, help: string, labels: string[] = []) {
super(name, MetricType.GAUGE, help, labels);
}
/**
* Set gauge to a specific value
*/
set(value: number, labels: Labels = {}): void {
const key = this.getKey(labels);
this.values.set(key, value);
}
/**
* Increment gauge
*/
inc(labels: Labels = {}, value: number = 1): void {
const key = this.getKey(labels);
const current = this.values.get(key) || 0;
this.values.set(key, current + value);
}
/**
* Decrement gauge
*/
dec(labels: Labels = {}, value: number = 1): void {
this.inc(labels, -value);
}
/**
* Get current value
*/
getValue(labels: Labels = {}): number {
const key = this.getKey(labels);
return this.values.get(key) || 0;
}
/**
* Reset gauge
*/
reset(): void {
this.values.clear();
}
/**
* Get all values with labels
*/
getAll(): Array<{ labels: Labels; value: number }> {
const results: Array<{ labels: Labels; value: number }> = [];
for (const [key, value] of this.values.entries()) {
const labels = this.parseKey(key);
results.push({ labels, value });
}
return results;
}
private getKey(labels: Labels): string {
const labelStr = this.labels
.map((label) => `${label}=${labels[label] || ''}`)
.join(',');
return labelStr || 'default';
}
private parseKey(key: string): Labels {
if (key === 'default') return {};
const labels: Labels = {};
const pairs = key.split(',');
for (const pair of pairs) {
const [name, value] = pair.split('=');
if (name && value !== undefined) {
labels[name] = value;
}
}
return labels;
}
}
/**
* Histogram metric - tracks distribution of values
*
* @example
* ```typescript
* const latency = new Histogram('request_duration_seconds', 'Request latency', ['endpoint'], [0.1, 0.5, 1, 2, 5]);
* latency.observe(0.234, { endpoint: '/api/users' });
* latency.observe(1.567, { endpoint: '/api/users' });
* ```
*/
export class Histogram extends Metric {
private buckets: number[];
private values: Map<string, HistogramBuckets> = new Map();
constructor(
name: string,
help: string,
labels: string[] = [],
buckets: number[] = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
) {
super(name, MetricType.HISTOGRAM, help, labels);
this.buckets = [...buckets].sort((a, b) => a - b);
}
/**
* Observe a value
*/
observe(value: number, labels: Labels = {}): void {
const key = this.getKey(labels);
let bucketData = this.values.get(key);
if (!bucketData) {
bucketData = {
buckets: this.buckets,
counts: new Map(this.buckets.map((b) => [b, 0])),
sum: 0,
count: 0,
};
this.values.set(key, bucketData);
}
// Update bucket counts
for (const bucket of this.buckets) {
if (value <= bucket) {
const current = bucketData.counts.get(bucket) || 0;
bucketData.counts.set(bucket, current + 1);
}
}
bucketData.sum += value;
bucketData.count++;
}
/**
* Get histogram data
*/
getValue(labels: Labels = {}): HistogramBuckets {
const key = this.getKey(labels);
return (
this.values.get(key) || {
buckets: this.buckets,
counts: new Map(this.buckets.map((b) => [b, 0])),
sum: 0,
count: 0,
}
);
}
/**
* Reset histogram
*/
reset(): void {
this.values.clear();
}
/**
* Get all histogram data with labels
*/
getAll(): Array<{ labels: Labels; value: HistogramBuckets }> {
const results: Array<{ labels: Labels; value: HistogramBuckets }> = [];
for (const [key, value] of this.values.entries()) {
const labels = this.parseKey(key);
results.push({ labels, value });
}
return results;
}
private getKey(labels: Labels): string {
const labelStr = this.labels
.map((label) => `${label}=${labels[label] || ''}`)
.join(',');
return labelStr || 'default';
}
private parseKey(key: string): Labels {
if (key === 'default') return {};
const labels: Labels = {};
const pairs = key.split(',');
for (const pair of pairs) {
const [name, value] = pair.split('=');
if (name && value !== undefined) {
labels[name] = value;
}
}
return labels;
}
}
/**
* Metrics registry
*/
export class MetricsRegistry {
private metrics: Map<string, Metric> = new Map();
/**
* Register a metric
*/
register(metric: Metric): void {
if (this.metrics.has(metric.name)) {
throw new Error(`Metric ${metric.name} already registered`);
}
this.metrics.set(metric.name, metric);
}
/**
* Get a metric by name
*/
get(name: string): Metric | undefined {
return this.metrics.get(name);
}
/**
* Get all metrics
*/
getAll(): Metric[] {
return Array.from(this.metrics.values());
}
/**
* Clear all metrics
*/
clear(): void {
this.metrics.clear();
}
/**
* Reset all metric values
*/
reset(): void {
for (const metric of this.metrics.values()) {
metric.reset();
}
}
/**
* Export metrics in Prometheus text format
*/
export(): string {
const lines: string[] = [];
for (const metric of this.metrics.values()) {
// Add help text
lines.push(`# HELP ${metric.name} ${metric.help}`);
lines.push(`# TYPE ${metric.name} ${metric.type}`);
if (metric instanceof Counter || metric instanceof Gauge) {
const all = metric.getAll();
for (const { labels, value } of all) {
const labelStr = Object.entries(labels)
.map(([k, v]) => `${k}="${v}"`)
.join(',');
const metricLine = labelStr
? `${metric.name}{${labelStr}} ${value}`
: `${metric.name} ${value}`;
lines.push(metricLine);
}
} else if (metric instanceof Histogram) {
const all = metric.getAll();
for (const { labels, value } of all) {
const labelStr = Object.entries(labels)
.map(([k, v]) => `${k}="${v}"`)
.join(',');
const labelPrefix = labelStr ? `{${labelStr}}` : '';
// Export bucket counts
for (const [bucket, count] of value.counts.entries()) {
const bucketLabel = labelStr
? `{${labelStr},le="${bucket}"}`
: `{le="${bucket}"}`;
lines.push(`${metric.name}_bucket${bucketLabel} ${count}`);
}
// Export +Inf bucket
const infLabel = labelStr ? `{${labelStr},le="+Inf"}` : `{le="+Inf"}`;
lines.push(`${metric.name}_bucket${infLabel} ${value.count}`);
// Export sum and count
lines.push(`${metric.name}_sum${labelPrefix} ${value.sum}`);
lines.push(`${metric.name}_count${labelPrefix} ${value.count}`);
}
}
lines.push(''); // Empty line between metrics
}
return lines.join('\n');
}
}
/**
* Default metrics registry
*/
export const defaultRegistry = new MetricsRegistry();
/**
* Metrics collector for Elasticsearch client
*/
export class MetricsCollector {
public readonly registry: MetricsRegistry;
// Standard metrics
public readonly requestsTotal: Counter;
public readonly requestDuration: Histogram;
public readonly requestErrors: Counter;
public readonly activeConnections: Gauge;
public readonly bulkOperations: Counter;
public readonly bulkDocuments: Counter;
public readonly retries: Counter;
constructor(registry: MetricsRegistry = defaultRegistry) {
this.registry = registry;
// Initialize standard metrics
this.requestsTotal = new Counter(
'elasticsearch_requests_total',
'Total number of Elasticsearch requests',
['operation', 'index']
);
this.registry.register(this.requestsTotal);
this.requestDuration = new Histogram(
'elasticsearch_request_duration_seconds',
'Elasticsearch request duration in seconds',
['operation', 'index']
);
this.registry.register(this.requestDuration);
this.requestErrors = new Counter(
'elasticsearch_request_errors_total',
'Total number of Elasticsearch request errors',
['operation', 'index', 'error_code']
);
this.registry.register(this.requestErrors);
this.activeConnections = new Gauge(
'elasticsearch_active_connections',
'Number of active Elasticsearch connections'
);
this.registry.register(this.activeConnections);
this.bulkOperations = new Counter(
'elasticsearch_bulk_operations_total',
'Total number of bulk operations',
['index']
);
this.registry.register(this.bulkOperations);
this.bulkDocuments = new Counter(
'elasticsearch_bulk_documents_total',
'Total number of documents in bulk operations',
['index', 'status']
);
this.registry.register(this.bulkDocuments);
this.retries = new Counter(
'elasticsearch_retries_total',
'Total number of retry attempts',
['operation']
);
this.registry.register(this.retries);
}
/**
* Create a custom counter
*/
counter(name: string, help: string, labels?: string[]): Counter {
const counter = new Counter(name, help, labels);
this.registry.register(counter);
return counter;
}
/**
* Create a custom gauge
*/
gauge(name: string, help: string, labels?: string[]): Gauge {
const gauge = new Gauge(name, help, labels);
this.registry.register(gauge);
return gauge;
}
/**
* Create a custom histogram
*/
histogram(name: string, help: string, labels?: string[], buckets?: number[]): Histogram {
const histogram = new Histogram(name, help, labels, buckets);
this.registry.register(histogram);
return histogram;
}
/**
* Export all metrics in Prometheus format
*/
export(): string {
return this.registry.export();
}
}
/**
* Default metrics collector
*/
export const defaultMetricsCollector = new MetricsCollector();

View File

@@ -0,0 +1,438 @@
/**
* Span attributes
*/
export type SpanAttributes = Record<string, string | number | boolean | null | undefined>;
/**
* 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<T>(name: string, fn: (span: Span) => Promise<T>): Promise<T>;
}
/**
* 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<string, unknown> {
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<void>;
}
/**
* 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<T>(name: string, fn: (span: Span) => Promise<T>): Promise<T> {
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<string, string>): 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<string, string>): SpanContext | null {
const traceparent = carrier['traceparent'];
if (!traceparent) return null;
const parts = traceparent.split('-');
if (parts.length !== 4) return null;
return {
traceId: parts[1],
spanId: parts[2],
traceFlags: parseInt(parts[3], 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<T>(_name: string, fn: (span: Span) => Promise<T>): Promise<T> {
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<TracingConfig> = {}) {
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<T>(name: string, fn: (span: Span) => Promise<T>): Promise<T> {
return this.tracer.withSpan(name, fn);
}
propagateContext(carrier: Record<string, string>): void {
if (this.tracer instanceof InMemoryTracer) {
this.tracer.inject(carrier);
}
}
extractContext(carrier: Record<string, string>): SpanContext | null {
if (this.tracer instanceof InMemoryTracer) {
return this.tracer.extract(carrier);
}
return null;
}
}
/**
* Default tracing provider
*/
export const defaultTracingProvider = new TracingProvider();