import * as Cause from "effect/Cause"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Tracer from "effect/Tracer"; import type { HttpClient } from "effect/unstable/http"; import { OtlpSerialization, OtlpTracer } from "effect/unstable/observability"; export interface RelayClientTracingConfig { readonly tracesUrl: string; readonly tracesDataset: string; readonly tracesToken: string; } export interface RelayClientTracingResource { readonly serviceName: string; readonly serviceVersion?: string; readonly runtime: string; readonly client: string; readonly component?: string; } export class RelayClientTracer extends Context.Reference( "@t3tools/shared/relayTracing/RelayClientTracer", { defaultValue: () => Option.none(), }, ) {} export const withRelayClientTracing = ( effect: Effect.Effect, ): Effect.Effect => RelayClientTracer.pipe( Effect.flatMap( Option.match({ onNone: () => effect, onSome: (tracer) => effect.pipe(Effect.provideService(Tracer.Tracer, tracer)), }), ), ); function traceSafeError(value: unknown): Error { const message = value instanceof Error ? value.message : typeof value === "object" && value !== null && "message" in value && typeof value.message === "string" ? value.message : String(value); const error = new Error(message); if (value instanceof Error) { error.name = value.name; if (value.stack !== undefined) { error.stack = value.stack; } } else if ( typeof value === "object" && value !== null && "name" in value && typeof value.name === "string" ) { error.name = value.name; } return error; } function traceSafeExit(exit: Exit.Exit): Exit.Exit { if (Exit.isSuccess(exit)) { return exit; } return Exit.failCause( Cause.fromReasons( exit.cause.reasons.map((reason) => { if (Cause.isFailReason(reason)) { return Cause.makeFailReason(traceSafeError(reason.error)); } if (Cause.isDieReason(reason)) { return Cause.makeDieReason(traceSafeError(reason.defect)); } return reason; }), ), ); } function nonInterferingTracer(delegate: Tracer.Tracer): Tracer.Tracer { return Tracer.make({ span(options) { const span = delegate.span(options); const end = span.end.bind(span); span.end = (endTime, exit) => { try { end(endTime, traceSafeExit(exit)); } catch { // Telemetry is best-effort and must never change application behavior. } }; return span; }, ...(delegate.context ? { context: delegate.context } : {}), }); } export function makeRelayClientTracingLayer( config: RelayClientTracingConfig | null, resource: RelayClientTracingResource, ): Layer.Layer { if (config === null) { return Layer.succeed(RelayClientTracer, Option.none()); } const tracerLayer = OtlpTracer.layer({ url: config.tracesUrl, headers: { Authorization: `Bearer ${config.tracesToken}`, "X-Axiom-Dataset": config.tracesDataset, }, resource: { serviceName: resource.serviceName, serviceVersion: resource.serviceVersion, attributes: { "service.runtime": resource.runtime, "service.component": resource.component ?? "relay-client", "t3.client.surface": resource.client, }, }, }).pipe(Layer.provide(OtlpSerialization.layerJson)); return Layer.effect( RelayClientTracer, Tracer.Tracer.pipe(Effect.map(nonInterferingTracer), Effect.map(Option.some)), ).pipe(Layer.provide(tracerLayer)); }