124 lines · 4.1 KB · TypeScript
RawDownload
import * as Exit from "effect/Exit";import * as Layer from "effect/Layer";import * as ManagedRuntime from "effect/ManagedRuntime";import * as Scope from "effect/Scope";import * as Tracer from "effect/Tracer";import { HttpClient } from "effect/unstable/http";import { OtlpExporter, OtlpSerialization, OtlpTracer } from "effect/unstable/observability"; import { settleAsyncResult, squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime";import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors";import { resolvePrimaryEnvironmentHttpUrl } from "../environments/primary";import { primaryEnvironmentHttpLayer } from "../environments/primary/httpLayer";import { isElectron } from "../env";import { APP_VERSION } from "~/branding"; const DEFAULT_EXPORT_INTERVAL_MS = 1_000;const CLIENT_TRACING_RESOURCE = {  serviceName: "t3-web",  attributes: {    "service.runtime": "t3-web",    "service.mode": isElectron ? "electron" : "browser",    "service.version": APP_VERSION,  },} as const; const delegateRuntimeLayer = Layer.mergeAll(  primaryEnvironmentHttpLayer,  OtlpExporter.layerFlusher,  OtlpSerialization.layerJson,  Layer.succeed(HttpClient.TracerDisabledWhen, () => true),); let activeDelegate: Tracer.Tracer | null = null;let activeRuntime: ManagedRuntime.ManagedRuntime<never, never> | null = null;let activeScope: Scope.Closeable | null = null;let activeConfigKey: string | null = null;let configurationGeneration = 0;let pendingConfiguration = Promise.resolve(); export interface ClientTracingConfig {  readonly exportIntervalMs?: number;} export function configureClientTracing(config: ClientTracingConfig = {}): Promise<void> {  if (config.exportIntervalMs === undefined && activeConfigKey !== null) {    return pendingConfiguration;  }  pendingConfiguration = pendingConfiguration.finally(() => applyClientTracingConfig(config));  return pendingConfiguration;} async function applyClientTracingConfig(config: ClientTracingConfig): Promise<void> {  const otlpTracesUrl = resolvePrimaryEnvironmentHttpUrl("/api/observability/v1/traces");  const exportIntervalMs = Math.max(10, config.exportIntervalMs ?? DEFAULT_EXPORT_INTERVAL_MS);  const nextConfigKey = `${otlpTracesUrl}|${exportIntervalMs}`;   if (activeConfigKey === nextConfigKey && activeDelegate !== null) {    return;  }   activeConfigKey = nextConfigKey;  const generation = ++configurationGeneration;   const previousRuntime = activeRuntime;  const previousScope = activeScope;   activeDelegate = null;  activeRuntime = null;  activeScope = null;   await disposeTracerRuntime(previousRuntime, previousScope);   const runtime = ManagedRuntime.make(delegateRuntimeLayer);  const scope = runtime.runSync(Scope.make());   const delegateResult = await settleAsyncResult(() =>    runtime.runPromiseExit(      Scope.provide(scope)(        OtlpTracer.make({          url: otlpTracesUrl,          exportInterval: `${exportIntervalMs} millis`,          resource: CLIENT_TRACING_RESOURCE,        }),      ),    ),  );  if (delegateResult._tag === "Failure") {    await disposeTracerRuntime(runtime, scope);     if (generation === configurationGeneration) {      const error = squashAtomCommandFailure(delegateResult);      const tracesUrl = new URL(otlpTracesUrl);      console.warn("Failed to configure client tracing exporter", {        scheme: tracesUrl.protocol.replace(/:$/, ""),        host: tracesUrl.hostname,        port: tracesUrl.port || undefined,        exportIntervalMs,        ...safeErrorLogAttributes(error),      });    }    return;  }   if (generation !== configurationGeneration) {    await disposeTracerRuntime(runtime, scope);    return;  }   activeDelegate = delegateResult.value;  activeRuntime = runtime;  activeScope = scope;} async function disposeTracerRuntime(  runtime: ManagedRuntime.ManagedRuntime<never, never> | null,  scope: Scope.Closeable | null,): Promise<void> {  if (runtime === null || scope === null) {    return;  }   await settleAsyncResult(() => runtime.runPromiseExit(Scope.close(scope, Exit.void)));  runtime.dispose();}