287 lines · 8.1 KB · TypeScript
RawDownload
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;  }}