/** * The span model + exporter seam. Attribute names follow OpenTelemetry HTTP semantic conventions * (`http.request.method`, `url.path`, `http.response.status_code`, …) so a span maps cleanly onto an * OTel `Span` when bridged - but nothing here depends on the OTel SDK. You supply an * {@link ObservationAdapter} * (a ~10-line adapter to `@opentelemetry/api`, or the bundled {@link consoleSpanExporter}). */ export type SpanStatus = "unset" | "ok" | "error" export type AttributeValue = string | number | boolean /** A non-parent causal relationship to a span in another trace (the OTel `Link` model). */ export interface ObservationLink { readonly traceId: string readonly spanId: string readonly attributes?: Readonly> } /** A completed (or in-flight) server span for one request. */ export interface NifraSpan { /** 32-hex W3C trace id - shared across every span/service in the trace. */ readonly traceId: string /** 16-hex id of this span. */ readonly spanId: string /** The inbound span's id, if this request continued an upstream trace. */ readonly parentSpanId?: string /** Whether the trace is sampled (the W3C flag). */ readonly sampled: boolean /** Span name - `" "`. */ readonly name: string /** Wall-clock start (epoch ms). */ readonly startTime: number /** Wall-clock end (epoch ms) - set on completion. */ endTime?: number /** Duration in ms (monotonic). */ durationMs?: number status: SpanStatus /** OTel-semantic-convention attributes. */ readonly attributes: Record /** Durable/asynchronous parents that are related but are not this span's single trace parent. */ readonly links?: readonly ObservationLink[] } /** * Where ended spans go. Implement this to bridge to the OpenTelemetry SDK (map each field onto a * real `Span` from a `Tracer`), ship to a collector, or just log. `onStart` is optional (most * backends only need the completed span). */ export interface ObservationAdapter { onStart?(span: NifraSpan): void onEnd(span: NifraSpan): void } /** * Fan out lifecycle notifications to several adapters. Each adapter is isolated: an exception in * one sink cannot prevent the remaining sinks from observing the span. */ export function combineObservationAdapters( adapters: readonly ObservationAdapter[], ): ObservationAdapter { return { onStart(span) { for (const adapter of adapters) { try { adapter.onStart?.(span) } catch { // Observation is fail-open by contract. } } }, onEnd(span) { for (const adapter of adapters) { try { adapter.onEnd(span) } catch { // Observation is fail-open by contract. } } }, } } /** A no-frills exporter that logs each completed span as one structured line. Useful in dev or as a * starting point before wiring a real backend. */ export function consoleSpanExporter( log: (line: string) => void = (l) => { console.log(l) }, ): ObservationAdapter { return { onEnd(span) { log( JSON.stringify({ name: span.name, traceId: span.traceId, spanId: span.spanId, parentSpanId: span.parentSpanId, durationMs: span.durationMs, status: span.status, ...span.attributes, }), ) }, } }