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";
44import * as Effect from "effect/Effect";
55import * as FileSystem from "effect/FileSystem";
66import * as Layer from "effect/Layer";
7import * as Metric from "effect/Metric";
87import * as Schema from "effect/Schema";
98import * as HttpClient from "effect/unstable/http/HttpClient";
109import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
......@@ -428,32 +427,6 @@ describe("DesktopObservability", () => {
428427 );
429428 });
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
457430 it.effect("reads every signal endpoint from Settings when the environment names none", () => {
458431 const requests: Array<ExportedRequest> = [];
459432 return Effect.gen(function* () {
......@@ -463,30 +436,31 @@ describe("DesktopObservability", () => {
463436 });
464437 const environmentLayer = makeEnvironmentLayer(baseDir);
465438 yield* writeObservabilitySettings(environmentLayer, {
439 otlpTracesUrl: "https://settings.example.com/v1/traces",
466440 otlpLogsUrl: "https://settings.example.com/v1/logs",
441 // The main process records no metrics yet, so this endpoint must
442 // not produce a request.
467443 otlpMetricsUrl: "https://settings.example.com/v1/metrics",
468444 });
469445
470446 yield* Effect.scoped(
471 Effect.gen(function* () {
472 yield* Effect.logInfo("desktop log export from settings");
473 yield* Metric.update(Metric.counter("desktop_settings_export_test_total"), 1);
474 }).pipe(
447 Effect.logInfo("desktop log export from settings").pipe(
448 Effect.withSpan("desktop-settings-export-test"),
475449 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))),
476450 ),
477451 );
478452
479453 assert.deepEqual(requests.map((request) => request.url).toSorted(), [
480454 "https://settings.example.com/v1/logs",
481 "https://settings.example.com/v1/metrics",
455 "https://settings.example.com/v1/traces",
482456 ]);
483457 assert.include(
484458 requests.find((request) => request.url.endsWith("/v1/logs"))?.body ?? "",
485459 "desktop log export from settings",
486460 );
487461 assert.include(
488 requests.find((request) => request.url.endsWith("/v1/metrics"))?.body ?? "",
489 "desktop_settings_export_test_total",
462 requests.find((request) => request.url.endsWith("/v1/traces"))?.body ?? "",
463 "desktop-settings-export-test",
490464 );
491465 }).pipe(
492466 Effect.scoped,
apps/desktop/src/app/DesktopObservability.ts+22-16
......@@ -24,7 +24,7 @@ import * as Scope from "effect/Scope";
2424import * as Semaphore from "effect/Semaphore";
2525import * as SynchronizedRef from "effect/SynchronizedRef";
2626import * as Tracer from "effect/Tracer";
27import { OtlpExporter, OtlpLogger, OtlpMetrics, OtlpTracer } from "effect/unstable/observability";
27import { OtlpExporter, OtlpLogger, OtlpTracer } from "effect/unstable/observability";
2828
2929import * as DesktopEnvironment from "./DesktopEnvironment.ts";
3030
......@@ -349,7 +349,7 @@ const readPersistedObservabilitySettings: Effect.Effect<
349349});
350350
351351/**
352 * Settings is read once for all three signals, so the main process cannot
352 * Settings is read once for every signal, so the main process cannot
353353 * resolve traces against one revision of the file and logs against another.
354354 */
355355const resolveOtlpEndpoints = Effect.gen(function* () {
......@@ -580,9 +580,9 @@ const backendOutputLogFactoryLayer = Layer.effect(
580580);
581581
582582/**
583 * Logs, traces, and metrics for the main process, assembled together because
584 * they share one read of the environment and Settings, and because a process
585 * gets exactly one logger set.
583 * Logs and traces for the main process, assembled together because they share
584 * one read of the environment and Settings, and because a process gets exactly
585 * one logger set.
586586 */
587587const telemetryLayer = Layer.unwrap(
588588 Effect.gen(function* () {
......@@ -655,17 +655,23 @@ const telemetryLayer = Layer.unwrap(
655655 }),
656656 ).pipe(Layer.provide(OtlpExporter.layerFlusher));
657657
658 const metricsLayer =
659 endpoints.metrics === undefined
660 ? Layer.empty
661 : OtlpMetrics.layer({
662 url: endpoints.metrics,
663 exportInterval: `${environment.otlpExportIntervalMs} millis`,
664 headers,
665 resource,
666 }).pipe(Layer.provide(serializationLayer));
667
668 return Layer.mergeAll(loggerLayer, tracerLayer, metricsLayer);
658 // Metrics stay off until the main process records one. `OtlpMetrics`
659 // exports on every interval even when the registry is empty, so wiring
660 // it up today would post an empty payload every ten seconds to any
661 // collector configured for the backend. Restore this when a desktop
662 // metric exists, and add it to the `Layer.mergeAll` below.
663 //
664 // const metricsLayer =
665 // endpoints.metrics === undefined
666 // ? Layer.empty
667 // : OtlpMetrics.layer({
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);
669675 }),
670676);
671677
docs/operations/observability.md+2-1
......@@ -527,7 +527,8 @@ The desktop main process is a second producer, assembled in
527527Settings entries as the backend it supervises, and covers work the backend cannot see: app startup,
528528window and menu handling, backend supervision, and updates. It reports as service `desktop`
529529regardless 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
532533### Env Vars
533534