| author | |
| committer | |
| log | bd5dc58c65b87bfd2222e4697c4742b48e639ea4 |
| tree | a2d246d140c8809c65e5dc6f43a62afdf17f92eb |
| parent | ccad9f69aae7465c4ad4ea169d273f61ffac1eb5 |
| signature | Signed by PGP key B5690EEEBB952194 |
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>4 files changed, 64 insertions(+), 5 deletions(-)
apps/web/src/lib/runtime.test.ts created+28| ... | ... | @@ -0,0 +1,28 @@ |
| 1 | import { describe, expect, it } from "@effect/vitest"; | |
| 2 | import * as Effect from "effect/Effect"; | |
| 3 | import * as Tracer from "effect/Tracer"; | |
| 4 | ||
| 5 | import * as ClientTracer from "../observability/clientTracer"; | |
| 6 | import { runtime } from "./runtime"; | |
| 7 | ||
| 8 | describe("web runtime", () => { | |
| 9 | it("routes client spans to the exporter client tracing configured", async () => { | |
| 10 | const exported: Array<string> = []; | |
| 11 | ClientTracer.setDelegate( | |
| 12 | Tracer.make({ | |
| 13 | span(options) { | |
| 14 | exported.push(options.name); | |
| 15 | return new Tracer.NativeSpan(options); | |
| 16 | }, | |
| 17 | }), | |
| 18 | ); | |
| 19 | ||
| 20 | try { | |
| 21 | await runtime.runPromise(Effect.void.pipe(Effect.withSpan("client.work"))); | |
| 22 | } finally { | |
| 23 | ClientTracer.setDelegate(null); | |
| 24 | } | |
| 25 | ||
| 26 | expect(exported).toEqual(["client.work"]); | |
| 27 | }); | |
| 28 | }); |
apps/web/src/lib/runtime.ts+3| ... | ... | @@ -11,6 +11,7 @@ import { primaryEnvironmentHttpLayer } from "../environments/primary/httpLayer"; |
| 11 | 11 | import { browserCryptoLayer } from "../cloud/dpop"; |
| 12 | 12 | import { managedRelayClientLayer } from "../cloud/managedRelayLayer"; |
| 13 | 13 | import { resolveCloudPublicConfig, resolveRelayTracingConfig } from "../cloud/publicConfig"; |
| 14 | import * as ClientTracer from "../observability/clientTracer"; | |
| 14 | 15 | |
| 15 | 16 | function configuredRelayUrl(): string { |
| 16 | 17 | return resolveCloudPublicConfig().relayUrl ?? "http://relay.invalid"; |
| ... | ... | @@ -29,6 +30,7 @@ type RuntimeLayerSource = |
| 29 | 30 | | typeof browserCryptoLayer |
| 30 | 31 | | typeof Socket.layerWebSocketConstructorGlobal |
| 31 | 32 | | typeof relayTracingLayer |
| 33 | | typeof ClientTracer.layer | |
| 32 | 34 | | ReturnType<typeof managedRelayClientLayer>; |
| 33 | 35 | |
| 34 | 36 | const primaryHttpRuntime = ManagedRuntime.make( |
| ... | ... | @@ -56,6 +58,7 @@ const runtimeLayer = Layer.mergeAll( |
| 56 | 58 | httpClientLayer, |
| 57 | 59 | browserCryptoLayer, |
| 58 | 60 | Socket.layerWebSocketConstructorGlobal, |
| 61 | ClientTracer.layer, | |
| 59 | 62 | relayTracingLayer, |
| 60 | 63 | managedRelayClientLayer(configuredRelayUrl()).pipe( |
| 61 | 64 | Layer.provide(Layer.mergeAll(httpClientLayer, browserCryptoLayer)), |
apps/web/src/observability/clientTracer.ts created+29| ... | ... | @@ -0,0 +1,29 @@ |
| 1 | import * as Layer from "effect/Layer"; | |
| 2 | import * as Tracer from "effect/Tracer"; | |
| 3 | ||
| 4 | let delegate: Tracer.Tracer | null = null; | |
| 5 | ||
| 6 | /** | |
| 7 | * Points the installed tracer at the exporter `configureClientTracing` just built, | |
| 8 | * or at nothing while no exporter is configured. | |
| 9 | */ | |
| 10 | export function setDelegate(next: Tracer.Tracer | null): void { | |
| 11 | delegate = next; | |
| 12 | } | |
| 13 | ||
| 14 | export function hasDelegate(): boolean { | |
| 15 | return delegate !== null; | |
| 16 | } | |
| 17 | ||
| 18 | /** | |
| 19 | * Installed once when the client runtime is built, before any exporter exists, so | |
| 20 | * client spans keep flowing to whatever exporter is configured later on. | |
| 21 | */ | |
| 22 | export const layer = Layer.succeed( | |
| 23 | Tracer.Tracer, | |
| 24 | Tracer.make({ | |
| 25 | span(options) { | |
| 26 | return delegate?.span(options) ?? new Tracer.NativeSpan(options); | |
| 27 | }, | |
| 28 | }), | |
| 29 | ); |
apps/web/src/observability/clientTracing.ts+4-5| ... | ... | @@ -2,13 +2,13 @@ import * as Exit from "effect/Exit"; |
| 2 | 2 | import * as Layer from "effect/Layer"; |
| 3 | 3 | import * as ManagedRuntime from "effect/ManagedRuntime"; |
| 4 | 4 | import * as Scope from "effect/Scope"; |
| 5 | import * as Tracer from "effect/Tracer"; | |
| 6 | 5 | import { HttpClient } from "effect/unstable/http"; |
| 7 | 6 | import { OtlpExporter, OtlpSerialization, OtlpTracer } from "effect/unstable/observability"; |
| 8 | 7 | |
| 9 | 8 | import { settleAsyncResult, squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime"; |
| 10 | 9 | import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors"; |
| 11 | 10 | import { resolvePrimaryEnvironmentHttpUrl } from "../environments/primary"; |
| 11 | import * as ClientTracer from "./clientTracer"; | |
| 12 | 12 | import { primaryEnvironmentHttpLayer } from "../environments/primary/httpLayer"; |
| 13 | 13 | import { isElectron } from "../env"; |
| 14 | 14 | import { APP_VERSION } from "~/branding"; |
| ... | ... | @@ -30,7 +30,6 @@ const delegateRuntimeLayer = Layer.mergeAll( |
| 30 | 30 | Layer.succeed(HttpClient.TracerDisabledWhen, () => true), |
| 31 | 31 | ); |
| 32 | 32 | |
| 33 | let activeDelegate: Tracer.Tracer | null = null; | |
| 34 | 33 | let activeRuntime: ManagedRuntime.ManagedRuntime<never, never> | null = null; |
| 35 | 34 | let activeScope: Scope.Closeable | null = null; |
| 36 | 35 | let activeConfigKey: string | null = null; |
| ... | ... | @@ -54,7 +53,7 @@ async function applyClientTracingConfig(config: ClientTracingConfig): Promise<vo |
| 54 | 53 | const exportIntervalMs = Math.max(10, config.exportIntervalMs ?? DEFAULT_EXPORT_INTERVAL_MS); |
| 55 | 54 | const nextConfigKey = `${otlpTracesUrl}|${exportIntervalMs}`; |
| 56 | 55 | |
| 57 | if (activeConfigKey === nextConfigKey && activeDelegate !== null) { | |
| 56 | if (activeConfigKey === nextConfigKey && ClientTracer.hasDelegate()) { | |
| 58 | 57 | return; |
| 59 | 58 | } |
| 60 | 59 | |
| ... | ... | @@ -64,7 +63,7 @@ async function applyClientTracingConfig(config: ClientTracingConfig): Promise<vo |
| 64 | 63 | const previousRuntime = activeRuntime; |
| 65 | 64 | const previousScope = activeScope; |
| 66 | 65 | |
| 67 | activeDelegate = null; | |
| 66 | ClientTracer.setDelegate(null); | |
| 68 | 67 | activeRuntime = null; |
| 69 | 68 | activeScope = null; |
| 70 | 69 | |
| ... | ... | @@ -106,7 +105,7 @@ async function applyClientTracingConfig(config: ClientTracingConfig): Promise<vo |
| 106 | 105 | return; |
| 107 | 106 | } |
| 108 | 107 | |
| 109 | activeDelegate = delegateResult.value; | |
| 108 | ClientTracer.setDelegate(delegateResult.value); | |
| 110 | 109 | activeRuntime = runtime; |
| 111 | 110 | activeScope = scope; |
| 112 | 111 | } |