otel-tracing.ts
1 import { 2 trace, 3 SpanStatusCode, 4 type Attributes, 5 type AttributeValue, 6 type Span, 7 } from '@opentelemetry/api' 8 import { errorMessage } from '@/lib/shared-utils' 9 10 type SpanAttributeInput = Record<string, AttributeValue | null | undefined> 11 12 function sanitizeAttributes(attributes?: SpanAttributeInput): Attributes | undefined { 13 if (!attributes) return undefined 14 const cleaned: Attributes = {} 15 for (const [key, value] of Object.entries(attributes)) { 16 if (value === undefined || value === null) continue 17 cleaned[key] = value 18 } 19 return Object.keys(cleaned).length > 0 ? cleaned : undefined 20 } 21 22 export function setSpanAttributes(span: Span, attributes?: SpanAttributeInput): void { 23 const cleaned = sanitizeAttributes(attributes) 24 if (!cleaned) return 25 span.setAttributes(cleaned) 26 } 27 28 export function recordSpanError(span: Span, err: unknown): void { 29 span.recordException(err instanceof Error ? err : new Error(errorMessage(err))) 30 span.setStatus({ 31 code: SpanStatusCode.ERROR, 32 message: errorMessage(err), 33 }) 34 } 35 36 export async function withServerSpan<T>( 37 name: string, 38 attributes: SpanAttributeInput | undefined, 39 fn: (span: Span) => Promise<T> | T, 40 ): Promise<T> { 41 const tracer = trace.getTracer('swarmclaw.runtime') 42 return tracer.startActiveSpan(name, { attributes: sanitizeAttributes(attributes) }, async (span) => { 43 try { 44 return await fn(span) 45 } catch (err) { 46 recordSpanError(span, err) 47 throw err 48 } finally { 49 span.end() 50 } 51 }) 52 }