1import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { assert, describe, it } from "@effect/vitest";
4import * as ConfigProvider from "effect/ConfigProvider";
5import * as Effect from "effect/Effect";
6import * as FileSystem from "effect/FileSystem";
7import * as Layer from "effect/Layer";
8import * as Schema from "effect/Schema";
9import * as HttpClient from "effect/http/HttpClient";
10import * as HttpClientResponse from "effect/http/HttpClientResponse";
11
12import * as DesktopConfig from "./DesktopConfig.ts";
13import * as DesktopEnvironment from "./DesktopEnvironment.ts";
14import * as DesktopObservability from "./DesktopObservability.ts";
15
16const DesktopBackendChildLogRecord = Schema.Struct({
17 message: Schema.String,
18 level: Schema.Literals(["INFO", "ERROR"]),
19 timestamp: Schema.String,
20 annotations: Schema.Record(Schema.String, Schema.Unknown),
21 spans: Schema.Record(Schema.String, Schema.Unknown),
22 fiberId: Schema.String,
23});
24
25const decodeDesktopBackendChildLogRecord = Schema.decodeEffect(
26 Schema.fromJsonString(DesktopBackendChildLogRecord),
27);
28
29const TraceRecordLine = Schema.Struct({
30 name: Schema.String,
31 attributes: Schema.Record(Schema.String, Schema.Unknown),
32 events: Schema.Array(
33 Schema.Struct({
34 name: Schema.String,
35 attributes: Schema.Record(Schema.String, Schema.Unknown),
36 }),
37 ),
38});
39
40const decodeTraceRecordLine = Schema.decodeUnknownSync(Schema.fromJsonString(TraceRecordLine));
41
42const environmentInput = (baseDir: string) =>
43 ({
44 dirname: "/repo/apps/desktop/dist-electron",
45 homeDirectory: baseDir,
46 platform: "darwin",
47 processArch: "arm64",
48 appVersion: "1.2.3",
49 appPath: "/repo",
50 isPackaged: false,
51 resourcesPath: "/repo/resources",
52 runningUnderArm64Translation: false,
53 }) satisfies DesktopEnvironment.MakeDesktopEnvironmentInput;
54
55const layerEnvironmentFor = (
56 baseDir: string,
57 isDevelopment = true,
58 env: Readonly<Record<string, string | undefined>> = {},
59) =>
60 DesktopEnvironment.layer(environmentInput(baseDir)).pipe(
61 Layer.provide(
62 Layer.mergeAll(
63 NodeServices.layer,
64 DesktopConfig.layerTest({
65 T3CODE_HOME: baseDir,
66 VITE_DEV_SERVER_URL: isDevelopment ? "http://127.0.0.1:5733" : undefined,
67 ...env,
68 }),
69 ),
70 ),
71 );
72
73interface 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. */
80const layerCollector = (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// A developer's own OTEL_* variables would otherwise pick the endpoints.
97const layerEmptyEnv = ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} }));
98
99const encodeObservabilitySettingsFile = Schema.encodeSync(
100 Schema.fromJsonString(
101 Schema.Struct({ observability: Schema.Record(Schema.String, Schema.String) }),
102 ),
103);
104
105const writeObservabilitySettings = Effect.fn(function* (
106 environmentLayer: ReturnType<typeof layerEnvironmentFor>,
107 observability: Readonly<Record<string, string>>,
108) {
109 const fileSystem = yield* FileSystem.FileSystem;
110 const { path, serverSettingsPath } = yield* Effect.gen(function* () {
111 const environment = yield* DesktopEnvironment.DesktopEnvironment;
112 return environment;
113 }).pipe(Effect.provide(environmentLayer));
114 yield* fileSystem.makeDirectory(path.dirname(serverSettingsPath), { recursive: true });
115 yield* fileSystem.writeFileString(
116 serverSettingsPath,
117 encodeObservabilitySettingsFile({ observability }),
118 );
119});
120
121describe("DesktopObservability", () => {
122 it("advances a retained output offset instead of repeatedly copying a full head chunk", () => {
123 const maxBufferedBytes = 1024 * 1024;
124 const initial = DesktopObservability.appendBoundedOutputChunk(
125 {
126 runId: "test-run",
127 startDetails: "pid=123",
128 chunks: [],
129 byteLength: 0,
130 },
131 "stderr",
132 new Uint8Array(maxBufferedBytes),
133 );
134 const initialBackingBuffer = initial.chunks[0]?.chunk.buffer;
135
136 const next = DesktopObservability.appendBoundedOutputChunk(initial, "stderr", Uint8Array.of(1));
137
138 assert.equal(next.chunks[0]?.chunk.buffer, initialBackingBuffer);
139 assert.equal(next.chunks[0]?.offset, 1);
140 assert.equal(next.byteLength, maxBufferedBytes);
141 });
142
143 it.effect("persists desktop Effect logs as span events in desktop.trace.ndjson", () =>
144 Effect.gen(function* () {
145 const fileSystem = yield* FileSystem.FileSystem;
146 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
147 prefix: "t3-desktop-observability-test-",
148 });
149 const layerEnvironment = layerEnvironmentFor(baseDir);
150 const tracePath = yield* Effect.gen(function* () {
151 const environment = yield* DesktopEnvironment.DesktopEnvironment;
152 return environment.path.join(environment.logDir, "desktop.trace.ndjson");
153 }).pipe(Effect.provide(layerEnvironment));
154 const logPath = yield* Effect.gen(function* () {
155 const environment = yield* DesktopEnvironment.DesktopEnvironment;
156 return environment.path.join(environment.logDir, "desktop-main.log");
157 }).pipe(Effect.provide(layerEnvironment));
158
159 yield* Effect.scoped(
160 Effect.gen(function* () {
161 yield* Effect.annotateCurrentSpan({ "desktop.test": true });
162 yield* Effect.logInfo("desktop trace event");
163 }).pipe(
164 Effect.withSpan("desktop-observability-test"),
165 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(layerEnvironment))),
166 ),
167 );
168
169 const records = (yield* fileSystem.readFileString(tracePath))
170 .trim()
171 .split("\n")
172 .filter((line) => line.length > 0)
173 .map((line) => decodeTraceRecordLine(line));
174 const record = records.find((entry) => entry.name === "desktop-observability-test");
175
176 assert.notEqual(record, undefined);
177 if (!record) {
178 return;
179 }
180 assert.equal(record.attributes["desktop.test"], true);
181 assert.equal(
182 record.events.some((event) => event.name === "desktop trace event"),
183 true,
184 );
185 assert.isFalse(yield* fileSystem.exists(logPath));
186 }).pipe(
187 Effect.scoped,
188 Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici, layerEmptyEnv)),
189 ),
190 );
191
192 it.effect("buffers backend child output and persists it only when a failure is reported", () =>
193 Effect.gen(function* () {
194 const fileSystem = yield* FileSystem.FileSystem;
195 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
196 prefix: "t3-desktop-backend-output-log-test-",
197 });
198 const layerEnvironment = layerEnvironmentFor(baseDir, false);
199 const logPath = yield* Effect.gen(function* () {
200 const environment = yield* DesktopEnvironment.DesktopEnvironment;
201 return environment.path.join(environment.logDir, "server-child.log");
202 }).pipe(Effect.provide(layerEnvironment));
203 const tracePath = yield* Effect.gen(function* () {
204 const environment = yield* DesktopEnvironment.DesktopEnvironment;
205 return environment.path.join(environment.logDir, "desktop.trace.ndjson");
206 }).pipe(Effect.provide(layerEnvironment));
207
208 yield* Effect.gen(function* () {
209 const factory = yield* DesktopObservability.DesktopBackendOutputLogFactory;
210 const outputLog = yield* factory.forInstance("primary");
211 yield* outputLog.beginSession({
212 details: "pid=123 port=3773 cwd=/repo",
213 });
214 yield* outputLog.writeOutputChunk("stdout", new TextEncoder().encode("hello server\n"));
215 assert.isFalse(yield* fileSystem.exists(logPath));
216 yield* outputLog.persistFailure({ details: "code=1" });
217 yield* outputLog.beginSession({ details: "pid=456" });
218 yield* outputLog.writeOutputChunk("stderr", new TextEncoder().encode("normal shutdown\n"));
219 yield* outputLog.discardSession;
220 }).pipe(
221 Effect.annotateLogs({ runId: "test-run" }),
222 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(layerEnvironment))),
223 );
224
225 const log = yield* fileSystem.readFileString(logPath);
226 const lines = log.trimEnd().split("\n");
227 const start = yield* decodeDesktopBackendChildLogRecord(lines[0] ?? "");
228 const output = yield* decodeDesktopBackendChildLogRecord(lines[1] ?? "");
229 const end = yield* decodeDesktopBackendChildLogRecord(lines[2] ?? "");
230
231 assert.equal(lines.length, 3);
232 assert.equal(start.message, "backend child process failure output start");
233 assert.equal(start.level, "ERROR");
234 assert.equal(start.annotations.component, "desktop-backend-child");
235 assert.equal(start.annotations.runId, "test-run");
236 assert.equal(start.annotations.instanceId, "primary");
237 assert.equal(start.annotations.phase, "START");
238 assert.equal(start.annotations.details, "pid=123 port=3773 cwd=/repo");
239
240 assert.equal(output.message, "backend child process output");
241 assert.equal(output.level, "INFO");
242 assert.equal(output.annotations.component, "desktop-backend-child");
243 assert.equal(output.annotations.runId, "test-run");
244 assert.equal(output.annotations.instanceId, "primary");
245 assert.equal(output.annotations.stream, "stdout");
246 assert.equal(output.annotations.text, "hello server\n");
247
248 assert.equal(end.message, "backend child process failure output end");
249 assert.equal(end.level, "ERROR");
250 assert.equal(end.annotations.instanceId, "primary");
251 assert.equal(end.annotations.phase, "END");
252 assert.equal(end.annotations.details, "code=1");
253
254 const traceRecords = (yield* fileSystem.readFileString(tracePath))
255 .trim()
256 .split("\n")
257 .filter((line) => line.length > 0)
258 .map((line) => decodeTraceRecordLine(line));
259 assert.isFalse(
260 traceRecords.some(
261 (record) => record.name === "desktop.observability.backendOutput.writeOutputChunk",
262 ),
263 );
264 }).pipe(
265 Effect.scoped,
266 Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici, layerEmptyEnv)),
267 ),
268 );
269
270 it.effect("keeps buffering output after a non-terminal failure snapshot", () =>
271 Effect.gen(function* () {
272 const fileSystem = yield* FileSystem.FileSystem;
273 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
274 prefix: "t3-desktop-backend-output-snapshot-test-",
275 });
276 const layerEnvironment = layerEnvironmentFor(baseDir, false);
277 const logPath = yield* Effect.gen(function* () {
278 const environment = yield* DesktopEnvironment.DesktopEnvironment;
279 return environment.path.join(environment.logDir, "server-child.log");
280 }).pipe(Effect.provide(layerEnvironment));
281
282 yield* Effect.gen(function* () {
283 const factory = yield* DesktopObservability.DesktopBackendOutputLogFactory;
284 const outputLog = yield* factory.forInstance("primary");
285 yield* outputLog.beginSession({ details: "pid=123" });
286 yield* outputLog.writeOutputChunk("stdout", new TextEncoder().encode("before timeout\n"));
287 yield* outputLog.persistFailureSnapshot({ details: "readiness timeout" });
288 yield* outputLog.writeOutputChunk("stderr", new TextEncoder().encode("after timeout\n"));
289 yield* outputLog.persistFailure({ details: "code=1" });
290 }).pipe(
291 Effect.annotateLogs({ runId: "test-run" }),
292 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(layerEnvironment))),
293 );
294
295 const records = yield* Effect.forEach(
296 (yield* fileSystem.readFileString(logPath)).trimEnd().split("\n"),
297 (line) => decodeDesktopBackendChildLogRecord(line),
298 );
299 assert.equal(
300 records.some((record) => record.annotations.text === "after timeout\n"),
301 true,
302 );
303 assert.equal(records.at(-1)?.annotations.details, "code=1");
304 }).pipe(
305 Effect.scoped,
306 Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici, layerEmptyEnv)),
307 ),
308 );
309
310 it.effect("retains only the last mebibyte of backend child output", () =>
311 Effect.gen(function* () {
312 const fileSystem = yield* FileSystem.FileSystem;
313 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
314 prefix: "t3-desktop-backend-output-bound-test-",
315 });
316 const layerEnvironment = layerEnvironmentFor(baseDir, false);
317 const logPath = yield* Effect.gen(function* () {
318 const environment = yield* DesktopEnvironment.DesktopEnvironment;
319 return environment.path.join(environment.logDir, "server-child.log");
320 }).pipe(Effect.provide(layerEnvironment));
321 const maxBufferedBytes = 1024 * 1024;
322 const discardedPrefixBytes = 128;
323 const output = new Uint8Array(maxBufferedBytes + discardedPrefixBytes);
324 output.fill("x".charCodeAt(0));
325 output.fill("y".charCodeAt(0), 0, discardedPrefixBytes);
326
327 yield* Effect.scoped(
328 Effect.gen(function* () {
329 const factory = yield* DesktopObservability.DesktopBackendOutputLogFactory;
330 const outputLog = yield* factory.forInstance("primary");
331 yield* outputLog.beginSession({ details: "pid=123" });
332 yield* outputLog.writeOutputChunk("stderr", output);
333 yield* outputLog.persistFailure({ details: "code=1" });
334 }).pipe(
335 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(layerEnvironment))),
336 ),
337 );
338
339 const lines = (yield* fileSystem.readFileString(logPath)).trimEnd().split("\n");
340 const record = yield* decodeDesktopBackendChildLogRecord(lines[1] ?? "");
341 const text = record.annotations.text;
342 assert.equal(typeof text, "string");
343 if (typeof text !== "string") {
344 return;
345 }
346 assert.equal(new TextEncoder().encode(text).byteLength, maxBufferedBytes);
347 assert.isFalse(text.includes("y"));
348 }).pipe(
349 Effect.scoped,
350 Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici, layerEmptyEnv)),
351 ),
352 );
353
354 it.effect("bounds the number of retained backend child output chunks", () =>
355 Effect.gen(function* () {
356 const fileSystem = yield* FileSystem.FileSystem;
357 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
358 prefix: "t3-desktop-backend-output-chunks-test-",
359 });
360 const layerEnvironment = layerEnvironmentFor(baseDir, false);
361 const logPath = yield* Effect.gen(function* () {
362 const environment = yield* DesktopEnvironment.DesktopEnvironment;
363 return environment.path.join(environment.logDir, "server-child.log");
364 }).pipe(Effect.provide(layerEnvironment));
365
366 yield* Effect.scoped(
367 Effect.gen(function* () {
368 const factory = yield* DesktopObservability.DesktopBackendOutputLogFactory;
369 const outputLog = yield* factory.forInstance("primary");
370 yield* outputLog.beginSession({ details: "pid=123" });
371 for (let index = 0; index < 300; index += 1) {
372 yield* outputLog.writeOutputChunk("stderr", Uint8Array.of(index % 128));
373 }
374 yield* outputLog.persistFailure({ details: "code=1" });
375 }).pipe(
376 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(layerEnvironment))),
377 ),
378 );
379
380 const lines = (yield* fileSystem.readFileString(logPath)).trimEnd().split("\n");
381 assert.equal(lines.length, 258);
382 }).pipe(
383 Effect.scoped,
384 Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici, layerEmptyEnv)),
385 ),
386 );
387
388 it.effect("exports main process log records to the configured logs endpoint", () => {
389 const requests: Array<ExportedRequest> = [];
390 return Effect.gen(function* () {
391 const fileSystem = yield* FileSystem.FileSystem;
392 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
393 prefix: "t3-desktop-observability-test-",
394 });
395 const layerEnvironment = layerEnvironmentFor(baseDir, true, {
396 T3CODE_OTLP_LOGS_URL: "https://collector.example.com/v1/logs",
397 T3CODE_OTLP_HEADERS: "x-scope=desktop",
398 });
399 const tracePath = yield* Effect.gen(function* () {
400 const environment = yield* DesktopEnvironment.DesktopEnvironment;
401 return environment.path.join(environment.logDir, "desktop.trace.ndjson");
402 }).pipe(Effect.provide(layerEnvironment));
403
404 yield* Effect.scoped(
405 Effect.logInfo("desktop log export").pipe(
406 Effect.withSpan("desktop-log-export-test"),
407 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(layerEnvironment))),
408 ),
409 );
410
411 assert.lengthOf(requests, 1);
412 const [request] = requests;
413 assert.strictEqual(request?.url, "https://collector.example.com/v1/logs");
414 assert.include(request?.body ?? "", "desktop log export");
415 assert.include(request?.body ?? "", "service.runtime");
416 assert.strictEqual(request?.headers["x-scope"], "desktop");
417
418 // The log record is the export now, so the same message must not also
419 // ride along as an event on the span.
420 const record = (yield* fileSystem.readFileString(tracePath))
421 .trim()
422 .split("\n")
423 .filter((line) => line.length > 0)
424 .map((line) => decodeTraceRecordLine(line))
425 .find((entry) => entry.name === "desktop-log-export-test");
426 assert.notEqual(record, undefined);
427 assert.lengthOf(record?.events ?? [], 0);
428 }).pipe(
429 Effect.scoped,
430 Effect.provide(Layer.mergeAll(NodeServices.layer, layerCollector(requests), layerEmptyEnv)),
431 );
432 });
433
434 it.effect("exports to an OTEL endpoint over Settings, with its own headers and protocol", () => {
435 const requests: Array<ExportedRequest> = [];
436 return Effect.gen(function* () {
437 const fileSystem = yield* FileSystem.FileSystem;
438 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
439 prefix: "t3-desktop-observability-test-",
440 });
441 const layerEnvironment = layerEnvironmentFor(baseDir, true, {
442 T3CODE_OTLP_HEADERS: "x-scope=desktop",
443 });
444 yield* writeObservabilitySettings(layerEnvironment, {
445 otlpLogsUrl: "https://settings.example.com/v1/logs",
446 });
447
448 yield* Effect.scoped(
449 Effect.logInfo("desktop otel export").pipe(
450 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(layerEnvironment))),
451 ),
452 );
453
454 assert.lengthOf(requests, 1);
455 const [request] = requests;
456 assert.strictEqual(request?.url, "https://collector.example.com/v1/logs");
457 assert.strictEqual(request?.headers["x-otel"], "desktop");
458 assert.strictEqual(request?.headers["x-scope"], undefined);
459 assert.strictEqual(request?.headers["content-type"], "application/json");
460 }).pipe(
461 Effect.scoped,
462 Effect.provide(
463 Layer.mergeAll(
464 NodeServices.layer,
465 layerCollector(requests),
466 ConfigProvider.layer(
467 ConfigProvider.fromEnv({
468 env: {
469 OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector.example.com",
470 OTEL_EXPORTER_OTLP_HEADERS: "x-otel=desktop",
471 OTEL_EXPORTER_OTLP_LOGS_PROTOCOL: "http/json",
472 },
473 }),
474 ),
475 ),
476 ),
477 );
478 });
479
480 it.effect("keeps its service name while OTEL resource attributes add dimensions", () => {
481 const requests: Array<ExportedRequest> = [];
482 return Effect.gen(function* () {
483 const fileSystem = yield* FileSystem.FileSystem;
484 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
485 prefix: "t3-desktop-observability-test-",
486 });
487 const layerEnvironment = layerEnvironmentFor(baseDir, true, {
488 T3CODE_OTLP_LOGS_URL: "https://collector.example.com/v1/logs",
489 });
490
491 yield* Effect.scoped(
492 Effect.logInfo("desktop service name").pipe(
493 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(layerEnvironment))),
494 ),
495 );
496
497 assert.lengthOf(requests, 1);
498 const body = requests[0]?.body ?? "";
499 assert.include(body, '"stringValue":"t3code-desktop"');
500 assert.include(body, "deployment.environment.name");
501 assert.include(body, '"key":"service.namespace","value":{"stringValue":"t3code"}');
502 assert.notInclude(body, "renamed");
503 }).pipe(
504 Effect.scoped,
505 Effect.provide(
506 Layer.mergeAll(
507 NodeServices.layer,
508 layerCollector(requests),
509 ConfigProvider.layer(
510 ConfigProvider.fromEnv({
511 env: {
512 OTEL_SERVICE_NAME: "renamed",
513 OTEL_RESOURCE_ATTRIBUTES:
514 "service.name=renamed,service.namespace=renamed,deployment.environment.name=development",
515 },
516 }),
517 ),
518 ),
519 ),
520 );
521 });
522
523 it.effect("exports nothing to Settings for logs an unusable OTEL endpoint claimed", () => {
524 const requests: Array<ExportedRequest> = [];
525 return Effect.gen(function* () {
526 const fileSystem = yield* FileSystem.FileSystem;
527 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
528 prefix: "t3-desktop-observability-test-",
529 });
530 const layerEnvironment = layerEnvironmentFor(baseDir, true, {
531 T3CODE_OTLP_HEADERS: "x-scope=desktop",
532 });
533 yield* writeObservabilitySettings(layerEnvironment, {
534 otlpLogsUrl: "https://settings.example.com/v1/logs",
535 });
536
537 yield* Effect.scoped(
538 Effect.logInfo("desktop otel off").pipe(
539 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(layerEnvironment))),
540 ),
541 );
542
543 assert.lengthOf(requests, 0);
544 }).pipe(
545 Effect.scoped,
546 Effect.provide(
547 Layer.mergeAll(
548 NodeServices.layer,
549 layerCollector(requests),
550 ConfigProvider.layer(
551 ConfigProvider.fromEnv({
552 env: {
553 OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector.example.com",
554 OTEL_EXPORTER_OTLP_LOGS_PROTOCOL: "grpc",
555 },
556 }),
557 ),
558 ),
559 ),
560 );
561 });
562
563 it.effect("exports kill switch warnings through the configured logger", () => {
564 const requests: Array<ExportedRequest> = [];
565 return Effect.gen(function* () {
566 const fileSystem = yield* FileSystem.FileSystem;
567 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
568 prefix: "t3-desktop-observability-test-",
569 });
570 const layerEnvironment = layerEnvironmentFor(baseDir, true, {
571 T3CODE_OTLP_LOGS_URL: "https://collector.example.com/v1/logs",
572 });
573
574 yield* Effect.scoped(
575 Effect.void.pipe(
576 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(layerEnvironment))),
577 ),
578 );
579
580 assert.include(requests[0]?.body ?? "", "OTEL_SDK_DISABLED=1 was read as false");
581 }).pipe(
582 Effect.scoped,
583 Effect.provide(
584 Layer.mergeAll(
585 NodeServices.layer,
586 layerCollector(requests),
587 ConfigProvider.layer(ConfigProvider.fromEnv({ env: { OTEL_SDK_DISABLED: "1" } })),
588 ),
589 ),
590 );
591 });
592
593 it.effect("reads every signal endpoint from Settings when the environment names none", () => {
594 const requests: Array<ExportedRequest> = [];
595 return Effect.gen(function* () {
596 const fileSystem = yield* FileSystem.FileSystem;
597 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
598 prefix: "t3-desktop-observability-test-",
599 });
600 const layerEnvironment = layerEnvironmentFor(baseDir);
601 yield* writeObservabilitySettings(layerEnvironment, {
602 otlpTracesUrl: "https://settings.example.com/v1/traces",
603 otlpLogsUrl: "https://settings.example.com/v1/logs",
604 // The main process records no metrics yet, so this endpoint must
605 // not produce a request.
606 otlpMetricsUrl: "https://settings.example.com/v1/metrics",
607 });
608
609 yield* Effect.scoped(
610 Effect.logInfo("desktop log export from settings").pipe(
611 Effect.withSpan("desktop-settings-export-test"),
612 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(layerEnvironment))),
613 ),
614 );
615
616 assert.deepEqual(requests.map((request) => request.url).toSorted(), [
617 "https://settings.example.com/v1/logs",
618 "https://settings.example.com/v1/traces",
619 ]);
620 assert.include(
621 requests.find((request) => request.url.endsWith("/v1/logs"))?.body ?? "",
622 "desktop log export from settings",
623 );
624 assert.include(
625 requests.find((request) => request.url.endsWith("/v1/traces"))?.body ?? "",
626 "desktop-settings-export-test",
627 );
628 }).pipe(
629 Effect.scoped,
630 Effect.provide(Layer.mergeAll(NodeServices.layer, layerCollector(requests), layerEmptyEnv)),
631 );
632 });
633
634 it.effect("stays off the network when no endpoint is configured", () => {
635 const requests: Array<ExportedRequest> = [];
636 return Effect.gen(function* () {
637 const fileSystem = yield* FileSystem.FileSystem;
638 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
639 prefix: "t3-desktop-observability-test-",
640 });
641
642 yield* Effect.scoped(
643 Effect.logInfo("desktop log stays local").pipe(
644 Effect.withSpan("desktop-offline-test"),
645 Effect.provide(
646 DesktopObservability.layer.pipe(Layer.provideMerge(layerEnvironmentFor(baseDir))),
647 ),
648 ),
649 );
650
651 assert.lengthOf(requests, 0);
652 }).pipe(
653 Effect.scoped,
654 Effect.provide(Layer.mergeAll(NodeServices.layer, layerCollector(requests), layerEmptyEnv)),
655 );
656 });
657
658 it.effect("stops every export when the OpenTelemetry SDK is disabled", () => {
659 const requests: Array<ExportedRequest> = [];
660 return Effect.gen(function* () {
661 const fileSystem = yield* FileSystem.FileSystem;
662 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
663 prefix: "t3-desktop-observability-test-",
664 });
665 const layerEnvironment = layerEnvironmentFor(baseDir);
666 yield* writeObservabilitySettings(layerEnvironment, {
667 otlpTracesUrl: "https://settings.example.com/v1/traces",
668 otlpLogsUrl: "https://settings.example.com/v1/logs",
669 });
670
671 yield* Effect.scoped(
672 Effect.logInfo("desktop log stays local when disabled").pipe(
673 Effect.withSpan("desktop-disabled-test"),
674 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(layerEnvironment))),
675 ),
676 );
677
678 assert.lengthOf(requests, 0);
679 }).pipe(
680 Effect.scoped,
681 Effect.provide(
682 Layer.mergeAll(
683 NodeServices.layer,
684 layerCollector(requests),
685 ConfigProvider.layer(ConfigProvider.fromEnv({ env: { OTEL_SDK_DISABLED: "true" } })),
686 ),
687 ),
688 );
689 });
690});