authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-18 21:38:48-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-18 21:38:48-07:00
loga8693eb4ed3a66d72e6b814117e7beeaf0fb5f8b
treecc9f496d391760efbe48c42027c15b76b71b66aa
parentefb96939fbb135b9f4de3bff19447200b178b23a
signature Signed by PGP key B5690EEEBB952194

chore(desktop): leave main process metrics export off until a metric exists (#12540)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

3 files changed, 32 insertions(+), 51 deletions(-)

apps/desktop/src/app/DesktopObservability.test.ts+8-34
...@@ -4,7 +4,6 @@ import { assert, describe, it } from "@effect/vitest";...@@ -4,7 +4,6 @@ import { assert, describe, it } from "@effect/vitest";
4import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";5import * as FileSystem from "effect/FileSystem";
6import * as Layer from "effect/Layer";6import * as Layer from "effect/Layer";
7import * as Metric from "effect/Metric";
8import * as Schema from "effect/Schema";7import * as Schema from "effect/Schema";
9import * as HttpClient from "effect/unstable/http/HttpClient";8import * as HttpClient from "effect/unstable/http/HttpClient";
10import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";9import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
...@@ -428,32 +427,6 @@ describe("DesktopObservability", () => {...@@ -428,32 +427,6 @@ describe("DesktopObservability", () => {
428 );427 );
429 });428 });
430429
431 it.effect("exports main process metrics to the configured metrics endpoint", () => {
432 const requests: Array<ExportedRequest> = [];
433 return Effect.gen(function* () {
434 const fileSystem = yield* FileSystem.FileSystem;
435 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
436 prefix: "t3-desktop-observability-test-",
437 });
438 const environmentLayer = makeEnvironmentLayer(baseDir, true, {
439 T3CODE_OTLP_METRICS_URL: "https://collector.example.com/v1/metrics",
440 });
441
442 yield* Effect.scoped(
443 Metric.update(Metric.counter("desktop_metrics_export_test_total"), 1).pipe(
444 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))),
445 ),
446 );
447
448 assert.lengthOf(requests, 1);
449 assert.strictEqual(requests[0]?.url, "https://collector.example.com/v1/metrics");
450 assert.include(requests[0]?.body ?? "", "desktop_metrics_export_test_total");
451 }).pipe(
452 Effect.scoped,
453 Effect.provide(Layer.mergeAll(NodeServices.layer, collectorLayer(requests))),
454 );
455 });
456
457 it.effect("reads every signal endpoint from Settings when the environment names none", () => {430 it.effect("reads every signal endpoint from Settings when the environment names none", () => {
458 const requests: Array<ExportedRequest> = [];431 const requests: Array<ExportedRequest> = [];
459 return Effect.gen(function* () {432 return Effect.gen(function* () {
...@@ -463,30 +436,31 @@ describe("DesktopObservability", () => {...@@ -463,30 +436,31 @@ describe("DesktopObservability", () => {
463 });436 });
464 const environmentLayer = makeEnvironmentLayer(baseDir);437 const environmentLayer = makeEnvironmentLayer(baseDir);
465 yield* writeObservabilitySettings(environmentLayer, {438 yield* writeObservabilitySettings(environmentLayer, {
439 otlpTracesUrl: "https://settings.example.com/v1/traces",
466 otlpLogsUrl: "https://settings.example.com/v1/logs",440 otlpLogsUrl: "https://settings.example.com/v1/logs",
441 // The main process records no metrics yet, so this endpoint must
442 // not produce a request.
467 otlpMetricsUrl: "https://settings.example.com/v1/metrics",443 otlpMetricsUrl: "https://settings.example.com/v1/metrics",
468 });444 });
469445
470 yield* Effect.scoped(446 yield* Effect.scoped(
471 Effect.gen(function* () {447 Effect.logInfo("desktop log export from settings").pipe(
472 yield* Effect.logInfo("desktop log export from settings");448 Effect.withSpan("desktop-settings-export-test"),
473 yield* Metric.update(Metric.counter("desktop_settings_export_test_total"), 1);
474 }).pipe(
475 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))),449 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))),
476 ),450 ),
477 );451 );
478452
479 assert.deepEqual(requests.map((request) => request.url).toSorted(), [453 assert.deepEqual(requests.map((request) => request.url).toSorted(), [
480 "https://settings.example.com/v1/logs",454 "https://settings.example.com/v1/logs",
481 "https://settings.example.com/v1/metrics",455 "https://settings.example.com/v1/traces",
482 ]);456 ]);
483 assert.include(457 assert.include(
484 requests.find((request) => request.url.endsWith("/v1/logs"))?.body ?? "",458 requests.find((request) => request.url.endsWith("/v1/logs"))?.body ?? "",
485 "desktop log export from settings",459 "desktop log export from settings",
486 );460 );
487 assert.include(461 assert.include(
488 requests.find((request) => request.url.endsWith("/v1/metrics"))?.body ?? "",462 requests.find((request) => request.url.endsWith("/v1/traces"))?.body ?? "",
489 "desktop_settings_export_test_total",463 "desktop-settings-export-test",
490 );464 );
491 }).pipe(465 }).pipe(
492 Effect.scoped,466 Effect.scoped,
apps/desktop/src/app/DesktopObservability.ts+22-16
...@@ -24,7 +24,7 @@ import * as Scope from "effect/Scope";...@@ -24,7 +24,7 @@ import * as Scope from "effect/Scope";
24import * as Semaphore from "effect/Semaphore";24import * as Semaphore from "effect/Semaphore";
25import * as SynchronizedRef from "effect/SynchronizedRef";25import * as SynchronizedRef from "effect/SynchronizedRef";
26import * as Tracer from "effect/Tracer";26import * as Tracer from "effect/Tracer";
27import { OtlpExporter, OtlpLogger, OtlpMetrics, OtlpTracer } from "effect/unstable/observability";27import { OtlpExporter, OtlpLogger, OtlpTracer } from "effect/unstable/observability";
2828
29import * as DesktopEnvironment from "./DesktopEnvironment.ts";29import * as DesktopEnvironment from "./DesktopEnvironment.ts";
3030
...@@ -349,7 +349,7 @@ const readPersistedObservabilitySettings: Effect.Effect<...@@ -349,7 +349,7 @@ const readPersistedObservabilitySettings: Effect.Effect<
349});349});
350350
351/**351/**
352 * Settings is read once for all three signals, so the main process cannot352 * Settings is read once for every signal, so the main process cannot
353 * resolve traces against one revision of the file and logs against another.353 * resolve traces against one revision of the file and logs against another.
354 */354 */
355const resolveOtlpEndpoints = Effect.gen(function* () {355const resolveOtlpEndpoints = Effect.gen(function* () {
...@@ -580,9 +580,9 @@ const backendOutputLogFactoryLayer = Layer.effect(...@@ -580,9 +580,9 @@ const backendOutputLogFactoryLayer = Layer.effect(
580);580);
581581
582/**582/**
583 * Logs, traces, and metrics for the main process, assembled together because583 * Logs and traces for the main process, assembled together because they share
584 * they share one read of the environment and Settings, and because a process584 * one read of the environment and Settings, and because a process gets exactly
585 * gets exactly one logger set.585 * one logger set.
586 */586 */
587const telemetryLayer = Layer.unwrap(587const telemetryLayer = Layer.unwrap(
588 Effect.gen(function* () {588 Effect.gen(function* () {
...@@ -655,17 +655,23 @@ const telemetryLayer = Layer.unwrap(...@@ -655,17 +655,23 @@ const telemetryLayer = Layer.unwrap(
655 }),655 }),
656 ).pipe(Layer.provide(OtlpExporter.layerFlusher));656 ).pipe(Layer.provide(OtlpExporter.layerFlusher));
657657
658 const metricsLayer =658 // Metrics stay off until the main process records one. `OtlpMetrics`
659 endpoints.metrics === undefined659 // exports on every interval even when the registry is empty, so wiring
660 ? Layer.empty660 // it up today would post an empty payload every ten seconds to any
661 : OtlpMetrics.layer({661 // collector configured for the backend. Restore this when a desktop
662 url: endpoints.metrics,662 // metric exists, and add it to the `Layer.mergeAll` below.
663 exportInterval: `${environment.otlpExportIntervalMs} millis`,663 //
664 headers,664 // const metricsLayer =
665 resource,665 // endpoints.metrics === undefined
666 }).pipe(Layer.provide(serializationLayer));666 // ? Layer.empty
667667 // : OtlpMetrics.layer({
668 return Layer.mergeAll(loggerLayer, tracerLayer, metricsLayer);668 // url: endpoints.metrics,
669 // exportInterval: `${environment.otlpExportIntervalMs} millis`,
670 // headers,
671 // resource,
672 // }).pipe(Layer.provide(serializationLayer));
673
674 return Layer.mergeAll(loggerLayer, tracerLayer);
669 }),675 }),
670);676);
671677
docs/operations/observability.md+2-1
...@@ -527,7 +527,8 @@ The desktop main process is a second producer, assembled in...@@ -527,7 +527,8 @@ The desktop main process is a second producer, assembled in
527Settings entries as the backend it supervises, and covers work the backend cannot see: app startup,527Settings entries as the backend it supervises, and covers work the backend cannot see: app startup,
528window and menu handling, backend supervision, and updates. It reports as service `desktop`528window and menu handling, backend supervision, and updates. It reports as service `desktop`
529regardless of `T3CODE_OTLP_SERVICE_NAME`, so a collector shows it alongside the backend rather than529regardless of `T3CODE_OTLP_SERVICE_NAME`, so a collector shows it alongside the backend rather than
530mixed into it.530mixed into it. It exports traces and logs only; the main process records no metrics, so the metrics
531endpoint applies to the backend alone.
531532
532### Env Vars533### Env Vars
533534