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<Record<string, unknown>>;} interface TraceRecordLink { readonly traceId: string; readonly spanId: string; readonly attributes: Readonly<Record<string, unknown>>;} 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<Record<string, unknown>>; readonly events: ReadonlyArray<TraceRecordEvent>; readonly links: ReadonlyArray<TraceRecordLink>;} 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<Record<string, unknown>>; readonly scope: Readonly<{ readonly name?: string; readonly version?: string; readonly attributes: Readonly<Record<string, unknown>>; }>; 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<Tracer.AnySpan>; readonly status: Tracer.SpanStatus; readonly sampled: boolean; readonly kind: Tracer.SpanKind; readonly attributes: ReadonlyMap<string, unknown>; readonly links: ReadonlyArray<Tracer.SpanLink>; readonly events: ReadonlyArray< readonly [name: string, startTime: bigint, attributes: Record<string, unknown>] >;} function formatTraceExit(exit: Exit.Exit<unknown, unknown>): 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<Tracer.SpanStatus, { _tag: "Ended" }>; 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<number, OtlpTraceRecord["kind"]> = { 1: "internal", 2: "server", 3: "client", 4: "producer", 5: "consumer",}; export function decodeOtlpTraceRecords( payload: OtlpTracer.TraceData,): ReadonlyArray<OtlpTraceRecord> { const records: Array<OtlpTraceRecord> = []; 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<Record<string, unknown>>; readonly scopeAttributes: Readonly<Record<string, unknown>>; 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<OtlpSpanEvent>): ReadonlyArray<TraceRecordEvent> { return input.map((current) => ({ name: current.name, timeUnixNano: current.timeUnixNano, attributes: decodeAttributes(current.attributes), }));} function decodeLinks(input: ReadonlyArray<OtlpSpanLink>): ReadonlyArray<TraceRecordLink> { return input.flatMap((current) => { const traceId = current.traceId; const spanId = current.spanId; return { traceId, spanId, attributes: decodeAttributes(current.attributes), }; });} function decodeAttributes( input: ReadonlyArray<OtlpResource.KeyValue>,): Readonly<Record<string, unknown>> { const entries: Record<string, unknown> = {}; 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; }}
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<Record<string, unknown>>;
}
interface TraceRecordLink {
readonly traceId: string;
readonly spanId: string;
readonly attributes: Readonly<Record<string, unknown>>;
}
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<Record<string, unknown>>;
readonly events: ReadonlyArray<TraceRecordEvent>;
readonly links: ReadonlyArray<TraceRecordLink>;
}
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<Record<string, unknown>>;
readonly scope: Readonly<{
readonly name?: string;
readonly version?: string;
readonly attributes: Readonly<Record<string, unknown>>;
}>;
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<Tracer.AnySpan>;
readonly status: Tracer.SpanStatus;
readonly sampled: boolean;
readonly kind: Tracer.SpanKind;
readonly attributes: ReadonlyMap<string, unknown>;
readonly links: ReadonlyArray<Tracer.SpanLink>;
readonly events: ReadonlyArray<
readonly [name: string, startTime: bigint, attributes: Record<string, unknown>]
>;
}
function formatTraceExit(exit: Exit.Exit<unknown, unknown>): 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<Tracer.SpanStatus, { _tag: "Ended" }>;
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<number, OtlpTraceRecord["kind"]> = {
1: "internal",
2: "server",
3: "client",
4: "producer",
5: "consumer",
};
export function decodeOtlpTraceRecords(
payload: OtlpTracer.TraceData,
): ReadonlyArray<OtlpTraceRecord> {
const records: Array<OtlpTraceRecord> = [];
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<Record<string, unknown>>;
readonly scopeAttributes: Readonly<Record<string, unknown>>;
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<OtlpSpanEvent>): ReadonlyArray<TraceRecordEvent> {
return input.map((current) => ({
name: current.name,
timeUnixNano: current.timeUnixNano,
attributes: decodeAttributes(current.attributes),
}));
}
function decodeLinks(input: ReadonlyArray<OtlpSpanLink>): ReadonlyArray<TraceRecordLink> {
return input.flatMap((current) => {
const traceId = current.traceId;
const spanId = current.spanId;
return {
traceId,
spanId,
attributes: decodeAttributes(current.attributes),
};
});
}
function decodeAttributes(
input: ReadonlyArray<OtlpResource.KeyValue>,
): Readonly<Record<string, unknown>> {
const entries: Record<string, unknown> = {};
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;
}
}