| author | |
| committer | |
| log | 82059df1523d070b00ab6f45159115f626a4977e |
| tree | 9ed319aecdc08d6274f0770568d3ceb5e979a7e1 |
| parent | 0f1b572b9865256dd61e245b4dd09f6a7b15f0e1 |
| signature | Signed by PGP key B5690EEEBB952194 |
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>3 files changed, 311 insertions(+), 56 deletions(-)
apps/desktop/src/app/DesktopObservability.test.ts+190-1| ... | ... | @@ -4,7 +4,10 @@ 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"; | |
| 7 | 8 | import * as Schema from "effect/Schema"; |
| 9 | import * as HttpClient from "effect/unstable/http/HttpClient"; | |
| 10 | import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"; | |
| 8 | 11 | |
| 9 | 12 | import * as DesktopConfig from "./DesktopConfig.ts"; |
| 10 | 13 | import * as DesktopEnvironment from "./DesktopEnvironment.ts"; |
| ... | ... | @@ -49,7 +52,11 @@ const environmentInput = (baseDir: string) => |
| 49 | 52 | runningUnderArm64Translation: false, |
| 50 | 53 | }) satisfies DesktopEnvironment.MakeDesktopEnvironmentInput; |
| 51 | 54 | |
| 52 | const makeEnvironmentLayer = (baseDir: string, isDevelopment = true) => | |
| 55 | const makeEnvironmentLayer = ( | |
| 56 | baseDir: string, | |
| 57 | isDevelopment = true, | |
| 58 | env: Readonly<Record<string, string | undefined>> = {}, | |
| 59 | ) => | |
| 53 | 60 | DesktopEnvironment.layer(environmentInput(baseDir)).pipe( |
| 54 | 61 | Layer.provide( |
| 55 | 62 | Layer.mergeAll( |
| ... | ... | @@ -57,11 +64,57 @@ const makeEnvironmentLayer = (baseDir: string, isDevelopment = true) => |
| 57 | 64 | DesktopConfig.layerTest({ |
| 58 | 65 | T3CODE_HOME: baseDir, |
| 59 | 66 | VITE_DEV_SERVER_URL: isDevelopment ? "http://127.0.0.1:5733" : undefined, |
| 67 | ...env, | |
| 60 | 68 | }), |
| 61 | 69 | ), |
| 62 | 70 | ), |
| 63 | 71 | ); |
| 64 | 72 | |
| 73 | interface ExportedRequest { | |
| 74 | readonly url: string; | |
| 75 | readonly headers: Readonly<Record<string, string>>; | |
| 76 | readonly body: string; | |
| 77 | } | |
| 78 | ||
| 79 | /** Answers every export with a 200 and keeps what was posted for assertions. */ | |
| 80 | const collectorLayer = (requests: Array<ExportedRequest>) => | |
| 81 | Layer.succeed( | |
| 82 | HttpClient.HttpClient, | |
| 83 | HttpClient.make((request) => | |
| 84 | Effect.sync(() => { | |
| 85 | requests.push({ | |
| 86 | url: request.url, | |
| 87 | headers: request.headers, | |
| 88 | body: | |
| 89 | request.body._tag === "Uint8Array" ? new TextDecoder().decode(request.body.body) : "", | |
| 90 | }); | |
| 91 | return HttpClientResponse.fromWeb(request, new Response(null, { status: 200 })); | |
| 92 | }), | |
| 93 | ), | |
| 94 | ); | |
| 95 | ||
| 96 | const encodeObservabilitySettingsFile = Schema.encodeSync( | |
| 97 | Schema.fromJsonString( | |
| 98 | Schema.Struct({ observability: Schema.Record(Schema.String, Schema.String) }), | |
| 99 | ), | |
| 100 | ); | |
| 101 | ||
| 102 | const writeObservabilitySettings = Effect.fn(function* ( | |
| 103 | environmentLayer: ReturnType<typeof makeEnvironmentLayer>, | |
| 104 | observability: Readonly<Record<string, string>>, | |
| 105 | ) { | |
| 106 | const fileSystem = yield* FileSystem.FileSystem; | |
| 107 | const { path, serverSettingsPath } = yield* Effect.gen(function* () { | |
| 108 | const environment = yield* DesktopEnvironment.DesktopEnvironment; | |
| 109 | return environment; | |
| 110 | }).pipe(Effect.provide(environmentLayer)); | |
| 111 | yield* fileSystem.makeDirectory(path.dirname(serverSettingsPath), { recursive: true }); | |
| 112 | yield* fileSystem.writeFileString( | |
| 113 | serverSettingsPath, | |
| 114 | encodeObservabilitySettingsFile({ observability }), | |
| 115 | ); | |
| 116 | }); | |
| 117 | ||
| 65 | 118 | describe("DesktopObservability", () => { |
| 66 | 119 | it("advances a retained output offset instead of repeatedly copying a full head chunk", () => { |
| 67 | 120 | const maxBufferedBytes = 1024 * 1024; |
| ... | ... | @@ -328,4 +381,140 @@ describe("DesktopObservability", () => { |
| 328 | 381 | Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici)), |
| 329 | 382 | ), |
| 330 | 383 | ); |
| 384 | ||
| 385 | it.effect("exports main process log records to the configured logs endpoint", () => { | |
| 386 | const requests: Array<ExportedRequest> = []; | |
| 387 | return Effect.gen(function* () { | |
| 388 | const fileSystem = yield* FileSystem.FileSystem; | |
| 389 | const baseDir = yield* fileSystem.makeTempDirectoryScoped({ | |
| 390 | prefix: "t3-desktop-observability-test-", | |
| 391 | }); | |
| 392 | const environmentLayer = makeEnvironmentLayer(baseDir, true, { | |
| 393 | T3CODE_OTLP_LOGS_URL: "https://collector.example.com/v1/logs", | |
| 394 | T3CODE_OTLP_HEADERS: "x-scope=desktop", | |
| 395 | }); | |
| 396 | const tracePath = yield* Effect.gen(function* () { | |
| 397 | const environment = yield* DesktopEnvironment.DesktopEnvironment; | |
| 398 | return environment.path.join(environment.logDir, "desktop.trace.ndjson"); | |
| 399 | }).pipe(Effect.provide(environmentLayer)); | |
| 400 | ||
| 401 | yield* Effect.scoped( | |
| 402 | Effect.logInfo("desktop log export").pipe( | |
| 403 | Effect.withSpan("desktop-log-export-test"), | |
| 404 | Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))), | |
| 405 | ), | |
| 406 | ); | |
| 407 | ||
| 408 | assert.lengthOf(requests, 1); | |
| 409 | const [request] = requests; | |
| 410 | assert.strictEqual(request?.url, "https://collector.example.com/v1/logs"); | |
| 411 | assert.include(request?.body ?? "", "desktop log export"); | |
| 412 | assert.include(request?.body ?? "", "service.runtime"); | |
| 413 | assert.strictEqual(request?.headers["x-scope"], "desktop"); | |
| 414 | ||
| 415 | // The log record is the export now, so the same message must not also | |
| 416 | // ride along as an event on the span. | |
| 417 | const record = (yield* fileSystem.readFileString(tracePath)) | |
| 418 | .trim() | |
| 419 | .split("\n") | |
| 420 | .filter((line) => line.length > 0) | |
| 421 | .map((line) => decodeTraceRecordLine(line)) | |
| 422 | .find((entry) => entry.name === "desktop-log-export-test"); | |
| 423 | assert.notEqual(record, undefined); | |
| 424 | assert.lengthOf(record?.events ?? [], 0); | |
| 425 | }).pipe( | |
| 426 | Effect.scoped, | |
| 427 | Effect.provide(Layer.mergeAll(NodeServices.layer, collectorLayer(requests))), | |
| 428 | ); | |
| 429 | }); | |
| 430 | ||
| 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", () => { | |
| 458 | const requests: Array<ExportedRequest> = []; | |
| 459 | return Effect.gen(function* () { | |
| 460 | const fileSystem = yield* FileSystem.FileSystem; | |
| 461 | const baseDir = yield* fileSystem.makeTempDirectoryScoped({ | |
| 462 | prefix: "t3-desktop-observability-test-", | |
| 463 | }); | |
| 464 | const environmentLayer = makeEnvironmentLayer(baseDir); | |
| 465 | yield* writeObservabilitySettings(environmentLayer, { | |
| 466 | otlpLogsUrl: "https://settings.example.com/v1/logs", | |
| 467 | otlpMetricsUrl: "https://settings.example.com/v1/metrics", | |
| 468 | }); | |
| 469 | ||
| 470 | 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( | |
| 475 | Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))), | |
| 476 | ), | |
| 477 | ); | |
| 478 | ||
| 479 | assert.deepEqual(requests.map((request) => request.url).toSorted(), [ | |
| 480 | "https://settings.example.com/v1/logs", | |
| 481 | "https://settings.example.com/v1/metrics", | |
| 482 | ]); | |
| 483 | assert.include( | |
| 484 | requests.find((request) => request.url.endsWith("/v1/logs"))?.body ?? "", | |
| 485 | "desktop log export from settings", | |
| 486 | ); | |
| 487 | assert.include( | |
| 488 | requests.find((request) => request.url.endsWith("/v1/metrics"))?.body ?? "", | |
| 489 | "desktop_settings_export_test_total", | |
| 490 | ); | |
| 491 | }).pipe( | |
| 492 | Effect.scoped, | |
| 493 | Effect.provide(Layer.mergeAll(NodeServices.layer, collectorLayer(requests))), | |
| 494 | ); | |
| 495 | }); | |
| 496 | ||
| 497 | it.effect("stays off the network when no endpoint is configured", () => { | |
| 498 | const requests: Array<ExportedRequest> = []; | |
| 499 | return Effect.gen(function* () { | |
| 500 | const fileSystem = yield* FileSystem.FileSystem; | |
| 501 | const baseDir = yield* fileSystem.makeTempDirectoryScoped({ | |
| 502 | prefix: "t3-desktop-observability-test-", | |
| 503 | }); | |
| 504 | ||
| 505 | yield* Effect.scoped( | |
| 506 | Effect.logInfo("desktop log stays local").pipe( | |
| 507 | Effect.withSpan("desktop-offline-test"), | |
| 508 | Effect.provide( | |
| 509 | DesktopObservability.layer.pipe(Layer.provideMerge(makeEnvironmentLayer(baseDir))), | |
| 510 | ), | |
| 511 | ), | |
| 512 | ); | |
| 513 | ||
| 514 | assert.lengthOf(requests, 0); | |
| 515 | }).pipe( | |
| 516 | Effect.scoped, | |
| 517 | Effect.provide(Layer.mergeAll(NodeServices.layer, collectorLayer(requests))), | |
| 518 | ); | |
| 519 | }); | |
| 331 | 520 | }); |
apps/desktop/src/app/DesktopObservability.ts+114-55| ... | ... | @@ -4,7 +4,10 @@ import { |
| 4 | 4 | makeTraceSink, |
| 5 | 5 | otlpSerializationLayer, |
| 6 | 6 | } from "@t3tools/shared/observability"; |
| 7 | import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings"; | |
| 7 | import { | |
| 8 | parsePersistedServerObservabilitySettings, | |
| 9 | type PersistedServerObservabilitySettings, | |
| 10 | } from "@t3tools/shared/serverSettings"; | |
| 8 | 11 | import * as Context from "effect/Context"; |
| 9 | 12 | import * as DateTime from "effect/DateTime"; |
| 10 | 13 | import * as Effect from "effect/Effect"; |
| ... | ... | @@ -21,7 +24,7 @@ import * as Scope from "effect/Scope"; |
| 21 | 24 | import * as Semaphore from "effect/Semaphore"; |
| 22 | 25 | import * as SynchronizedRef from "effect/SynchronizedRef"; |
| 23 | 26 | import * as Tracer from "effect/Tracer"; |
| 24 | import { OtlpExporter, OtlpTracer } from "effect/unstable/observability"; | |
| 27 | import { OtlpExporter, OtlpLogger, OtlpMetrics, OtlpTracer } from "effect/unstable/observability"; | |
| 25 | 28 | |
| 26 | 29 | import * as DesktopEnvironment from "./DesktopEnvironment.ts"; |
| 27 | 30 | |
| ... | ... | @@ -326,28 +329,37 @@ const makeRotatingLogFileWriter = Effect.fn("makeRotatingLogFileWriter")(functio |
| 326 | 329 | } satisfies RotatingLogFileWriter; |
| 327 | 330 | }); |
| 328 | 331 | |
| 329 | const readPersistedOtlpTracesUrl: Effect.Effect< | |
| 330 | Option.Option<string>, | |
| 332 | const noPersistedObservabilitySettings: PersistedServerObservabilitySettings = { | |
| 333 | otlpTracesUrl: undefined, | |
| 334 | otlpMetricsUrl: undefined, | |
| 335 | otlpLogsUrl: undefined, | |
| 336 | }; | |
| 337 | ||
| 338 | const readPersistedObservabilitySettings: Effect.Effect< | |
| 339 | PersistedServerObservabilitySettings, | |
| 331 | 340 | never, |
| 332 | 341 | FileSystem.FileSystem | DesktopEnvironment.DesktopEnvironment |
| 333 | 342 | > = Effect.gen(function* () { |
| 334 | 343 | const fileSystem = yield* FileSystem.FileSystem; |
| 335 | 344 | const environment = yield* DesktopEnvironment.DesktopEnvironment; |
| 336 | 345 | const raw = yield* fileSystem.readFileString(environment.serverSettingsPath).pipe(Effect.option); |
| 337 | if (Option.isNone(raw)) { | |
| 338 | return Option.none(); | |
| 339 | } | |
| 340 | ||
| 341 | const parsed = parsePersistedServerObservabilitySettings(raw.value); | |
| 342 | return Option.fromNullishOr(parsed.otlpTracesUrl); | |
| 346 | return Option.isNone(raw) | |
| 347 | ? noPersistedObservabilitySettings | |
| 348 | : parsePersistedServerObservabilitySettings(raw.value); | |
| 343 | 349 | }); |
| 344 | 350 | |
| 345 | const resolveOtlpTracesUrl = Effect.gen(function* () { | |
| 351 | /** | |
| 352 | * Settings is read once for all three signals, so the main process cannot | |
| 353 | * resolve traces against one revision of the file and logs against another. | |
| 354 | */ | |
| 355 | const resolveOtlpEndpoints = Effect.gen(function* () { | |
| 346 | 356 | const environment = yield* DesktopEnvironment.DesktopEnvironment; |
| 347 | if (Option.isSome(environment.otlpTracesUrl)) { | |
| 348 | return environment.otlpTracesUrl; | |
| 349 | } | |
| 350 | return yield* readPersistedOtlpTracesUrl; | |
| 357 | const persisted = yield* readPersistedObservabilitySettings; | |
| 358 | return { | |
| 359 | traces: Option.getOrUndefined(environment.otlpTracesUrl) ?? persisted.otlpTracesUrl, | |
| 360 | metrics: Option.getOrUndefined(environment.otlpMetricsUrl) ?? persisted.otlpMetricsUrl, | |
| 361 | logs: Option.getOrUndefined(environment.otlpLogsUrl) ?? persisted.otlpLogsUrl, | |
| 362 | }; | |
| 351 | 363 | }); |
| 352 | 364 | |
| 353 | 365 | const writeDevelopmentConsoleOutput = ( |
| ... | ... | @@ -567,53 +579,100 @@ const backendOutputLogFactoryLayer = Layer.effect( |
| 567 | 579 | }), |
| 568 | 580 | ); |
| 569 | 581 | |
| 570 | const desktopLoggerLayer = Layer.mergeAll( | |
| 571 | Logger.layer([Logger.consolePretty(), Logger.tracerLogger], { mergeWithExisting: false }), | |
| 572 | Layer.succeed(References.MinimumLogLevel, "Info"), | |
| 573 | ); | |
| 574 | ||
| 575 | const tracerLayer = Layer.unwrap( | |
| 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. | |
| 586 | */ | |
| 587 | const telemetryLayer = Layer.unwrap( | |
| 576 | 588 | Effect.gen(function* () { |
| 577 | 589 | const environment = yield* DesktopEnvironment.DesktopEnvironment; |
| 578 | const otlpTracesUrl = yield* resolveOtlpTracesUrl; | |
| 579 | const tracePath = environment.path.join(environment.logDir, "desktop.trace.ndjson"); | |
| 580 | const sink = yield* makeTraceSink({ | |
| 581 | filePath: tracePath, | |
| 582 | maxBytes: DESKTOP_LOG_FILE_MAX_BYTES, | |
| 583 | maxFiles: DESKTOP_LOG_FILE_MAX_FILES, | |
| 584 | batchWindowMs: DESKTOP_TRACE_BATCH_WINDOW_MS, | |
| 585 | }); | |
| 586 | const delegate = Option.isNone(otlpTracesUrl) | |
| 587 | ? undefined | |
| 588 | : yield* OtlpTracer.make({ | |
| 589 | url: otlpTracesUrl.value, | |
| 590 | exportInterval: `${environment.otlpExportIntervalMs} millis`, | |
| 591 | headers: Option.getOrUndefined(environment.otlpHeaders), | |
| 592 | resource: { | |
| 593 | serviceName: "desktop", | |
| 594 | attributes: { | |
| 595 | "service.runtime": "desktop", | |
| 596 | "service.mode": environment.isDevelopment ? "development" : "packaged", | |
| 597 | }, | |
| 598 | }, | |
| 599 | }).pipe(Effect.provide(otlpSerializationLayer(environment.otlpProtocol))); | |
| 600 | const tracer = yield* makeLocalFileTracer({ | |
| 601 | filePath: tracePath, | |
| 602 | maxBytes: DESKTOP_LOG_FILE_MAX_BYTES, | |
| 603 | maxFiles: DESKTOP_LOG_FILE_MAX_FILES, | |
| 604 | batchWindowMs: DESKTOP_TRACE_BATCH_WINDOW_MS, | |
| 605 | sink, | |
| 606 | ...(delegate ? { delegate } : {}), | |
| 607 | }); | |
| 590 | const endpoints = yield* resolveOtlpEndpoints; | |
| 591 | const headers = Option.getOrUndefined(environment.otlpHeaders); | |
| 592 | const serializationLayer = otlpSerializationLayer(environment.otlpProtocol); | |
| 593 | const resource = { | |
| 594 | serviceName: "desktop", | |
| 595 | attributes: { | |
| 596 | "service.runtime": "desktop", | |
| 597 | "service.mode": environment.isDevelopment ? "development" : "packaged", | |
| 598 | }, | |
| 599 | }; | |
| 608 | 600 | |
| 609 | return Layer.succeed(Tracer.Tracer, tracer); | |
| 601 | // `Logger.layer` writes the whole logger set rather than adding to it, so | |
| 602 | // every logger the main process wants has to be named in this one call. | |
| 603 | // Splitting the OTLP logger back out into a layer of its own silently | |
| 604 | // drops either it or the console output. | |
| 605 | // | |
| 606 | // Swapping `Logger.tracerLogger` out for the OTLP logger matches the | |
| 607 | // server: both reach a collector, but the tracer logger covers only | |
| 608 | // messages logged inside a recorded span and files them under traces, | |
| 609 | // while the OTLP logger carries every message as a log record stamped | |
| 610 | // with its trace and span ids. Keeping both would export every in-span | |
| 611 | // message twice. | |
| 612 | const loggerLayer = Logger.layer( | |
| 613 | endpoints.logs === undefined | |
| 614 | ? [Logger.consolePretty(), Logger.tracerLogger] | |
| 615 | : [ | |
| 616 | Logger.consolePretty(), | |
| 617 | OtlpLogger.make({ | |
| 618 | url: endpoints.logs, | |
| 619 | exportInterval: `${environment.otlpExportIntervalMs} millis`, | |
| 620 | headers, | |
| 621 | resource, | |
| 622 | }), | |
| 623 | ], | |
| 624 | { mergeWithExisting: false }, | |
| 625 | ).pipe(Layer.provide(OtlpExporter.layerFlusher), Layer.provide(serializationLayer)); | |
| 626 | ||
| 627 | const tracerLayer = Layer.unwrap( | |
| 628 | Effect.gen(function* () { | |
| 629 | const tracePath = environment.path.join(environment.logDir, "desktop.trace.ndjson"); | |
| 630 | const sink = yield* makeTraceSink({ | |
| 631 | filePath: tracePath, | |
| 632 | maxBytes: DESKTOP_LOG_FILE_MAX_BYTES, | |
| 633 | maxFiles: DESKTOP_LOG_FILE_MAX_FILES, | |
| 634 | batchWindowMs: DESKTOP_TRACE_BATCH_WINDOW_MS, | |
| 635 | }); | |
| 636 | const delegate = | |
| 637 | endpoints.traces === undefined | |
| 638 | ? undefined | |
| 639 | : yield* OtlpTracer.make({ | |
| 640 | url: endpoints.traces, | |
| 641 | exportInterval: `${environment.otlpExportIntervalMs} millis`, | |
| 642 | headers, | |
| 643 | resource, | |
| 644 | }).pipe(Effect.provide(serializationLayer)); | |
| 645 | const tracer = yield* makeLocalFileTracer({ | |
| 646 | filePath: tracePath, | |
| 647 | maxBytes: DESKTOP_LOG_FILE_MAX_BYTES, | |
| 648 | maxFiles: DESKTOP_LOG_FILE_MAX_FILES, | |
| 649 | batchWindowMs: DESKTOP_TRACE_BATCH_WINDOW_MS, | |
| 650 | sink, | |
| 651 | ...(delegate ? { delegate } : {}), | |
| 652 | }); | |
| 653 | ||
| 654 | return Layer.succeed(Tracer.Tracer, tracer); | |
| 655 | }), | |
| 656 | ).pipe(Layer.provide(OtlpExporter.layerFlusher)); | |
| 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); | |
| 610 | 669 | }), |
| 611 | ).pipe(Layer.provide(OtlpExporter.layerFlusher)); | |
| 670 | ); | |
| 612 | 671 | |
| 613 | 672 | export const layer = Layer.mergeAll( |
| 614 | 673 | backendOutputLogFactoryLayer, |
| 615 | desktopLoggerLayer, | |
| 616 | tracerLayer, | |
| 674 | telemetryLayer, | |
| 675 | Layer.succeed(References.MinimumLogLevel, "Info"), | |
| 617 | 676 | Layer.succeed(Tracer.MinimumTraceLevel, "Info"), |
| 618 | 677 | Layer.succeed(References.TracerTimingEnabled, true), |
| 619 | 678 | ); |
docs/operations/observability.md+7| ... | ... | @@ -522,6 +522,13 @@ It provides: |
| 522 | 522 | - optional OTLP log exporter |
| 523 | 523 | - Effect trace-level and timing refs |
| 524 | 524 | |
| 525 | The desktop main process is a second producer, assembled in | |
| 526 | `apps/desktop/src/app/DesktopObservability.ts`. It reads the same `T3CODE_OTLP_*` names and the same | |
| 527 | Settings entries as the backend it supervises, and covers work the backend cannot see: app startup, | |
| 528 | window and menu handling, backend supervision, and updates. It reports as service `desktop` | |
| 529 | regardless of `T3CODE_OTLP_SERVICE_NAME`, so a collector shows it alongside the backend rather than | |
| 530 | mixed into it. | |
| 531 | ||
| 525 | 532 | ### Env Vars |
| 526 | 533 | |
| 527 | 534 | Local trace file: |