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:
438
ts/core/observability/tracing.ts
Normal file
438
ts/core/observability/tracing.ts
Normal 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();
|
||||
Reference in New Issue
Block a user