diff --git a/apps/desktop/src/app/DesktopObservability.ts b/apps/desktop/src/app/DesktopObservability.ts index 78be929c088c..720f9b317cc9 100644 --- a/apps/desktop/src/app/DesktopObservability.ts +++ b/apps/desktop/src/app/DesktopObservability.ts @@ -39,8 +39,8 @@ 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"; +/** What the main process calls itself, in the family with `t3-server` and `t3-web`. */ +const DESKTOP_SERVICE_NAME = "t3-desktop"; const DESKTOP_BACKEND_OUTPUT_BUFFER_MAX_BYTES = 1024 * 1024; const DESKTOP_BACKEND_OUTPUT_BUFFER_MAX_CHUNKS = 256; @@ -372,7 +372,7 @@ const resolveOtlpExport = Effect.gen(function* () { logs: Option.getOrUndefined(environment.otlpLogsUrl) ?? persisted.otlpLogsUrl, }, namedExportIntervalMs: Option.getOrUndefined(environment.otlpExportIntervalMs), - defaultServiceName: DESKTOP_SERVICE_NAME, + serviceName: DESKTOP_SERVICE_NAME, runtimeAttributes: { "service.runtime": "desktop", "service.mode": environment.isDevelopment ? "development" : "packaged", diff --git a/apps/desktop/src/app/DesktopOtlpExport.test.ts b/apps/desktop/src/app/DesktopOtlpExport.test.ts index 5ba253e220c6..ac70ef48ea11 100644 --- a/apps/desktop/src/app/DesktopOtlpExport.test.ts +++ b/apps/desktop/src/app/DesktopOtlpExport.test.ts @@ -30,7 +30,7 @@ const resolve = ( otel, named: { ...noNamedEndpoints, ...overrides.named }, namedExportIntervalMs: overrides.namedExportIntervalMs, - defaultServiceName: "desktop", + serviceName: "t3-desktop", runtimeAttributes: { "service.runtime": "desktop", "service.mode": "development" }, }), ), @@ -154,7 +154,6 @@ describe("resolveDesktopOtlpExport", () => { 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", }); @@ -165,10 +164,34 @@ describe("resolveDesktopOtlpExport", () => { }), ); - it.effect("keeps calling itself the desktop when nothing named the service", () => + it.effect("cannot be renamed by the environment", () => + Effect.gen(function* () { + const resolved = yield* resolve({ + OTEL_SERVICE_NAME: "some-other-app", + OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector.example.com", + }); + assert.strictEqual(resolved.resource.serviceName, "t3-desktop"); + assert.lengthOf(resolved.warnings, 1); + assert.include(resolved.warnings[0] ?? "", "OTEL_SERVICE_NAME was ignored"); + }), + ); + + it.effect("cannot be renamed through the resource attributes either", () => + Effect.gen(function* () { + const resolved = yield* resolve({ + OTEL_RESOURCE_ATTRIBUTES: "service.name=some-other-app,host.name=lab-01", + }); + assert.strictEqual(resolved.resource.serviceName, "t3-desktop"); + assert.strictEqual(resolved.resource.attributes["service.name"], undefined); + assert.strictEqual(resolved.resource.attributes["host.name"], "lab-01"); + assert.include(resolved.warnings[0] ?? "", "service.name was ignored"); + }), + ); + + it.effect("names itself even when the environment says nothing", () => Effect.gen(function* () { const resolved = yield* resolve({}); - assert.strictEqual(resolved.resource.serviceName, "desktop"); + assert.strictEqual(resolved.resource.serviceName, "t3-desktop"); assert.strictEqual(resolved.resource.serviceVersion, undefined); }), ); diff --git a/apps/desktop/src/app/DesktopOtlpExport.ts b/apps/desktop/src/app/DesktopOtlpExport.ts index a10b15d760f0..f041e65d655a 100644 --- a/apps/desktop/src/app/DesktopOtlpExport.ts +++ b/apps/desktop/src/app/DesktopOtlpExport.ts @@ -56,12 +56,12 @@ export interface DesktopOtlpExportInput { 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 calls itself. The environment cannot rename it. */ + readonly serviceName: 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. + * What this process is. Applied last so an ambient + * `OTEL_RESOURCE_ATTRIBUTES` cannot make the main process claim to be the + * server. */ readonly runtimeAttributes: Readonly>; } @@ -106,7 +106,7 @@ const resolveSignal = ( export const resolveDesktopOtlpExport = (input: DesktopOtlpExportInput): DesktopOtlpExport => { const { otel, named } = input; const resource: DesktopOtlpResource = { - serviceName: otel.resource.serviceName ?? input.defaultServiceName, + serviceName: input.serviceName, serviceVersion: otel.resource.serviceVersion, attributes: { ...otel.resource.attributes, ...input.runtimeAttributes }, }; diff --git a/apps/server/src/cli/config.test.ts b/apps/server/src/cli/config.test.ts index a30dc3c517fe..5151e07cb2de 100644 --- a/apps/server/src/cli/config.test.ts +++ b/apps/server/src/cli/config.test.ts @@ -539,7 +539,21 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => { expect(resolved.otlpTracesUrl).toBe("https://collector.example.com/v1/traces"); expect(resolved.otlpMetricsUrl).toBe("https://collector.example.com/v1/metrics"); expect(resolved.otlpLogsUrl).toBe("https://collector.example.com/v1/logs"); - expect(resolved.otlpServiceName).toBe("t3"); + // The endpoint is the machine's to name. The service is not. + expect(resolved.otlpServiceName).toBe("t3-server"); + }), + ); + + it.effect("cannot be renamed by the environment", () => + Effect.gen(function* () { + // A shell profile that names the app it was written for must not decide + // what T3 Code calls itself, or one dashboard quietly covers two apps. + const resolved = yield* resolveWithEnv({ + OTEL_SERVICE_NAME: "some-other-app", + OTEL_RESOURCE_ATTRIBUTES: "service.name=some-other-app", + }); + + expect(resolved.otlpServiceName).toBe("t3-server"); }), ); @@ -560,7 +574,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => { expect(resolved.otlpTracesUrl).toBe("https://collector.example.com/v1/traces"); expect(resolved.otlpMetricsUrl).toBe("https://collector.example.com/v1/metrics"); expect(resolved.otlpLogsUrl).toBe("https://collector.example.com/v1/logs"); - expect(resolved.otlpServiceName).toBe("t3"); + expect(resolved.otlpServiceName).toBe("t3-server"); }), ); diff --git a/apps/server/src/cli/config.ts b/apps/server/src/cli/config.ts index 770ace8ed1be..9b5d96cdf65c 100644 --- a/apps/server/src/cli/config.ts +++ b/apps/server/src/cli/config.ts @@ -421,8 +421,7 @@ export const resolveServerConfig = ( env.otlpExportIntervalMs ?? otelEnvironment.metrics.settings?.exportIntervalMs ?? 10_000, otlpLogsExportIntervalMs: env.otlpExportIntervalMs ?? otelEnvironment.logs.settings?.exportIntervalMs ?? 10_000, - otlpServiceName: - named(env.otlpServiceName) ?? otelEnvironment.resource.serviceName ?? "t3-server", + otlpServiceName: named(env.otlpServiceName) ?? "t3-server", otelEnvironment, mode, port, diff --git a/docs/fork/0018-the-standard-otel-variables-are-honored.md b/docs/fork/0018-the-standard-otel-variables-are-honored.md index 5ee42491364b..553aaa52d104 100644 --- a/docs/fork/0018-the-standard-otel-variables-are-honored.md +++ b/docs/fork/0018-the-standard-otel-variables-are-honored.md @@ -12,10 +12,10 @@ - Send the credentials your collector requires. `OTEL_EXPORTER_OTLP_HEADERS` reaches the exporter, including the proxy that forwards browser traces, so an authenticated endpoint stops rejecting the whole stream. -- Get one service identity across your fleet. `OTEL_SERVICE_NAME`, - `OTEL_SERVICE_VERSION`, and `OTEL_RESOURCE_ATTRIBUTES` are attached to every - span and metric, so T3 Code sits in the same dashboards as everything else - rather than under a name only it uses. +- Tell your instances apart. `OTEL_SERVICE_VERSION` and + `OTEL_RESOURCE_ATTRIBUTES` are attached to every span and metric, so T3 Code + sits in the same dashboards as everything else. Service names themselves are + static, and `OTEL_SERVICE_NAME` is refused with a warning; see 0023. - Turn export off from the environment. `OTEL_SDK_DISABLED=true` stops every export, including one configured in Settings, which is the one switch a shared machine needs. 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 index e71dee97f4b8..e6e9f10b772d 100644 --- a/docs/fork/0022-the-desktop-app-reports-its-own-work.md +++ b/docs/fork/0022-the-desktop-app-reports-its-own-work.md @@ -10,18 +10,19 @@ 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. + reads the same `OTEL_*` endpoint 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 +- Tell the two apart without trusting the environment. The main process reports + as `t3-desktop`, joining `t3-server` and `t3-web`, and `service.runtime` on it + is always `desktop`, so an ambient `OTEL_RESOURCE_ATTRIBUTES=service.runtime=t3-server` cannot make it file its - work under the server's name. + work under the server's name. See 0023 for why names are static. ## Why diff --git a/docs/fork/0023-a-service-name-is-not-an-environment-variable.md b/docs/fork/0023-a-service-name-is-not-an-environment-variable.md new file mode 100644 index 000000000000..c767e5d8c3e0 --- /dev/null +++ b/docs/fork/0023-a-service-name-is-not-an-environment-variable.md @@ -0,0 +1,58 @@ +# 0023: A service name is not an environment variable + +- PR: [TrogonStack/t3code#36](https://github.com/TrogonStack/t3code/pull/36) +- Status: active + +## What you can do now + +- Trust what a service name means. T3 Code reports as `t3-server`, `t3-desktop`, + and `t3-web`, always, and nothing in the environment can change that. A + dashboard built on one of those names keeps meaning what it meant when you + built it. +- Set `OTEL_SERVICE_NAME` on a machine without consequence. It is refused, and + the refusal is named in the startup log rather than applied quietly or + dropped quietly. +- Keep telling your instances apart. `OTEL_RESOURCE_ATTRIBUTES` still works and + is the right lever for it, including `service.instance.id`, + `deployment.environment`, and `host.name`. +- Rename the server on purpose if you need to. `T3CODE_OTLP_SERVICE_NAME` is + still honored, because it is T3 Code's own variable and nobody exports it + fleet-wide by accident. + +## Why + +This reverses part of 0018, which honored `OTEL_SERVICE_NAME` because every +other OpenTelemetry SDK does. Consistency was the wrong thing to optimize for +here. + +A service name is not a preference, it is a key. Endpoints, headers, protocols, +and resource attributes are all things a machine legitimately knows better than +the app does, which is why reading them from the environment is right. A service +name is the opposite: it is the identity every dashboard, alert, and saved query +is keyed on, and it is worth exactly as much as it is stable. One ambient +variable, exported years ago in a shell profile for a different app, silently +merges T3 Code into that app's dashboards and pulls it out of its own. + +The failure mode is what makes it worth diverging over. It is invisible from +inside the app, the telemetry keeps flowing, and every panel still renders. What +you get is not an empty dashboard, which someone would investigate, but a +plausible one that is quietly describing two applications at once. + +Worth being honest that the specification does not settle this. It fixes the +precedence between `OTEL_SERVICE_NAME` and a `service.name` in +`OTEL_RESOURCE_ATTRIBUTES`, and then leaves environment-versus-code to whichever +order an SDK happens to merge its resources in. Most SDKs let hardcoded values +win, which is the same answer this reaches. The difference is that they reach it +silently and this says so out loud. + +## Upstream considerations + +The server half of this is not a divergence at all: it restores upstream's +behavior, which was static `t3-server` with `T3CODE_OTLP_SERVICE_NAME` as the +only override. 0018 is what moved away from that, and this moves back. Upstream +taking 0018 should take this with it. + +The rebase burden is one line per process plus the absence of a field. The +environment reader has no `serviceName` in its resource type at all, so a sync +that reintroduces the variable has to add the field back before it can be wired +anywhere, rather than silently succeeding. diff --git a/docs/fork/README.md b/docs/fork/README.md index 14b71c5ac977..3a8968263182 100644 --- a/docs/fork/README.md +++ b/docs/fork/README.md @@ -61,3 +61,5 @@ Each entry uses these sections: 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) +- **0023** [A service name is not an environment variable](./0023-a-service-name-is-not-an-environment-variable.md) + active, [#36](https://github.com/TrogonStack/t3code/pull/36) diff --git a/docs/operations/observability.md b/docs/operations/observability.md index eaba1f8b739b..84d6eead4b9c 100644 --- a/docs/operations/observability.md +++ b/docs/operations/observability.md @@ -187,7 +187,7 @@ it, T3 Code joins in without being told twice. Nothing above is required: ```bash export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 -export OTEL_SERVICE_NAME=t3-local +export OTEL_RESOURCE_ATTRIBUTES=deployment.environment=lab ``` The base endpoint is a base, not a full URL: traces go to `/v1/traces`, metrics to @@ -202,11 +202,27 @@ T3 Code exports to it, so use `OTEL_SDK_DISABLED=true` if that is not what you w 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 +- **The Electron main process**, under service name `t3-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. + points both. + +The web client reports as `t3-web`, so the three service names are `t3-server`, `t3-desktop`, and +`t3-web`. + +**Service names are static and the environment cannot change them.** `OTEL_SERVICE_NAME` and a +`service.name` inside `OTEL_RESOURCE_ATTRIBUTES` are both refused, with a warning naming the one you +set. This is a deliberate departure from what most OpenTelemetry SDKs do, and the reason is that a +service name is not a preference: renaming one process merges two services in every dashboard built +on them, and a shell profile that names the app it was written for should not be able to do that to +T3 Code. Use `OTEL_RESOURCE_ATTRIBUTES` to tell instances apart, which is what it is for: + +```bash +export OTEL_RESOURCE_ATTRIBUTES=service.instance.id=laptop-01,deployment.environment=lab +``` + +`T3CODE_OTLP_SERVICE_NAME` still renames the server, because it is T3 Code's own variable and nobody +sets it across a fleet by accident. There is no equivalent for the main process. 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 @@ -245,7 +261,8 @@ Settings. | `OTEL_EXPORTER_OTLP_HEADERS`, `OTEL_EXPORTER_OTLP_{TRACES,METRICS,LOGS}_HEADERS` | Export headers, per signal overriding the shared ones | | `OTEL_EXPORTER_OTLP_PROTOCOL`, `OTEL_EXPORTER_OTLP_{TRACES,METRICS,LOGS}_PROTOCOL` | `http/protobuf` (default) or `http/json` | | `OTEL_{TRACES,METRICS,LOGS}_EXPORTER` | A list; the signal is exported when it contains `otlp`, which is the default | -| `OTEL_SERVICE_NAME`, `OTEL_SERVICE_VERSION`, `OTEL_RESOURCE_ATTRIBUTES` | Resource identity attached to every span, metric, and log record | +| `OTEL_SERVICE_VERSION`, `OTEL_RESOURCE_ATTRIBUTES` | Resource identity attached to every span, metric, and log record | +| `OTEL_SERVICE_NAME` | Refused with a warning; service names are static | | `OTEL_BSP_SCHEDULE_DELAY`, `OTEL_METRIC_EXPORT_INTERVAL`, `OTEL_BLRP_SCHEDULE_DELAY` | Export interval, one per signal | | `OTEL_BSP_MAX_EXPORT_BATCH_SIZE`, `OTEL_BLRP_MAX_EXPORT_BATCH_SIZE` | Spans per batch, log records per batch | | `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` | `cumulative` or `delta` | @@ -292,6 +309,10 @@ Not everything in the specification is implemented. These are the ones worth kno - **`OTEL_SERVICE_VERSION` is not a specification variable.** It is read as a convenience because the exporter library reads it too. `OTEL_RESOURCE_ATTRIBUTES=service.version=...` is the portable spelling. +- **`service.name` cannot be set from the environment.** See Which Processes Export above. + `OTEL_SERVICE_NAME` and `OTEL_RESOURCE_ATTRIBUTES=service.name=...` are both refused with a + warning, and the `service.name` key is dropped rather than passed through so the exporter never + receives two of them. Everything else not listed above is ignored, including `OTEL_BSP_MAX_QUEUE_SIZE`, `OTEL_BLRP_MAX_QUEUE_SIZE`, `OTEL_BSP_EXPORT_TIMEOUT`, `OTEL_BLRP_EXPORT_TIMEOUT`, sampler @@ -309,8 +330,9 @@ and both are logged once at startup: - **Export off.** Only `OTEL_EXPORTER_OTLP_PROTOCOL=grpc` does this, because it names a transport 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 +An empty value means the same thing as an unset one, so `OTEL_SERVICE_VERSION=` reads as if the +variable were not there at all. An empty `OTEL_SERVICE_NAME` is not an attempt to rename anything, +so it is not warned about either. `OTEL_SDK_DISABLED` follows the specification's one rule for booleans: the case-insensitive string `true` is the only value that switches export off, and anything else, including `yes` and `1`, leaves it on. @@ -686,7 +708,7 @@ OTLP export: - `T3CODE_OTLP_LOGS_URL`: OTLP log endpoint - `T3CODE_OTLP_EXPORT_INTERVAL_MS`: export interval, default `10000` - `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`. + does not read it and is always `t3-desktop`. If the OTLP URLs are unset, local tracing still works and metrics stay in-process only. diff --git a/packages/shared/src/otelEnvironment.test.ts b/packages/shared/src/otelEnvironment.test.ts index e0609c41d8d0..dda0afb7f6dd 100644 --- a/packages/shared/src/otelEnvironment.test.ts +++ b/packages/shared/src/otelEnvironment.test.ts @@ -130,18 +130,17 @@ describe("OtelEnvironment", () => { }), ); - it.effect("reads the service identity and the leftover resource attributes", () => + it.effect("reads the service version and the leftover resource attributes", () => Effect.gen(function* () { const resolved = yield* OtelEnvironment.load.pipe( withEnv({ - OTEL_RESOURCE_ATTRIBUTES: "org.name=Example,service.name=from-attributes,deployment=prod", + OTEL_RESOURCE_ATTRIBUTES: "org.name=Example,deployment=prod", OTEL_SERVICE_VERSION: "1.2.3", }), ); - assert.strictEqual(resolved.resource.serviceName, "from-attributes"); assert.strictEqual(resolved.resource.serviceVersion, "1.2.3"); - // service.name and service.version become the named fields, so leaving - // them in the attribute bag too would send each one twice. + // service.version becomes a named field, so leaving it in the attribute + // bag too would send it twice. assert.deepStrictEqual(resolved.resource.attributes, { "org.name": "Example", deployment: "prod", @@ -149,7 +148,30 @@ describe("OtelEnvironment", () => { }), ); - it.effect("lets OTEL_SERVICE_NAME win over the resource attribute", () => + it.effect("refuses to rename the service, and says so", () => + Effect.gen(function* () { + const resolved = yield* OtelEnvironment.load.pipe( + withEnv({ OTEL_SERVICE_NAME: "some-other-app" }), + ); + assert.deepStrictEqual(resolved.resource.attributes, {}); + assert.lengthOf(resolved.warnings, 1); + assert.include(resolved.warnings[0] ?? "", "OTEL_SERVICE_NAME was ignored"); + }), + ); + + it.effect("drops a service.name hidden in the resource attributes", () => + Effect.gen(function* () { + const resolved = yield* OtelEnvironment.load.pipe( + withEnv({ OTEL_RESOURCE_ATTRIBUTES: "service.name=some-other-app,host.name=lab-01" }), + ); + // Dropped rather than passed through, or the exporter would receive a + // second service.name beside the one the process chose. + assert.deepStrictEqual(resolved.resource.attributes, { "host.name": "lab-01" }); + assert.include(resolved.warnings[0] ?? "", "service.name was ignored"); + }), + ); + + it.effect("names OTEL_SERVICE_NAME rather than the attribute when both are set", () => Effect.gen(function* () { const resolved = yield* OtelEnvironment.load.pipe( withEnv({ @@ -157,7 +179,8 @@ describe("OtelEnvironment", () => { OTEL_RESOURCE_ATTRIBUTES: "service.name=from-attributes", }), ); - assert.strictEqual(resolved.resource.serviceName, "explicit"); + assert.lengthOf(resolved.warnings, 1); + assert.include(resolved.warnings[0] ?? "", "OTEL_SERVICE_NAME was ignored"); }), ); @@ -468,7 +491,8 @@ describe("OtelEnvironment", () => { OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "", }), ); - assert.strictEqual(resolved.resource.serviceName, undefined); + // An empty rename is not a rename, so it is not worth a warning either. + assert.deepStrictEqual(resolved.warnings, []); assert.strictEqual(resolved.traces.settings?.url, "https://collector.example.com/v1/traces"); }), ); @@ -545,11 +569,11 @@ describe("OtelEnvironment", () => { const resolved = yield* OtelEnvironment.load.pipe( withEnv({ OTEL_EXPORTER_OTLP_ENDPOINT: " https://collector.example.com/ ", - OTEL_SERVICE_NAME: " t3 ", + OTEL_SERVICE_VERSION: " 1.2.3 ", }), ); assert.strictEqual(resolved.traces.settings?.url, "https://collector.example.com/v1/traces"); - assert.strictEqual(resolved.resource.serviceName, "t3"); + assert.strictEqual(resolved.resource.serviceVersion, "1.2.3"); }), ); diff --git a/packages/shared/src/otelEnvironment.ts b/packages/shared/src/otelEnvironment.ts index dcf90dee3d7e..b8e53e49a8e7 100644 --- a/packages/shared/src/otelEnvironment.ts +++ b/packages/shared/src/otelEnvironment.ts @@ -71,8 +71,15 @@ export interface OtlpSignal { readonly declined: string | undefined; } +/** + * What the environment may contribute to the resource. `service.name` is + * deliberately absent: each T3 Code process names itself and nothing here can + * rename it, so a fleet-wide `OTEL_SERVICE_NAME` cannot quietly merge two + * processes into one service or file T3 Code under some other app's name. An + * attempt to set it is reported through `warnings` rather than ignored in + * silence. + */ export interface OtlpResourceSettings { - readonly serviceName: string | undefined; readonly serviceVersion: string | undefined; readonly attributes: Readonly>; } @@ -412,13 +419,28 @@ const resolveResource = Effect.gen(function* () { "service.version": attributeVersion, ...rest } = parsed.value ?? {}; + // Named here only to say it was refused. Dropping it without a word is the + // failure this variable is prone to: the name never changes, the dashboards + // stay empty, and nothing in the log connects the two. + const declinedName = + (yield* optionalString("OTEL_SERVICE_NAME")) === undefined + ? attributeName === undefined + ? undefined + : "OTEL_RESOURCE_ATTRIBUTES=service.name" + : "OTEL_SERVICE_NAME"; return { value: { - serviceName: (yield* optionalString("OTEL_SERVICE_NAME")) ?? attributeName, serviceVersion: (yield* optionalString("OTEL_SERVICE_VERSION")) ?? attributeVersion, attributes: rest, }, - warnings: parsed.warnings, + warnings: [ + ...parsed.warnings, + ...(declinedName === undefined + ? [] + : [ + `${declinedName} was ignored; every T3 Code process names itself, so use OTEL_RESOURCE_ATTRIBUTES to tell instances apart instead`, + ]), + ], } satisfies Parsed; }); @@ -486,7 +508,7 @@ export const load: Effect.Effect = Effect.gen(function* () { traces: { settings: undefined, declined: UNREADABLE }, metrics: { settings: undefined, declined: UNREADABLE }, logs: { settings: undefined, declined: UNREADABLE }, - resource: { serviceName: undefined, serviceVersion: undefined, attributes: {} }, + resource: { serviceVersion: undefined, attributes: {} }, }), ), ), @@ -502,5 +524,5 @@ export const none: OtelEnvironment = { traces: noSignal, metrics: noSignal, logs: noSignal, - resource: { serviceName: undefined, serviceVersion: undefined, attributes: {} }, + resource: { serviceVersion: undefined, attributes: {} }, };