From a8693eb4ed3a66d72e6b814117e7beeaf0fb5f8b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 18 Sep 2026 21:38:48 -0700 Subject: [PATCH] chore(desktop): leave main process metrics export off until a metric exists (#12540) Co-authored-by: Claude Fable 5 --- .../src/app/DesktopObservability.test.ts | 42 ++++--------------- apps/desktop/src/app/DesktopObservability.ts | 36 +++++++++------- docs/operations/observability.md | 3 +- 3 files changed, 31 insertions(+), 50 deletions(-) diff --git a/apps/desktop/src/app/DesktopObservability.test.ts b/apps/desktop/src/app/DesktopObservability.test.ts index eacf99740f3b53323c850a45d1c7ae84571a98bd..d7ccfc43b1854cbf1c6ff88ff6541663bdc64e1b 100644 --- a/apps/desktop/src/app/DesktopObservability.test.ts +++ b/apps/desktop/src/app/DesktopObservability.test.ts @@ -4,7 +4,6 @@ import { assert, describe, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; -import * as Metric from "effect/Metric"; import * as Schema from "effect/Schema"; import * as HttpClient from "effect/unstable/http/HttpClient"; import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"; @@ -428,32 +427,6 @@ describe("DesktopObservability", () => { ); }); - it.effect("exports main process metrics to the configured metrics endpoint", () => { - const requests: Array = []; - return Effect.gen(function* () { - const fileSystem = yield* FileSystem.FileSystem; - const baseDir = yield* fileSystem.makeTempDirectoryScoped({ - prefix: "t3-desktop-observability-test-", - }); - const environmentLayer = makeEnvironmentLayer(baseDir, true, { - T3CODE_OTLP_METRICS_URL: "https://collector.example.com/v1/metrics", - }); - - yield* Effect.scoped( - Metric.update(Metric.counter("desktop_metrics_export_test_total"), 1).pipe( - Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))), - ), - ); - - assert.lengthOf(requests, 1); - assert.strictEqual(requests[0]?.url, "https://collector.example.com/v1/metrics"); - assert.include(requests[0]?.body ?? "", "desktop_metrics_export_test_total"); - }).pipe( - Effect.scoped, - Effect.provide(Layer.mergeAll(NodeServices.layer, collectorLayer(requests))), - ); - }); - it.effect("reads every signal endpoint from Settings when the environment names none", () => { const requests: Array = []; return Effect.gen(function* () { @@ -463,30 +436,31 @@ describe("DesktopObservability", () => { }); const environmentLayer = makeEnvironmentLayer(baseDir); yield* writeObservabilitySettings(environmentLayer, { + otlpTracesUrl: "https://settings.example.com/v1/traces", otlpLogsUrl: "https://settings.example.com/v1/logs", + // The main process records no metrics yet, so this endpoint must + // not produce a request. otlpMetricsUrl: "https://settings.example.com/v1/metrics", }); yield* Effect.scoped( - Effect.gen(function* () { - yield* Effect.logInfo("desktop log export from settings"); - yield* Metric.update(Metric.counter("desktop_settings_export_test_total"), 1); - }).pipe( + Effect.logInfo("desktop log export from settings").pipe( + Effect.withSpan("desktop-settings-export-test"), Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))), ), ); assert.deepEqual(requests.map((request) => request.url).toSorted(), [ "https://settings.example.com/v1/logs", - "https://settings.example.com/v1/metrics", + "https://settings.example.com/v1/traces", ]); assert.include( requests.find((request) => request.url.endsWith("/v1/logs"))?.body ?? "", "desktop log export from settings", ); assert.include( - requests.find((request) => request.url.endsWith("/v1/metrics"))?.body ?? "", - "desktop_settings_export_test_total", + requests.find((request) => request.url.endsWith("/v1/traces"))?.body ?? "", + "desktop-settings-export-test", ); }).pipe( Effect.scoped, diff --git a/apps/desktop/src/app/DesktopObservability.ts b/apps/desktop/src/app/DesktopObservability.ts index 62e3d865a27324907a5284b12cc7034abdac0d70..96657215748b667ee8c3295f5308d0e83a9d80d2 100644 --- a/apps/desktop/src/app/DesktopObservability.ts +++ b/apps/desktop/src/app/DesktopObservability.ts @@ -24,7 +24,7 @@ import * as Scope from "effect/Scope"; import * as Semaphore from "effect/Semaphore"; import * as SynchronizedRef from "effect/SynchronizedRef"; import * as Tracer from "effect/Tracer"; -import { OtlpExporter, OtlpLogger, OtlpMetrics, OtlpTracer } from "effect/unstable/observability"; +import { OtlpExporter, OtlpLogger, OtlpTracer } from "effect/unstable/observability"; import * as DesktopEnvironment from "./DesktopEnvironment.ts"; @@ -349,7 +349,7 @@ const readPersistedObservabilitySettings: Effect.Effect< }); /** - * Settings is read once for all three signals, so the main process cannot + * Settings is read once for every signal, so the main process cannot * resolve traces against one revision of the file and logs against another. */ const resolveOtlpEndpoints = Effect.gen(function* () { @@ -580,9 +580,9 @@ const backendOutputLogFactoryLayer = Layer.effect( ); /** - * Logs, traces, and metrics for the main process, assembled together because - * they share one read of the environment and Settings, and because a process - * gets exactly one logger set. + * Logs and traces for the main process, assembled together because they share + * one read of the environment and Settings, and because a process gets exactly + * one logger set. */ const telemetryLayer = Layer.unwrap( Effect.gen(function* () { @@ -655,17 +655,23 @@ const telemetryLayer = Layer.unwrap( }), ).pipe(Layer.provide(OtlpExporter.layerFlusher)); - const metricsLayer = - endpoints.metrics === undefined - ? Layer.empty - : OtlpMetrics.layer({ - url: endpoints.metrics, - exportInterval: `${environment.otlpExportIntervalMs} millis`, - headers, - resource, - }).pipe(Layer.provide(serializationLayer)); + // Metrics stay off until the main process records one. `OtlpMetrics` + // exports on every interval even when the registry is empty, so wiring + // it up today would post an empty payload every ten seconds to any + // collector configured for the backend. Restore this when a desktop + // metric exists, and add it to the `Layer.mergeAll` below. + // + // const metricsLayer = + // endpoints.metrics === undefined + // ? Layer.empty + // : OtlpMetrics.layer({ + // url: endpoints.metrics, + // exportInterval: `${environment.otlpExportIntervalMs} millis`, + // headers, + // resource, + // }).pipe(Layer.provide(serializationLayer)); - return Layer.mergeAll(loggerLayer, tracerLayer, metricsLayer); + return Layer.mergeAll(loggerLayer, tracerLayer); }), ); diff --git a/docs/operations/observability.md b/docs/operations/observability.md index c7be6b936c45638bac15f663b75ae9541d06b233..adeb983d1bc72f9dfbfe7645dd42c5d52bdf1e31 100644 --- a/docs/operations/observability.md +++ b/docs/operations/observability.md @@ -527,7 +527,8 @@ The desktop main process is a second producer, assembled in Settings entries as the backend it supervises, and covers work the backend cannot see: app startup, window and menu handling, backend supervision, and updates. It reports as service `desktop` regardless of `T3CODE_OTLP_SERVICE_NAME`, so a collector shows it alongside the backend rather than -mixed into it. +mixed into it. It exports traces and logs only; the main process records no metrics, so the metrics +endpoint applies to the backend alone. ### Env Vars -- 2.54.0