| author | |
| committer | |
| log | a8693eb4ed3a66d72e6b814117e7beeaf0fb5f8b |
| tree | cc9f496d391760efbe48c42027c15b76b71b66aa |
| parent | efb96939fbb135b9f4de3bff19447200b178b23a |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | 4 | import * as Effect from "effect/Effect"; |
| 5 | 5 | import * as FileSystem from "effect/FileSystem"; |
| 6 | 6 | import * as Layer from "effect/Layer"; |
| 7 | import * as Metric from "effect/Metric"; | |
| 8 | 7 | import * as Schema from "effect/Schema"; |
| 9 | 8 | import * as HttpClient from "effect/unstable/http/HttpClient"; |
| 10 | 9 | import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"; |
| ... | ... | @@ -428,32 +427,6 @@ describe("DesktopObservability", () => { |
| 428 | 427 | ); |
| 429 | 428 | }); |
| 430 | 429 | |
| 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 | 430 | it.effect("reads every signal endpoint from Settings when the environment names none", () => { |
| 458 | 431 | const requests: Array<ExportedRequest> = []; |
| 459 | 432 | return Effect.gen(function* () { |
| ... | ... | @@ -463,30 +436,31 @@ describe("DesktopObservability", () => { |
| 463 | 436 | }); |
| 464 | 437 | const environmentLayer = makeEnvironmentLayer(baseDir); |
| 465 | 438 | yield* writeObservabilitySettings(environmentLayer, { |
| 439 | otlpTracesUrl: "https://settings.example.com/v1/traces", | |
| 466 | 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 | 443 | otlpMetricsUrl: "https://settings.example.com/v1/metrics", |
| 468 | 444 | }); |
| 469 | 445 | |
| 470 | 446 | 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"), | |
| 475 | 449 | Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))), |
| 476 | 450 | ), |
| 477 | 451 | ); |
| 478 | 452 | |
| 479 | 453 | assert.deepEqual(requests.map((request) => request.url).toSorted(), [ |
| 480 | 454 | "https://settings.example.com/v1/logs", |
| 481 | "https://settings.example.com/v1/metrics", | |
| 455 | "https://settings.example.com/v1/traces", | |
| 482 | 456 | ]); |
| 483 | 457 | assert.include( |
| 484 | 458 | requests.find((request) => request.url.endsWith("/v1/logs"))?.body ?? "", |
| 485 | 459 | "desktop log export from settings", |
| 486 | 460 | ); |
| 487 | 461 | 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", | |
| 490 | 464 | ); |
| 491 | 465 | }).pipe( |
| 492 | 466 | Effect.scoped, |
apps/desktop/src/app/DesktopObservability.ts+22-16| ... | ... | @@ -24,7 +24,7 @@ import * as Scope from "effect/Scope"; |
| 24 | 24 | import * as Semaphore from "effect/Semaphore"; |
| 25 | 25 | import * as SynchronizedRef from "effect/SynchronizedRef"; |
| 26 | 26 | import * as Tracer from "effect/Tracer"; |
| 27 | import { OtlpExporter, OtlpLogger, OtlpMetrics, OtlpTracer } from "effect/unstable/observability"; | |
| 27 | import { OtlpExporter, OtlpLogger, OtlpTracer } from "effect/unstable/observability"; | |
| 28 | 28 | |
| 29 | 29 | import * as DesktopEnvironment from "./DesktopEnvironment.ts"; |
| 30 | 30 | |
| ... | ... | @@ -349,7 +349,7 @@ const readPersistedObservabilitySettings: Effect.Effect< |
| 349 | 349 | }); |
| 350 | 350 | |
| 351 | 351 | /** |
| 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 | |
| 353 | 353 | * resolve traces against one revision of the file and logs against another. |
| 354 | 354 | */ |
| 355 | 355 | const resolveOtlpEndpoints = Effect.gen(function* () { |
| ... | ... | @@ -580,9 +580,9 @@ const backendOutputLogFactoryLayer = Layer.effect( |
| 580 | 580 | ); |
| 581 | 581 | |
| 582 | 582 | /** |
| 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. | |
| 586 | 586 | */ |
| 587 | 587 | const telemetryLayer = Layer.unwrap( |
| 588 | 588 | Effect.gen(function* () { |
| ... | ... | @@ -655,17 +655,23 @@ const telemetryLayer = Layer.unwrap( |
| 655 | 655 | }), |
| 656 | 656 | ).pipe(Layer.provide(OtlpExporter.layerFlusher)); |
| 657 | 657 | |
| 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); | |
| 669 | 675 | }), |
| 670 | 676 | ); |
| 671 | 677 |
docs/operations/observability.md+2-1| ... | ... | @@ -527,7 +527,8 @@ The desktop main process is a second producer, assembled in |
| 527 | 527 | Settings entries as the backend it supervises, and covers work the backend cannot see: app startup, |
| 528 | 528 | window and menu handling, backend supervision, and updates. It reports as service `desktop` |
| 529 | 529 | regardless of `T3CODE_OTLP_SERVICE_NAME`, so a collector shows it alongside the backend rather than |
| 530 | mixed into it. | |
| 530 | mixed into it. It exports traces and logs only; the main process records no metrics, so the metrics | |
| 531 | endpoint applies to the backend alone. | |
| 531 | 532 | |
| 532 | 533 | ### Env Vars |
| 533 | 534 |