import { Cause, Exit, Option, Tracer } from "effect"; import { compactTraceAttributes } from "./Attributes.ts"; import { OtlpResource, OtlpTracer } from "effect/unstable/observability"; interface TraceRecordEvent { readonly name: string; readonly timeUnixNano: string; readonly attributes: Readonly>; } interface TraceRecordLink { readonly traceId: string; readonly spanId: string; readonly attributes: Readonly>; } interface BaseTraceRecord { readonly name: string; readonly kind: string; readonly traceId: string; readonly spanId: string; readonly parentSpanId?: string; readonly sampled: boolean; readonly startTimeUnixNano: string; readonly endTimeUnixNano: string; readonly durationMs: number; readonly attributes: Readonly>; readonly events: ReadonlyArray; readonly links: ReadonlyArray; } export interface EffectTraceRecord extends BaseTraceRecord { readonly type: "effect-span"; readonly exit: | { readonly _tag: "Success"; } | { readonly _tag: "Interrupted"; readonly cause: string; } | { readonly _tag: "Failure"; readonly cause: string; }; } interface OtlpTraceRecord extends BaseTraceRecord { readonly type: "otlp-span"; readonly resourceAttributes: Readonly>; readonly scope: Readonly<{ readonly name?: string; readonly version?: string; readonly attributes: Readonly>; }>; readonly status?: | { readonly code?: string; readonly message?: string; } | undefined; } export type TraceRecord = EffectTraceRecord | OtlpTraceRecord; type OtlpSpan = OtlpTracer.ScopeSpan["spans"][number]; type OtlpSpanEvent = OtlpSpan["events"][number]; type OtlpSpanLink = OtlpSpan["links"][number]; type OtlpSpanStatus = OtlpSpan["status"]; interface SerializableSpan { readonly name: string; readonly traceId: string; readonly spanId: string; readonly parent: Option.Option; readonly status: Tracer.SpanStatus; readonly sampled: boolean; readonly kind: Tracer.SpanKind; readonly attributes: ReadonlyMap; readonly links: ReadonlyArray; readonly events: ReadonlyArray< readonly [name: string, startTime: bigint, attributes: Record] >; } function formatTraceExit(exit: Exit.Exit): EffectTraceRecord["exit"] { if (Exit.isSuccess(exit)) { return { _tag: "Success" }; } if (Cause.hasInterruptsOnly(exit.cause)) { return { _tag: "Interrupted", cause: Cause.pretty(exit.cause), }; } return { _tag: "Failure", cause: Cause.pretty(exit.cause), }; } export function spanToTraceRecord(span: SerializableSpan): EffectTraceRecord { const status = span.status as Extract; const parentSpanId = Option.getOrUndefined(span.parent)?.spanId; return { type: "effect-span", name: span.name, traceId: span.traceId, spanId: span.spanId, ...(parentSpanId ? { parentSpanId } : {}), sampled: span.sampled, kind: span.kind, startTimeUnixNano: String(status.startTime), endTimeUnixNano: String(status.endTime), durationMs: Number(status.endTime - status.startTime) / 1_000_000, attributes: compactTraceAttributes(Object.fromEntries(span.attributes)), events: span.events.map(([name, startTime, attributes]) => ({ name, timeUnixNano: String(startTime), attributes: compactTraceAttributes(attributes), })), links: span.links.map((link) => ({ traceId: link.span.traceId, spanId: link.span.spanId, attributes: compactTraceAttributes(link.attributes), })), exit: formatTraceExit(status.exit), }; } const SPAN_KIND_MAP: Record = { 1: "internal", 2: "server", 3: "client", 4: "producer", 5: "consumer", }; export function decodeOtlpTraceRecords( payload: OtlpTracer.TraceData, ): ReadonlyArray { const records: Array = []; for (const resourceSpan of payload.resourceSpans) { const resourceAttributes = decodeAttributes(resourceSpan.resource?.attributes ?? []); for (const scopeSpan of resourceSpan.scopeSpans) { for (const span of scopeSpan.spans) { records.push( otlpSpanToTraceRecord({ resourceAttributes, scopeAttributes: decodeAttributes( "attributes" in scopeSpan.scope && Array.isArray(scopeSpan.scope.attributes) ? scopeSpan.scope.attributes : [], ), scopeName: scopeSpan.scope.name, scopeVersion: "version" in scopeSpan.scope && typeof scopeSpan.scope.version === "string" ? scopeSpan.scope.version : undefined, span, }), ); } } } return records; } function otlpSpanToTraceRecord(input: { readonly resourceAttributes: Readonly>; readonly scopeAttributes: Readonly>; readonly scopeName: string | undefined; readonly scopeVersion: string | undefined; readonly span: OtlpSpan; }): OtlpTraceRecord { return { type: "otlp-span", name: input.span.name, traceId: input.span.traceId, spanId: input.span.spanId, ...(input.span.parentSpanId ? { parentSpanId: input.span.parentSpanId } : {}), sampled: true, kind: normalizeSpanKind(input.span.kind), startTimeUnixNano: input.span.startTimeUnixNano, endTimeUnixNano: input.span.endTimeUnixNano, durationMs: Number(parseBigInt(input.span.endTimeUnixNano) - parseBigInt(input.span.startTimeUnixNano)) / 1_000_000, attributes: decodeAttributes(input.span.attributes), resourceAttributes: input.resourceAttributes, scope: { ...(input.scopeName ? { name: input.scopeName } : {}), ...(input.scopeVersion ? { version: input.scopeVersion } : {}), attributes: input.scopeAttributes, }, events: decodeEvents(input.span.events), links: decodeLinks(input.span.links), status: decodeStatus(input.span.status), }; } function decodeStatus(input: OtlpSpanStatus): OtlpTraceRecord["status"] { const code = String(input.code); const message = input.message; return { code, ...(message ? { message } : {}), }; } function decodeEvents(input: ReadonlyArray): ReadonlyArray { return input.map((current) => ({ name: current.name, timeUnixNano: current.timeUnixNano, attributes: decodeAttributes(current.attributes), })); } function decodeLinks(input: ReadonlyArray): ReadonlyArray { return input.flatMap((current) => { const traceId = current.traceId; const spanId = current.spanId; return { traceId, spanId, attributes: decodeAttributes(current.attributes), }; }); } function decodeAttributes( input: ReadonlyArray, ): Readonly> { const entries: Record = {}; for (const attribute of input) { entries[attribute.key] = decodeValue(attribute.value); } return compactTraceAttributes(entries); } function decodeValue(input: OtlpResource.AnyValue | null | undefined): unknown { if (input == null) { return null; } if ("stringValue" in input) { return input.stringValue; } if ("boolValue" in input) { return input.boolValue; } if ("intValue" in input) { return input.intValue; } if ("doubleValue" in input) { return input.doubleValue; } if ("bytesValue" in input) { return input.bytesValue; } if (input.arrayValue) { return input.arrayValue.values.map((entry) => decodeValue(entry)); } if (input.kvlistValue) { return decodeAttributes(input.kvlistValue.values); } return null; } function normalizeSpanKind(input: number): OtlpTraceRecord["kind"] { return SPAN_KIND_MAP[input] || "internal"; } function parseBigInt(input: string): bigint { try { return BigInt(input); } catch { return 0n; } }