/**
* OpenTelemetry tracing utilities
*
* Provides helpers for creating spans and adding attributes.
* Follows OpenTelemetry semantic conventions.
*/
import { trace, Span, SpanKind, SpanStatusCode } from '@opentelemetry/api';
import { getConfig } from '../config/index.js';
const config = getConfig();
const tracer = trace.getTracer(config.serverName, config.serverVersion);
/**
* Span options for creating custom spans
*/
interface SpanOptions {
kind?: SpanKind;
attributes?: Record<string, string | number | boolean>;
}
/**
* Execute a function within a traced span
*/
export async function withSpan<T>(
name: string,
fn: (span: Span) => Promise<T>,
options?: SpanOptions
): Promise<T> {
return tracer.startActiveSpan(
name,
{
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 sync function within a traced span
*/
export function withSpanSync<T>(
name: string,
fn: (span: Span) => T,
options?: SpanOptions
): T {
const span = tracer.startSpan(name, {
kind: options?.kind ?? SpanKind.INTERNAL,
attributes: options?.attributes,
});
try {
const result = 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();
}
}
/**
* Get the current active span
*/
export function getCurrentSpan(): Span | undefined {
return trace.getActiveSpan();
}
/**
* Get trace and span IDs from current context
*/
export function getTraceContext(): { traceId?: string; spanId?: string } {
const span = getCurrentSpan();
if (!span) {
return {};
}
const spanContext = span.spanContext();
return {
traceId: spanContext.traceId,
spanId: spanContext.spanId,
};
}
/**
* Add attributes to the current span
*/
export function addSpanAttributes(attributes: Record<string, string | number | boolean>): void {
const span = getCurrentSpan();
if (span) {
span.setAttributes(attributes);
}
}
/**
* Record an error on the current span
*/
export function recordSpanError(error: Error): void {
const span = getCurrentSpan();
if (span) {
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
span.recordException(error);
}
}
/**
* Create a span for tool execution
*/
export function createToolSpan(toolName: string): Span {
return tracer.startSpan(`tool.${toolName}`, {
kind: SpanKind.SERVER,
attributes: {
'mcp.tool.name': toolName,
},
});
}
/**
* Create a span for external API calls
*/
export function createApiSpan(service: string, operation: string): Span {
return tracer.startSpan(`${service}.${operation}`, {
kind: SpanKind.CLIENT,
attributes: {
'peer.service': service,
'rpc.method': operation,
},
});
}
/**
* Create a span for database operations
*/
export function createDbSpan(operation: string, table?: string): Span {
return tracer.startSpan(`db.${operation}`, {
kind: SpanKind.CLIENT,
attributes: {
'db.system': 'sqlite',
'db.operation': operation,
...(table && { 'db.sql.table': table }),
},
});
}