/ src / lib / server / observability / otel-tracing.ts
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  }