From ee4fe571270cb60fd84db1ca8ddb9b1f6750cf56 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 21 Aug 2026 04:45:46 -0400 Subject: [PATCH] feat(desktop): the process that starts the server reports what it did The desktop app is two processes and only the server was reachable, so the minutes before the server exists went unrecorded, and the main process trace exporter that did exist read one variable almost nobody sets. Signed-off-by: Yordis Prieto --- apps/desktop/src/app/DesktopConfig.ts | 9 +- .../src/app/DesktopEnvironment.test.ts | 2 +- apps/desktop/src/app/DesktopEnvironment.ts | 16 +- apps/desktop/src/app/DesktopObservability.ts | 206 +++++++++++++----- .../desktop/src/app/DesktopOtlpExport.test.ts | 175 +++++++++++++++ apps/desktop/src/app/DesktopOtlpExport.ts | 149 +++++++++++++ apps/server/src/bin.test.ts | 2 +- apps/server/src/cli/config.test.ts | 2 +- apps/server/src/cli/config.ts | 2 +- apps/server/src/cli/pair.ts | 2 +- apps/server/src/config.ts | 3 +- .../src/environment/ServerEnvironment.test.ts | 2 +- apps/server/src/server.test.ts | 2 +- apps/server/src/serverLogger.test.ts | 2 +- ...22-the-desktop-app-reports-its-own-work.md | 53 +++++ docs/fork/README.md | 2 + docs/operations/observability.md | 37 +++- packages/shared/package.json | 4 + .../shared/src/otelEnvironment.test.ts | 2 +- .../shared/src/otelEnvironment.ts | 19 +- 20 files changed, 610 insertions(+), 81 deletions(-) create mode 100644 apps/desktop/src/app/DesktopOtlpExport.test.ts create mode 100644 apps/desktop/src/app/DesktopOtlpExport.ts create mode 100644 docs/fork/0022-the-desktop-app-reports-its-own-work.md rename apps/server/src/observability/OtelEnvironment.test.ts => packages/shared/src/otelEnvironment.test.ts (99%) rename apps/server/src/observability/OtelEnvironment.ts => packages/shared/src/otelEnvironment.ts (96%) diff --git a/apps/desktop/src/app/DesktopConfig.ts b/apps/desktop/src/app/DesktopConfig.ts index d157a4c6ba44..9ebb814b6027 100644 --- a/apps/desktop/src/app/DesktopConfig.ts +++ b/apps/desktop/src/app/DesktopConfig.ts @@ -45,9 +45,12 @@ export const DesktopConfig = Config.all({ desktopLanHostOverride: trimmedString("T3CODE_DESKTOP_LAN_HOST"), desktopHttpsEndpointUrls: commaSeparatedStrings("T3CODE_DESKTOP_HTTPS_ENDPOINTS"), otlpTracesUrl: trimmedString("T3CODE_OTLP_TRACES_URL"), - otlpExportIntervalMs: Config.int("T3CODE_OTLP_EXPORT_INTERVAL_MS").pipe( - Config.withDefault(10_000), - ), + otlpMetricsUrl: trimmedString("T3CODE_OTLP_METRICS_URL"), + otlpLogsUrl: trimmedString("T3CODE_OTLP_LOGS_URL"), + // Left as an Option rather than defaulted here: an unset variable is what + // lets each signal fall back to the interval the OpenTelemetry environment + // asked for, which the specification defines per signal. + otlpExportIntervalMs: Config.int("T3CODE_OTLP_EXPORT_INTERVAL_MS").pipe(Config.option), appImagePath: trimmedString("APPIMAGE"), disableAutoUpdate: optionalBoolean("T3CODE_DISABLE_AUTO_UPDATE"), mockUpdates: optionalBoolean("T3CODE_DESKTOP_MOCK_UPDATES"), diff --git a/apps/desktop/src/app/DesktopEnvironment.test.ts b/apps/desktop/src/app/DesktopEnvironment.test.ts index 218e2c3e4ba2..a157ed153bca 100644 --- a/apps/desktop/src/app/DesktopEnvironment.test.ts +++ b/apps/desktop/src/app/DesktopEnvironment.test.ts @@ -78,7 +78,7 @@ describe("DesktopEnvironment", () => { assert.deepEqual(environment.configuredBackendPort, Option.some(4949)); assert.deepEqual(environment.commitHashOverride, Option.some("0123456789abcdef")); assert.deepEqual(environment.otlpTracesUrl, Option.some("http://127.0.0.1:4318/v1/traces")); - assert.equal(environment.otlpExportIntervalMs, 2500); + assert.deepEqual(environment.otlpExportIntervalMs, Option.some(2500)); }), ); diff --git a/apps/desktop/src/app/DesktopEnvironment.ts b/apps/desktop/src/app/DesktopEnvironment.ts index 4583e5124091..bdf20b83a52a 100644 --- a/apps/desktop/src/app/DesktopEnvironment.ts +++ b/apps/desktop/src/app/DesktopEnvironment.ts @@ -4,6 +4,7 @@ import type { DesktopRuntimeArch, DesktopRuntimeInfo, } from "@t3tools/contracts"; +import * as OtelEnvironment from "@t3tools/shared/otelEnvironment"; import * as Config from "effect/Config"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; @@ -68,7 +69,16 @@ export class DesktopEnvironment extends Context.Service< readonly configuredBackendPort: Option.Option; readonly commitHashOverride: Option.Option; readonly otlpTracesUrl: Option.Option; - readonly otlpExportIntervalMs: number; + readonly otlpMetricsUrl: Option.Option; + readonly otlpLogsUrl: Option.Option; + readonly otlpExportIntervalMs: Option.Option; + /** + * What the standard `OTEL_*` variables asked for. The `T3CODE_OTLP_*` + * endpoints above still win per signal; this is what the main process + * falls back to, and it carries the headers, wire format, resource, and + * batching that T3 Code has no names of its own for. + */ + readonly otelEnvironment: OtelEnvironment.OtelEnvironment; readonly branding: DesktopAppBranding; readonly displayName: string; readonly appUserModelId: string; @@ -145,6 +155,7 @@ const make = Effect.fn("desktop.environment.make")(function* ( ): Effect.fn.Return { const path = yield* Path.Path; const config = yield* DesktopConfig.DesktopConfig; + const otelEnvironment = yield* OtelEnvironment.load; const homeDirectory = input.homeDirectory; const devServerUrl = config.devServerUrl; const isDevelopment = Option.isSome(devServerUrl); @@ -220,7 +231,10 @@ const make = Effect.fn("desktop.environment.make")(function* ( configuredBackendPort: config.configuredBackendPort, commitHashOverride: config.commitHashOverride, otlpTracesUrl: config.otlpTracesUrl, + otlpMetricsUrl: config.otlpMetricsUrl, + otlpLogsUrl: config.otlpLogsUrl, otlpExportIntervalMs: config.otlpExportIntervalMs, + otelEnvironment, branding, displayName, appUserModelId: Option.getOrElse(config.appUserModelIdOverride, () => diff --git a/apps/desktop/src/app/DesktopObservability.ts b/apps/desktop/src/app/DesktopObservability.ts index c393f4ccd9a9..78be929c088c 100644 --- a/apps/desktop/src/app/DesktopObservability.ts +++ b/apps/desktop/src/app/DesktopObservability.ts @@ -1,6 +1,9 @@ import { PRIMARY_LOCAL_ENVIRONMENT_ID } from "@t3tools/contracts"; import { makeLocalFileTracer, makeTraceSink } from "@t3tools/shared/observability"; -import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings"; +import { + parsePersistedServerObservabilitySettings, + type PersistedServerObservabilitySettings, +} from "@t3tools/shared/serverSettings"; import * as Context from "effect/Context"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; @@ -17,14 +20,27 @@ import * as Scope from "effect/Scope"; import * as Semaphore from "effect/Semaphore"; import * as SynchronizedRef from "effect/SynchronizedRef"; import * as Tracer from "effect/Tracer"; -import { OtlpExporter, OtlpSerialization, OtlpTracer } from "effect/unstable/observability"; +import { + OtlpExporter, + OtlpLogger, + OtlpMetrics, + OtlpSerialization, + OtlpTracer, +} from "effect/unstable/observability"; import * as DesktopEnvironment from "./DesktopEnvironment.ts"; +import { + type DesktopOtlpResource, + type DesktopOtlpSignal, + resolveDesktopOtlpExport, +} from "./DesktopOtlpExport.ts"; const DESKTOP_LOG_FILE_MAX_BYTES = 10 * 1024 * 1024; const DESKTOP_LOG_FILE_MAX_FILES = 10; const DESKTOP_BACKEND_CHILD_LOG_FIBER_ID = "#backend-child"; const DESKTOP_TRACE_BATCH_WINDOW_MS = 1_000; +/** What the main process calls itself when nothing named the service. */ +const DESKTOP_SERVICE_NAME = "desktop"; const DESKTOP_BACKEND_OUTPUT_BUFFER_MAX_BYTES = 1024 * 1024; const DESKTOP_BACKEND_OUTPUT_BUFFER_MAX_CHUNKS = 256; @@ -322,28 +338,46 @@ const makeRotatingLogFileWriter = Effect.fn("makeRotatingLogFileWriter")(functio } satisfies RotatingLogFileWriter; }); -const readPersistedOtlpTracesUrl: Effect.Effect< - Option.Option, +const noPersistedObservabilitySettings: PersistedServerObservabilitySettings = { + otlpTracesUrl: undefined, + otlpMetricsUrl: undefined, + otlpLogsUrl: undefined, +}; + +const readPersistedObservabilitySettings: Effect.Effect< + PersistedServerObservabilitySettings, never, FileSystem.FileSystem | DesktopEnvironment.DesktopEnvironment > = Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const environment = yield* DesktopEnvironment.DesktopEnvironment; const raw = yield* fileSystem.readFileString(environment.serverSettingsPath).pipe(Effect.option); - if (Option.isNone(raw)) { - return Option.none(); - } - - const parsed = parsePersistedServerObservabilitySettings(raw.value); - return Option.fromNullishOr(parsed.otlpTracesUrl); + return Option.isNone(raw) + ? noPersistedObservabilitySettings + : parsePersistedServerObservabilitySettings(raw.value); }); -const resolveOtlpTracesUrl = Effect.gen(function* () { +/** + * Read once for all three signals, so the main process cannot resolve traces + * against one revision of Settings and logs against another. + */ +const resolveOtlpExport = Effect.gen(function* () { const environment = yield* DesktopEnvironment.DesktopEnvironment; - if (Option.isSome(environment.otlpTracesUrl)) { - return environment.otlpTracesUrl; - } - return yield* readPersistedOtlpTracesUrl; + const persisted = yield* readPersistedObservabilitySettings; + return resolveDesktopOtlpExport({ + otel: environment.otelEnvironment, + named: { + traces: Option.getOrUndefined(environment.otlpTracesUrl) ?? persisted.otlpTracesUrl, + metrics: Option.getOrUndefined(environment.otlpMetricsUrl) ?? persisted.otlpMetricsUrl, + logs: Option.getOrUndefined(environment.otlpLogsUrl) ?? persisted.otlpLogsUrl, + }, + namedExportIntervalMs: Option.getOrUndefined(environment.otlpExportIntervalMs), + defaultServiceName: DESKTOP_SERVICE_NAME, + runtimeAttributes: { + "service.runtime": "desktop", + "service.mode": environment.isDevelopment ? "development" : "packaged", + }, + }); }); const writeDevelopmentConsoleOutput = ( @@ -563,52 +597,120 @@ const backendOutputLogFactoryLayer = Layer.effect( }), ); -const desktopLoggerLayer = Layer.mergeAll( - Logger.layer([Logger.consolePretty(), Logger.tracerLogger], { mergeWithExisting: false }), - Layer.succeed(References.MinimumLogLevel, "Info"), -); +const serializationFor = (signal: DesktopOtlpSignal) => + signal.protocol === "http/protobuf" + ? OtlpSerialization.layerProtobuf + : OtlpSerialization.layerJson; + +const otlpResourceFor = (resource: DesktopOtlpResource) => ({ + serviceName: resource.serviceName, + ...(resource.serviceVersion === undefined ? {} : { serviceVersion: resource.serviceVersion }), + attributes: resource.attributes, +}); -const tracerLayer = Layer.unwrap( +/** + * Logs, traces, and metrics for the main process, built together because they + * share one read of the environment and Settings, and because a process gets + * exactly one logger set. + */ +const telemetryLayer = Layer.unwrap( Effect.gen(function* () { const environment = yield* DesktopEnvironment.DesktopEnvironment; - const otlpTracesUrl = yield* resolveOtlpTracesUrl; - const tracePath = environment.path.join(environment.logDir, "desktop.trace.ndjson"); - const sink = yield* makeTraceSink({ - filePath: tracePath, - maxBytes: DESKTOP_LOG_FILE_MAX_BYTES, - maxFiles: DESKTOP_LOG_FILE_MAX_FILES, - batchWindowMs: DESKTOP_TRACE_BATCH_WINDOW_MS, - }); - const delegate = Option.isNone(otlpTracesUrl) - ? undefined - : yield* OtlpTracer.make({ - url: otlpTracesUrl.value, - exportInterval: `${environment.otlpExportIntervalMs} millis`, - resource: { - serviceName: "desktop", - attributes: { - "service.runtime": "desktop", - "service.mode": environment.isDevelopment ? "development" : "packaged", - }, - }, + const resolved = yield* resolveOtlpExport; + + for (const warning of resolved.warnings) { + yield* Effect.logWarning(warning); + } + + const otlpResource = otlpResourceFor(resolved.resource); + + const otlpLogger = + resolved.logs.url === undefined + ? undefined + : OtlpLogger.make({ + url: resolved.logs.url, + exportInterval: `${resolved.logs.exportIntervalMs} millis`, + resource: otlpResource, + ...(resolved.logs.headers === undefined ? {} : { headers: resolved.logs.headers }), + ...(resolved.logs.maxBatchSize === undefined + ? {} + : { maxBatchSize: resolved.logs.maxBatchSize }), + }); + + const loggerLayer = Logger.layer( + [ + Logger.consolePretty(), + Logger.tracerLogger, + ...(otlpLogger === undefined ? [] : [otlpLogger]), + ], + { mergeWithExisting: false }, + ).pipe( + Layer.provide(OtlpExporter.layerFlusher), + Layer.provide(serializationFor(resolved.logs)), + ); + + const tracerLayer = Layer.unwrap( + Effect.gen(function* () { + const tracePath = environment.path.join(environment.logDir, "desktop.trace.ndjson"); + const sink = yield* makeTraceSink({ + filePath: tracePath, + maxBytes: DESKTOP_LOG_FILE_MAX_BYTES, + maxFiles: DESKTOP_LOG_FILE_MAX_FILES, + batchWindowMs: DESKTOP_TRACE_BATCH_WINDOW_MS, + }); + const delegate = + resolved.traces.url === undefined + ? undefined + : yield* OtlpTracer.make({ + url: resolved.traces.url, + exportInterval: `${resolved.traces.exportIntervalMs} millis`, + resource: otlpResource, + ...(resolved.traces.headers === undefined + ? {} + : { headers: resolved.traces.headers }), + ...(resolved.traces.maxBatchSize === undefined + ? {} + : { maxBatchSize: resolved.traces.maxBatchSize }), + }); + const tracer = yield* makeLocalFileTracer({ + filePath: tracePath, + maxBytes: DESKTOP_LOG_FILE_MAX_BYTES, + maxFiles: DESKTOP_LOG_FILE_MAX_FILES, + batchWindowMs: DESKTOP_TRACE_BATCH_WINDOW_MS, + sink, + ...(delegate ? { delegate } : {}), }); - const tracer = yield* makeLocalFileTracer({ - filePath: tracePath, - maxBytes: DESKTOP_LOG_FILE_MAX_BYTES, - maxFiles: DESKTOP_LOG_FILE_MAX_FILES, - batchWindowMs: DESKTOP_TRACE_BATCH_WINDOW_MS, - sink, - ...(delegate ? { delegate } : {}), - }); - return Layer.succeed(Tracer.Tracer, tracer); + return Layer.succeed(Tracer.Tracer, tracer); + }), + ).pipe( + Layer.provide(OtlpExporter.layerFlusher), + Layer.provide(serializationFor(resolved.traces)), + ); + + const metricsLayer = + resolved.metrics.url === undefined + ? Layer.empty + : OtlpMetrics.layer({ + url: resolved.metrics.url, + exportInterval: `${resolved.metrics.exportIntervalMs} millis`, + resource: otlpResource, + ...(resolved.metrics.headers === undefined + ? {} + : { headers: resolved.metrics.headers }), + ...(resolved.metrics.temporality === undefined + ? {} + : { temporality: resolved.metrics.temporality }), + }).pipe(Layer.provide(serializationFor(resolved.metrics))); + + return Layer.mergeAll(loggerLayer, tracerLayer, metricsLayer); }), -).pipe(Layer.provide(OtlpExporter.layerFlusher), Layer.provideMerge(OtlpSerialization.layerJson)); +); export const layer = Layer.mergeAll( backendOutputLogFactoryLayer, - desktopLoggerLayer, - tracerLayer, + telemetryLayer, + Layer.succeed(References.MinimumLogLevel, "Info"), Layer.succeed(Tracer.MinimumTraceLevel, "Info"), Layer.succeed(References.TracerTimingEnabled, true), ); diff --git a/apps/desktop/src/app/DesktopOtlpExport.test.ts b/apps/desktop/src/app/DesktopOtlpExport.test.ts new file mode 100644 index 000000000000..5ba253e220c6 --- /dev/null +++ b/apps/desktop/src/app/DesktopOtlpExport.test.ts @@ -0,0 +1,175 @@ +import { assert, describe, it } from "@effect/vitest"; +import * as OtelEnvironment from "@t3tools/shared/otelEnvironment"; +import * as ConfigProvider from "effect/ConfigProvider"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; + +import { + DEFAULT_DESKTOP_EXPORT_INTERVAL_MS, + type DesktopNamedOtlpEndpoints, + resolveDesktopOtlpExport, +} from "./DesktopOtlpExport.ts"; + +const noNamedEndpoints: DesktopNamedOtlpEndpoints = { + traces: undefined, + metrics: undefined, + logs: undefined, +}; + +const resolve = ( + env: Record, + overrides: { + readonly named?: Partial; + readonly namedExportIntervalMs?: number; + } = {}, +) => + OtelEnvironment.load.pipe( + Effect.provide(Layer.mergeAll(ConfigProvider.layer(ConfigProvider.fromEnv({ env })))), + Effect.map((otel) => + resolveDesktopOtlpExport({ + otel, + named: { ...noNamedEndpoints, ...overrides.named }, + namedExportIntervalMs: overrides.namedExportIntervalMs, + defaultServiceName: "desktop", + runtimeAttributes: { "service.runtime": "desktop", "service.mode": "development" }, + }), + ), + ); + +describe("resolveDesktopOtlpExport", () => { + it.effect("exports nothing when neither T3 Code nor OpenTelemetry named an endpoint", () => + Effect.gen(function* () { + const resolved = yield* resolve({}); + assert.strictEqual(resolved.traces.url, undefined); + assert.strictEqual(resolved.metrics.url, undefined); + assert.strictEqual(resolved.logs.url, undefined); + assert.deepStrictEqual(resolved.warnings, []); + }), + ); + + it.effect("picks up all three signals from the generic OpenTelemetry endpoint", () => + Effect.gen(function* () { + const resolved = yield* resolve({ + OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector.example.com", + }); + assert.strictEqual(resolved.traces.url, "https://collector.example.com/v1/traces"); + assert.strictEqual(resolved.metrics.url, "https://collector.example.com/v1/metrics"); + assert.strictEqual(resolved.logs.url, "https://collector.example.com/v1/logs"); + assert.strictEqual(resolved.traces.protocol, "http/protobuf"); + }), + ); + + it.effect("lets a named endpoint take the whole signal, not only its url", () => + Effect.gen(function* () { + const resolved = yield* resolve( + { + OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector.example.com", + OTEL_EXPORTER_OTLP_PROTOCOL: "http/protobuf", + OTEL_EXPORTER_OTLP_HEADERS: "authorization=Bearer%20token", + }, + { named: { traces: "http://127.0.0.1:4318/v1/traces" } }, + ); + + assert.strictEqual(resolved.traces.url, "http://127.0.0.1:4318/v1/traces"); + assert.strictEqual(resolved.traces.protocol, "http/json"); + assert.strictEqual(resolved.traces.headers, undefined); + assert.strictEqual(resolved.traces.exportIntervalMs, DEFAULT_DESKTOP_EXPORT_INTERVAL_MS); + + assert.strictEqual(resolved.metrics.url, "https://collector.example.com/v1/metrics"); + assert.strictEqual(resolved.metrics.protocol, "http/protobuf"); + assert.deepStrictEqual(resolved.metrics.headers, { authorization: "Bearer token" }); + }), + ); + + it.effect("stops every export when the OpenTelemetry SDK is disabled", () => + Effect.gen(function* () { + const resolved = yield* resolve( + { + OTEL_SDK_DISABLED: "true", + OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector.example.com", + }, + { named: { traces: "http://127.0.0.1:4318/v1/traces" } }, + ); + assert.strictEqual(resolved.traces.url, undefined); + assert.strictEqual(resolved.metrics.url, undefined); + assert.strictEqual(resolved.logs.url, undefined); + assert.include(resolved.warnings.join("\n"), "OTEL_SDK_DISABLED"); + }), + ); + + it.effect("declines only the signal that asked for a protocol T3 Code cannot speak", () => + Effect.gen(function* () { + const resolved = yield* resolve({ + OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector.example.com", + OTEL_EXPORTER_OTLP_TRACES_PROTOCOL: "grpc", + }); + assert.strictEqual(resolved.traces.url, undefined); + assert.strictEqual(resolved.metrics.url, "https://collector.example.com/v1/metrics"); + assert.strictEqual(resolved.logs.url, "https://collector.example.com/v1/logs"); + assert.lengthOf(resolved.warnings, 1); + assert.include(resolved.warnings[0] ?? "", "grpc"); + }), + ); + + it.effect("keeps exporting a signal whose grpc endpoint another variable overrode", () => + Effect.gen(function* () { + const resolved = yield* resolve( + { + OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector.example.com", + OTEL_EXPORTER_OTLP_TRACES_PROTOCOL: "grpc", + }, + { named: { traces: "http://127.0.0.1:4318/v1/traces" } }, + ); + assert.strictEqual(resolved.traces.url, "http://127.0.0.1:4318/v1/traces"); + assert.deepStrictEqual(resolved.warnings, []); + }), + ); + + it.effect("honors one T3 Code interval across every signal", () => + Effect.gen(function* () { + const resolved = yield* resolve( + { + OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector.example.com", + OTEL_METRIC_EXPORT_INTERVAL: "30000", + }, + { namedExportIntervalMs: 2500 }, + ); + assert.strictEqual(resolved.traces.exportIntervalMs, 2500); + assert.strictEqual(resolved.metrics.exportIntervalMs, 2500); + assert.strictEqual(resolved.logs.exportIntervalMs, 2500); + }), + ); + + it.effect("falls back to the interval the specification defines for each signal", () => + Effect.gen(function* () { + const resolved = yield* resolve({ + OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector.example.com", + }); + assert.strictEqual(resolved.traces.exportIntervalMs, 5_000); + assert.strictEqual(resolved.metrics.exportIntervalMs, 60_000); + assert.strictEqual(resolved.logs.exportIntervalMs, 1_000); + }), + ); + + it.effect("cannot be made to claim it is the server process", () => + Effect.gen(function* () { + const resolved = yield* resolve({ + OTEL_SERVICE_NAME: "t3-desktop", + OTEL_SERVICE_VERSION: "1.2.3", + OTEL_RESOURCE_ATTRIBUTES: "deployment.environment=lab,service.runtime=t3-server", + }); + assert.strictEqual(resolved.resource.serviceName, "t3-desktop"); + assert.strictEqual(resolved.resource.serviceVersion, "1.2.3"); + assert.strictEqual(resolved.resource.attributes["deployment.environment"], "lab"); + assert.strictEqual(resolved.resource.attributes["service.runtime"], "desktop"); + }), + ); + + it.effect("keeps calling itself the desktop when nothing named the service", () => + Effect.gen(function* () { + const resolved = yield* resolve({}); + assert.strictEqual(resolved.resource.serviceName, "desktop"); + assert.strictEqual(resolved.resource.serviceVersion, undefined); + }), + ); +}); diff --git a/apps/desktop/src/app/DesktopOtlpExport.ts b/apps/desktop/src/app/DesktopOtlpExport.ts new file mode 100644 index 000000000000..a10b15d760f0 --- /dev/null +++ b/apps/desktop/src/app/DesktopOtlpExport.ts @@ -0,0 +1,149 @@ +/** + * What the Electron main process exports, and where. + * + * The main process is its own OpenTelemetry producer: it owns app startup, + * window and menu work, backend supervision, and updates, none of which the + * server process can see. It reads the same sources as the server and in the + * same order, so a machine that points one of them at a collector points both. + * + * @module app/DesktopOtlpExport + */ +import * as OtelEnvironment from "@t3tools/shared/otelEnvironment"; + +/** + * The interval T3 Code has always used for a `T3CODE_OTLP_*` or Settings + * endpoint. An `OTEL_*` endpoint brings the specification's own per-signal + * default instead. + */ +export const DEFAULT_DESKTOP_EXPORT_INTERVAL_MS = 10_000; + +/** The wire format a `T3CODE_OTLP_*` or Settings endpoint has always been sent. */ +const DEFAULT_DESKTOP_PROTOCOL: OtelEnvironment.OtlpProtocol = "http/json"; + +export interface DesktopOtlpSignal { + readonly url: string | undefined; + readonly exportIntervalMs: number; + readonly protocol: OtelEnvironment.OtlpProtocol; + readonly headers: Readonly> | undefined; + readonly maxBatchSize: number | undefined; + readonly temporality: OtelEnvironment.MetricsTemporality | undefined; +} + +export interface DesktopOtlpResource { + readonly serviceName: string; + readonly serviceVersion: string | undefined; + readonly attributes: Readonly>; +} + +export interface DesktopOtlpExport { + readonly traces: DesktopOtlpSignal; + readonly metrics: DesktopOtlpSignal; + readonly logs: DesktopOtlpSignal; + readonly resource: DesktopOtlpResource; + /** Everything worth saying out loud once, already phrased for a human. */ + readonly warnings: ReadonlyArray; +} + +/** An endpoint named outside the OpenTelemetry variables, per signal. */ +export interface DesktopNamedOtlpEndpoints { + readonly traces: string | undefined; + readonly metrics: string | undefined; + readonly logs: string | undefined; +} + +export interface DesktopOtlpExportInput { + readonly otel: OtelEnvironment.OtelEnvironment; + readonly named: DesktopNamedOtlpEndpoints; + /** `T3CODE_OTLP_EXPORT_INTERVAL_MS`, which deliberately covers every signal. */ + readonly namedExportIntervalMs: number | undefined; + /** Used when nothing named a service, so existing dashboards keep working. */ + readonly defaultServiceName: string; + /** + * What this process is, as opposed to what the machine calls the service. + * Applied last so an ambient `OTEL_RESOURCE_ATTRIBUTES` cannot make the main + * process claim to be the server. + */ + readonly runtimeAttributes: Readonly>; +} + +const offSignal: DesktopOtlpSignal = { + url: undefined, + exportIntervalMs: DEFAULT_DESKTOP_EXPORT_INTERVAL_MS, + protocol: DEFAULT_DESKTOP_PROTOCOL, + headers: undefined, + maxBatchSize: undefined, + temporality: undefined, +}; + +/** + * A signal whose endpoint came from somewhere else is not the OpenTelemetry + * variables' to configure. Dropping the whole signal, rather than the endpoint + * alone, is what stops an ambient `OTEL_EXPORTER_OTLP_ENDPOINT` from changing + * the wire format, headers, or batching of an export a `T3CODE_OTLP_*` name or + * Settings already answered. + */ +const resolveSignal = ( + named: string | undefined, + signal: OtelEnvironment.OtlpSignal, + namedExportIntervalMs: number | undefined, +): DesktopOtlpSignal => { + const settings = named === undefined ? signal.settings : undefined; + const url = named ?? settings?.url; + if (url === undefined) { + return offSignal; + } + return { + url, + exportIntervalMs: + namedExportIntervalMs ?? settings?.exportIntervalMs ?? DEFAULT_DESKTOP_EXPORT_INTERVAL_MS, + protocol: settings?.protocol ?? DEFAULT_DESKTOP_PROTOCOL, + headers: settings?.headers, + maxBatchSize: settings?.maxBatchSize, + temporality: settings?.temporality, + }; +}; + +export const resolveDesktopOtlpExport = (input: DesktopOtlpExportInput): DesktopOtlpExport => { + const { otel, named } = input; + const resource: DesktopOtlpResource = { + serviceName: otel.resource.serviceName ?? input.defaultServiceName, + serviceVersion: otel.resource.serviceVersion, + attributes: { ...otel.resource.attributes, ...input.runtimeAttributes }, + }; + + if (otel.disabled) { + return { + traces: offSignal, + metrics: offSignal, + logs: offSignal, + resource, + warnings: [ + ...otel.warnings, + "OTEL_SDK_DISABLED is set, so the desktop app exports no telemetry; this overrides T3CODE_OTLP_* and Settings too", + ], + }; + } + + const signals = { + traces: named.traces === undefined ? otel.traces : OtelEnvironment.noSignal, + metrics: named.metrics === undefined ? otel.metrics : OtelEnvironment.noSignal, + logs: named.logs === undefined ? otel.logs : OtelEnvironment.noSignal, + }; + + // One variable can decline every signal, and saying so three times reads + // like three separate problems. + return { + traces: resolveSignal(named.traces, signals.traces, input.namedExportIntervalMs), + metrics: resolveSignal(named.metrics, signals.metrics, input.namedExportIntervalMs), + logs: resolveSignal(named.logs, signals.logs, input.namedExportIntervalMs), + resource, + warnings: [ + ...new Set([ + ...otel.warnings, + ...[signals.traces.declined, signals.metrics.declined, signals.logs.declined].filter( + (reason): reason is string => reason !== undefined, + ), + ]), + ], + }; +}; diff --git a/apps/server/src/bin.test.ts b/apps/server/src/bin.test.ts index df3ef8389bc3..2cfd48d8b4cf 100644 --- a/apps/server/src/bin.test.ts +++ b/apps/server/src/bin.test.ts @@ -41,7 +41,7 @@ import * as WorkspacePaths from "./workspace/WorkspacePaths.ts"; import * as ServerSecretStore from "./auth/ServerSecretStore.ts"; import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts"; import { environmentAuthenticatedAuthLayer } from "./auth/http.ts"; -import * as OtelEnvironment from "./observability/OtelEnvironment.ts"; +import * as OtelEnvironment from "@t3tools/shared/otelEnvironment"; const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, NetService.layer); class ProjectCliHttpApi extends HttpApi.make("environment").add(EnvironmentOrchestrationHttpApi) {} diff --git a/apps/server/src/cli/config.test.ts b/apps/server/src/cli/config.test.ts index 3c021840ec27..a30dc3c517fe 100644 --- a/apps/server/src/cli/config.test.ts +++ b/apps/server/src/cli/config.test.ts @@ -19,7 +19,7 @@ import { import * as NetService from "@t3tools/shared/Net"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { deriveServerPaths } from "../config.ts"; -import * as OtelEnvironment from "../observability/OtelEnvironment.ts"; +import * as OtelEnvironment from "@t3tools/shared/otelEnvironment"; import { resolveServerConfig } from "./config.ts"; const deriveExplicitServerPaths = (baseDir: string, devUrl: URL | undefined) => diff --git a/apps/server/src/cli/config.ts b/apps/server/src/cli/config.ts index b26aa879b98b..770ace8ed1be 100644 --- a/apps/server/src/cli/config.ts +++ b/apps/server/src/cli/config.ts @@ -1,4 +1,5 @@ import * as NetService from "@t3tools/shared/Net"; +import * as OtelEnvironment from "@t3tools/shared/otelEnvironment"; import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings"; import { DesktopBackendBootstrap, PortSchema } from "@t3tools/contracts"; import * as Config from "effect/Config"; @@ -16,7 +17,6 @@ import { Argument, Flag } from "effect/unstable/cli"; import { readBootstrapEnvelope } from "../bootstrap.ts"; import * as ServerConfig from "../config.ts"; import { expandHomePath, resolveBaseDir } from "../os-jank.ts"; -import * as OtelEnvironment from "../observability/OtelEnvironment.ts"; export const modeFlag = Flag.choice("mode", ServerConfig.RuntimeMode.literals).pipe( Flag.withDescription("Runtime mode. `desktop` keeps loopback defaults unless overridden."), diff --git a/apps/server/src/cli/pair.ts b/apps/server/src/cli/pair.ts index 11a19a9bef6a..6b05d96abc16 100644 --- a/apps/server/src/cli/pair.ts +++ b/apps/server/src/cli/pair.ts @@ -15,6 +15,7 @@ import { PortSchema, } from "@t3tools/contracts"; import { resolveWorktreeT3Home } from "@t3tools/shared/devHome"; +import * as OtelEnvironment from "@t3tools/shared/otelEnvironment"; import { buildTailscaleHttpsBaseUrl, DEFAULT_TAILSCALE_SERVE_PORT, @@ -54,7 +55,6 @@ import { resolveHeadlessConnectionString, } from "../startupAccess.ts"; import { baseDirFlag, DurationFromString } from "./config.ts"; -import * as OtelEnvironment from "../observability/OtelEnvironment.ts"; const WELL_KNOWN_ENVIRONMENT_PATH = "/.well-known/t3/environment"; const PAIR_PROBE_TIMEOUT = Duration.millis(2_500); diff --git a/apps/server/src/config.ts b/apps/server/src/config.ts index 2fb76e29529d..81c8d1b25e16 100644 --- a/apps/server/src/config.ts +++ b/apps/server/src/config.ts @@ -6,6 +6,7 @@ * * @module ServerConfig */ +import * as OtelEnvironment from "@t3tools/shared/otelEnvironment"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -14,8 +15,6 @@ import * as LogLevel from "effect/LogLevel"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; -import * as OtelEnvironment from "./observability/OtelEnvironment.ts"; - export const DEFAULT_PORT = 3773; export const RuntimeMode = Schema.Literals(["web", "desktop"]); diff --git a/apps/server/src/environment/ServerEnvironment.test.ts b/apps/server/src/environment/ServerEnvironment.test.ts index 10ff8a43680f..c798796a3be8 100644 --- a/apps/server/src/environment/ServerEnvironment.test.ts +++ b/apps/server/src/environment/ServerEnvironment.test.ts @@ -15,7 +15,7 @@ import { } from "../cloud/config.ts"; import * as ServerConfig from "../config.ts"; import * as ServerEnvironment from "./ServerEnvironment.ts"; -import * as OtelEnvironment from "../observability/OtelEnvironment.ts"; +import * as OtelEnvironment from "@t3tools/shared/otelEnvironment"; const isServerEnvironmentIdPersistenceError = Schema.is( ServerEnvironment.ServerEnvironmentIdPersistenceError, diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts index 2b2fa7f63c1d..8cf7c7aa3e97 100644 --- a/apps/server/src/server.test.ts +++ b/apps/server/src/server.test.ts @@ -181,7 +181,7 @@ import { type TransferBudgetRun, transferBudgetViolations, } from "../integration/TransferBudgetReport.integration.ts"; -import * as OtelEnvironment from "./observability/OtelEnvironment.ts"; +import * as OtelEnvironment from "@t3tools/shared/otelEnvironment"; const defaultProjectId = ProjectId.make("project-default"); const defaultThreadId = ThreadId.make("thread-default"); diff --git a/apps/server/src/serverLogger.test.ts b/apps/server/src/serverLogger.test.ts index d2adbf06f8c1..47608acfc727 100644 --- a/apps/server/src/serverLogger.test.ts +++ b/apps/server/src/serverLogger.test.ts @@ -6,7 +6,7 @@ import * as HttpClient from "effect/unstable/http/HttpClient"; import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"; import * as ServerConfig from "./config.ts"; -import * as OtelEnvironment from "./observability/OtelEnvironment.ts"; +import * as OtelEnvironment from "@t3tools/shared/otelEnvironment"; import { ServerLoggerLive } from "./serverLogger.ts"; interface ExportedRequest { diff --git a/docs/fork/0022-the-desktop-app-reports-its-own-work.md b/docs/fork/0022-the-desktop-app-reports-its-own-work.md new file mode 100644 index 000000000000..e71dee97f4b8 --- /dev/null +++ b/docs/fork/0022-the-desktop-app-reports-its-own-work.md @@ -0,0 +1,53 @@ +# 0022: The desktop app reports its own work + +- PR: [TrogonStack/t3code#35](https://github.com/TrogonStack/t3code/pull/35) +- Status: active + +## What you can do now + +- See what the desktop app itself is doing. App startup, window and menu work, + backend supervision, and updates now reach your collector as traces, logs, + and metrics under the service name `desktop`, alongside the server work they + cause. +- Configure it the way you configure everything else. The Electron main process + reads the same `OTEL_*` variables as the server, in the same order, so a + machine that points one of them at a collector points both. It had a trace + exporter before, and only its own `T3CODE_OTLP_TRACES_URL` could reach it, + which almost nobody sets. +- Get logs and metrics from it, not only traces. A crash loop before the server + is even up used to leave nothing behind but a local file on the machine it + happened on. +- Turn it off the same way. `OTEL_SDK_DISABLED=true` stops both processes. +- Tell the two apart without trusting the environment. `service.runtime` on the + main process is always `desktop`, so an ambient + `OTEL_RESOURCE_ATTRIBUTES=service.runtime=t3-server` cannot make it file its + work under the server's name. + +## Why + +The desktop app is two processes, and only one of them was reachable. A user who +set up a collector got the server and quietly got nothing from the process that +starts it, owns its windows, and restarts it when it dies. The most useful +telemetry the desktop app could send is about the minutes before the server +exists, and those were the minutes nothing was recorded. + +The trace exporter that was already there is the sharper part of the story. It +was real, it worked, and it read one variable nobody sets, so it read as a +feature that had been tried and found not to help. It had not been tried. + +Reading the same variables in both processes is the whole point. A telemetry +variable that half an app honors is worse than one it ignores entirely, because +the half that arrives looks like the whole. + +## Upstream considerations + +This belongs upstream and depends on 0018 being there first: it is the same +environment reading applied to the other process, which is why the reading moved +into a shared package instead of being copied. Upstream taking 0018 gets this +almost for free. + +The rebase burden is a single assembly point. The main process gets one logger +set for its lifetime, so the OTLP log exporter has to be built in the same call +that builds the console logger rather than merged in beside it. A sync that +rewrites that assembly and splits them apart will silently drop either the +console output or the export. diff --git a/docs/fork/README.md b/docs/fork/README.md index cae0dafbef6e..14b71c5ac977 100644 --- a/docs/fork/README.md +++ b/docs/fork/README.md @@ -59,3 +59,5 @@ Each entry uses these sections: active, [#33](https://github.com/TrogonStack/t3code/pull/33) - **0021** [A trace includes the client that started it](./0021-a-trace-includes-the-client-that-started-it.md) active, [#34](https://github.com/TrogonStack/t3code/pull/34) +- **0022** [The desktop app reports its own work](./0022-the-desktop-app-reports-its-own-work.md) + active, [#35](https://github.com/TrogonStack/t3code/pull/35) diff --git a/docs/operations/observability.md b/docs/operations/observability.md index d5658758e2cc..eaba1f8b739b 100644 --- a/docs/operations/observability.md +++ b/docs/operations/observability.md @@ -197,6 +197,23 @@ Set `OTEL_EXPORTER_OTLP_{TRACES,METRICS,LOGS}_ENDPOINT` when a signal needs a fu Ambient `OTEL_*` variables turn export on by themselves. A work collector in your shell profile means T3 Code exports to it, so use `OTEL_SDK_DISABLED=true` if that is not what you want. +#### Which Processes Export + +The desktop app is two processes, and each is its own OpenTelemetry producer: + +- **The server**, under service name `t3-server`. +- **The Electron main process**, under service name `desktop`. It owns app startup, window and menu + work, backend supervision, and updates, none of which the server can see. It reads the same + sources in the same order as the server, so a machine that points one of them at a collector + points both. `OTEL_SERVICE_NAME` renames it; `service.runtime` stays `desktop` no matter what + `OTEL_RESOURCE_ATTRIBUTES` says, so the two processes cannot be confused for each other. + +On macOS, ambient variables reach the desktop app only when it is launched from a shell. Opening it +from the Dock, Finder, or Spotlight inherits `launchd`'s environment instead, which is why the +instrumented walkthrough above launches from the same shell that exported the variables. Settings +and `T3CODE_OTLP_*` are not affected, and the server the desktop app spawns inherits whatever the +main process was given. + #### Precedence For each signal, the first source that names its endpoint wins: @@ -236,7 +253,7 @@ Settings. The wire format defaults to `http/protobuf` when the endpoint came from `OTEL_*`, matching the specification, and stays `http/json` for a `T3CODE_OTLP_*` setup that never mentioned a protocol. -`OTEL_EXPORTER_OTLP_PROTOCOL=grpc` is refused rather than downgraded, because this server has no gRPC +`OTEL_EXPORTER_OTLP_PROTOCOL=grpc` is refused rather than downgraded, because T3 Code has no gRPC transport and posting an HTTP body to a gRPC endpoint fails in a way that is harder to read than exporting nothing. The refusal is logged at startup and turns off only the signal that named gRPC, and only when that signal had no other endpoint to go to. @@ -249,8 +266,8 @@ its `=` padding. Not everything in the specification is implemented. These are the ones worth knowing about: -- **No gRPC.** `OTEL_EXPORTER_OTLP_PROTOCOL=grpc` is refused rather than downgraded, because this - server has no gRPC transport and posting an HTTP body to a gRPC endpoint fails in a way that is +- **No gRPC.** `OTEL_EXPORTER_OTLP_PROTOCOL=grpc` is refused rather than downgraded, because T3 Code + has no gRPC transport and posting an HTTP body to a gRPC endpoint fails in a way that is harder to read than exporting nothing. The refusal is logged at startup and turns off only the signal that named gRPC, so `OTEL_EXPORTER_OTLP_METRICS_PROTOCOL=grpc` leaves traces exporting. - **No compression and no client TLS.** `OTEL_EXPORTER_OTLP_COMPRESSION`, @@ -282,7 +299,7 @@ variables, propagator variables, and the attribute and span limit variables. #### When A Value Cannot Be Used -A variable this server cannot act on never stops it from starting. Two things can happen instead, +A variable T3 Code cannot act on never stops it from starting. Two things can happen instead, and both are logged once at startup: - **A warning, then the default.** A misspelled protocol, an unavailable temporality, a timeout or @@ -290,7 +307,7 @@ and both are logged once at startup: reported and ignored, and everything else keeps exporting. One bad value never costs you the other variables. - **Export off.** Only `OTEL_EXPORTER_OTLP_PROTOCOL=grpc` does this, because it names a transport - this server does not speak rather than a value it failed to parse. + T3 Code does not speak rather than a value it failed to parse. An empty value means the same thing as an unset one, so `OTEL_SERVICE_NAME=` reads as if the variable were not there at all. `OTEL_SDK_DISABLED` follows the specification's one rule for @@ -645,6 +662,12 @@ It provides: - optional OTLP metrics exporter - Effect trace-level and timing refs +The Electron main process assembles its own in +`apps/desktop/src/app/DesktopObservability.ts`, with the same pieces plus an optional OTLP log +exporter, and resolves its endpoints in `apps/desktop/src/app/DesktopOtlpExport.ts`. Both processes +read the `OTEL_*` variables through `packages/shared/src/otelEnvironment.ts`, so neither can disagree +with the other about what a variable means. + ### Env Vars Local trace file: @@ -660,8 +683,10 @@ OTLP export: - `T3CODE_OTLP_TRACES_URL`: OTLP trace endpoint - `T3CODE_OTLP_METRICS_URL`: OTLP metric endpoint +- `T3CODE_OTLP_LOGS_URL`: OTLP log endpoint - `T3CODE_OTLP_EXPORT_INTERVAL_MS`: export interval, default `10000` -- `T3CODE_OTLP_SERVICE_NAME`: service name, default `t3-server` +- `T3CODE_OTLP_SERVICE_NAME`: server service name, default `t3-server`. The Electron main process + does not read it; rename that one with `OTEL_SERVICE_NAME`. If the OTLP URLs are unset, local tracing still works and metrics stay in-process only. diff --git a/packages/shared/package.json b/packages/shared/package.json index a797e97b6625..b6cde3633dbc 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -43,6 +43,10 @@ "types": "./src/observability.ts", "import": "./src/observability.ts" }, + "./otelEnvironment": { + "types": "./src/otelEnvironment.ts", + "import": "./src/otelEnvironment.ts" + }, "./httpObservability": { "types": "./src/httpObservability.ts", "import": "./src/httpObservability.ts" diff --git a/apps/server/src/observability/OtelEnvironment.test.ts b/packages/shared/src/otelEnvironment.test.ts similarity index 99% rename from apps/server/src/observability/OtelEnvironment.test.ts rename to packages/shared/src/otelEnvironment.test.ts index d217d210ebe8..e0609c41d8d0 100644 --- a/apps/server/src/observability/OtelEnvironment.test.ts +++ b/packages/shared/src/otelEnvironment.test.ts @@ -3,7 +3,7 @@ import * as ConfigProvider from "effect/ConfigProvider"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; -import * as OtelEnvironment from "./OtelEnvironment.ts"; +import * as OtelEnvironment from "./otelEnvironment.ts"; const withEnv = (env: Record) => Effect.provide(Layer.mergeAll(ConfigProvider.layer(ConfigProvider.fromEnv({ env })))); diff --git a/apps/server/src/observability/OtelEnvironment.ts b/packages/shared/src/otelEnvironment.ts similarity index 96% rename from apps/server/src/observability/OtelEnvironment.ts rename to packages/shared/src/otelEnvironment.ts index dba89474c07e..dcf90dee3d7e 100644 --- a/apps/server/src/observability/OtelEnvironment.ts +++ b/packages/shared/src/otelEnvironment.ts @@ -1,5 +1,5 @@ /** - * OtelEnvironment: the OpenTelemetry environment variables, read the way the + * otelEnvironment: the OpenTelemetry environment variables, read the way the * specification says to read them. * * T3 Code has always had its own `T3CODE_OTLP_*` names, which stay the @@ -8,7 +8,10 @@ * other service on it and expects one more process to join in without being * told twice. * - * Only the variables this server can act on are read. The exporter speaks + * Read by every T3 Code process that exports telemetry, so the server and the + * desktop app cannot disagree about what a variable means. + * + * Only the variables T3 Code can act on are read. The exporter speaks * OTLP over HTTP, so `grpc` is declined loudly rather than answered with a * body the endpoint cannot parse. * @@ -16,20 +19,20 @@ * warning followed by the default, never a refusal to start and never a * silently different behavior. * - * @module observability/OtelEnvironment + * @module otelEnvironment */ import * as Config from "effect/Config"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; /** - * The signals this server exports. Each one is configured independently, and + * The signals T3 Code exports. Each one is configured independently, and * the specification spells every variable name with the signal in it, so the * name is the thing the readers below are parameterized by. */ export type OtlpSignalName = "TRACES" | "METRICS" | "LOGS"; -/** The wire formats this server can produce. `grpc` is not one of them. */ +/** The wire formats T3 Code can produce. `grpc` is not one of them. */ export type OtlpProtocol = "http/json" | "http/protobuf"; /** `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE`. */ @@ -327,7 +330,7 @@ interface ProtocolDecision { * OpenTelemetry keeps the wire format T3 Code has always used. * * `grpc` is the one value that turns export off rather than falling back. It - * is a real protocol this server does not speak, its endpoint has no + * is a real protocol T3 Code does not speak, its endpoint has no * `/v1/traces` path, and it expects a framing nothing here produces, so * posting to it is worse than exporting nothing. It turns off only the signal * that named it, since a metric endpoint speaking gRPC says nothing about @@ -363,7 +366,7 @@ const resolveProtocol = Effect.gen(function* () { : named.value === "grpc" ? { protocol: SPEC_DEFAULT_PROTOCOL, - declined: `${named.name}=grpc is not supported; this server exports OTLP over HTTP only, so this signal is not exported`, + declined: `${named.name}=grpc is not supported; T3 Code exports OTLP over HTTP only, so this signal is not exported`, } : { protocol: named.value, declined: undefined }; @@ -422,7 +425,7 @@ const resolveResource = Effect.gen(function* () { const UNREADABLE = "the OpenTelemetry environment could not be read"; /** - * Read the environment. Never fails: a variable this server cannot honor + * Read the environment. Never fails: a variable T3 Code cannot honor * leaves the corresponding setting unset and is reported through the signal's * `declined`, because an unparseable telemetry knob is not a reason to refuse * to start.