Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions apps/web/src/lib/runtime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { describe, expect, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as Tracer from "effect/Tracer";

import { ClientTracingLive } from "../observability/clientTracer";
import { runtime } from "./runtime";

const readTracer = Effect.service(Tracer.Tracer);

describe("web runtime", () => {
it.effect("installs the client tracer so client spans reach the trace proxy", () =>
Effect.gen(function* () {
const installed = yield* Effect.promise(() => runtime.runPromise(readTracer));

expect(installed).toBe(yield* readTracer);
}).pipe(Effect.provide(ClientTracingLive)),
);
});
3 changes: 3 additions & 0 deletions apps/web/src/lib/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { primaryEnvironmentHttpLayer } from "../environments/primary/httpLayer";
import { browserCryptoLayer } from "../cloud/dpop";
import { managedRelayClientLayer } from "../cloud/managedRelayLayer";
import { resolveCloudPublicConfig, resolveRelayTracingConfig } from "../cloud/publicConfig";
import { ClientTracingLive } from "../observability/clientTracer";

function configuredRelayUrl(): string {
return resolveCloudPublicConfig().relayUrl ?? "http://relay.invalid";
Expand All @@ -29,6 +30,7 @@ type RuntimeLayerSource =
| typeof browserCryptoLayer
| typeof Socket.layerWebSocketConstructorGlobal
| typeof relayTracingLayer
| typeof ClientTracingLive
| ReturnType<typeof managedRelayClientLayer>;

export const remoteHttpRuntime = ManagedRuntime.make(httpClientLayer);
Expand Down Expand Up @@ -58,6 +60,7 @@ const runtimeLayer = Layer.mergeAll(
httpClientLayer,
browserCryptoLayer,
Socket.layerWebSocketConstructorGlobal,
ClientTracingLive,
relayTracingLayer,
managedRelayClientLayer(configuredRelayUrl()).pipe(
Layer.provide(Layer.mergeAll(httpClientLayer, browserCryptoLayer)),
Expand Down
29 changes: 29 additions & 0 deletions apps/web/src/observability/clientTracer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as Layer from "effect/Layer";
import * as Tracer from "effect/Tracer";

let delegate: Tracer.Tracer | null = null;

/**
* Points the installed tracer at the exporter `configureClientTracing` just built,
* or at nothing while no exporter is configured.
*/
export function setClientTracerDelegate(next: Tracer.Tracer | null): void {
delegate = next;
}

export function hasClientTracerDelegate(): boolean {
return delegate !== null;
}

/**
* Installed once when the client runtime is built, before any exporter exists, so
* client spans keep flowing to whatever exporter is configured later on.
*/
export const ClientTracingLive = Layer.succeed(
Tracer.Tracer,
Tracer.make({
span(options) {
return delegate?.span(options) ?? new Tracer.NativeSpan(options);
},
}),
);
20 changes: 5 additions & 15 deletions apps/web/src/observability/clientTracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ 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 { hasClientTracerDelegate, setClientTracerDelegate } from "./clientTracer";
import { primaryEnvironmentHttpLayer } from "../environments/primary/httpLayer";
import { isElectron } from "../env";
import { APP_VERSION } from "~/branding";
Expand All @@ -30,7 +30,6 @@ const delegateRuntimeLayer = Layer.mergeAll(
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;
Expand All @@ -41,15 +40,6 @@ export interface ClientTracingConfig {
readonly exportIntervalMs?: number;
}

export const ClientTracingLive = Layer.succeed(
Tracer.Tracer,
Tracer.make({
span(options) {
return activeDelegate?.span(options) ?? new Tracer.NativeSpan(options);
},
}),
);

export function configureClientTracing(config: ClientTracingConfig = {}): Promise<void> {
if (config.exportIntervalMs === undefined && activeConfigKey !== null) {
return pendingConfiguration;
Expand All @@ -63,7 +53,7 @@ async function applyClientTracingConfig(config: ClientTracingConfig): Promise<vo
const exportIntervalMs = Math.max(10, config.exportIntervalMs ?? DEFAULT_EXPORT_INTERVAL_MS);
const nextConfigKey = `${otlpTracesUrl}|${exportIntervalMs}`;

if (activeConfigKey === nextConfigKey && activeDelegate !== null) {
if (activeConfigKey === nextConfigKey && hasClientTracerDelegate()) {
return;
}

Expand All @@ -73,7 +63,7 @@ async function applyClientTracingConfig(config: ClientTracingConfig): Promise<vo
const previousRuntime = activeRuntime;
const previousScope = activeScope;

activeDelegate = null;
setClientTracerDelegate(null);
activeRuntime = null;
activeScope = null;

Expand Down Expand Up @@ -115,7 +105,7 @@ async function applyClientTracingConfig(config: ClientTracingConfig): Promise<vo
return;
}

activeDelegate = delegateResult.value;
setClientTracerDelegate(delegateResult.value);
activeRuntime = runtime;
activeScope = scope;
}
Expand All @@ -135,7 +125,7 @@ async function disposeTracerRuntime(
export async function __resetClientTracingForTests() {
configurationGeneration++;
activeConfigKey = null;
activeDelegate = null;
setClientTracerDelegate(null);
pendingConfiguration = Promise.resolve();

const runtime = activeRuntime;
Expand Down
Loading