diff --git a/apps/desktop/src/app/DesktopObservability.test.ts b/apps/desktop/src/app/DesktopObservability.test.ts index 0e4c4d7a969d..e23d78aa2161 100644 --- a/apps/desktop/src/app/DesktopObservability.test.ts +++ b/apps/desktop/src/app/DesktopObservability.test.ts @@ -477,6 +477,49 @@ describe("DesktopObservability", () => { ); }); + it.effect("keeps its service name while OTEL resource attributes add dimensions", () => { + const requests: Array = []; + return Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const baseDir = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-desktop-observability-test-", + }); + const environmentLayer = makeEnvironmentLayer(baseDir, true, { + T3CODE_OTLP_LOGS_URL: "https://collector.example.com/v1/logs", + }); + + yield* Effect.scoped( + Effect.logInfo("desktop service name").pipe( + Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))), + ), + ); + + assert.lengthOf(requests, 1); + const body = requests[0]?.body ?? ""; + assert.include(body, '"stringValue":"t3code-desktop"'); + assert.include(body, "deployment.environment.name"); + assert.include(body, '"key":"service.namespace","value":{"stringValue":"t3code"}'); + assert.notInclude(body, "renamed"); + }).pipe( + Effect.scoped, + Effect.provide( + Layer.mergeAll( + NodeServices.layer, + collectorLayer(requests), + ConfigProvider.layer( + ConfigProvider.fromEnv({ + env: { + OTEL_SERVICE_NAME: "renamed", + OTEL_RESOURCE_ATTRIBUTES: + "service.name=renamed,service.namespace=renamed,deployment.environment.name=development", + }, + }), + ), + ), + ), + ); + }); + it.effect("exports nothing to Settings for logs an unusable OTEL endpoint claimed", () => { const requests: Array = []; return Effect.gen(function* () { diff --git a/apps/desktop/src/app/DesktopObservability.ts b/apps/desktop/src/app/DesktopObservability.ts index 0bcb4d602102..ce8233e56612 100644 --- a/apps/desktop/src/app/DesktopObservability.ts +++ b/apps/desktop/src/app/DesktopObservability.ts @@ -626,8 +626,9 @@ const telemetryLayer = Layer.unwrap( const environment = yield* DesktopEnvironment.DesktopEnvironment; const endpoints = yield* resolveOtlpEndpoints; const resource = { - serviceName: "desktop", + serviceName: "t3code-desktop", attributes: { + "service.namespace": "t3code", "service.runtime": "desktop", "service.mode": environment.isDevelopment ? "development" : "packaged", }, diff --git a/apps/mobile/src/features/observability/tracing.ts b/apps/mobile/src/features/observability/tracing.ts index eb73abba292b..ae204413e777 100644 --- a/apps/mobile/src/features/observability/tracing.ts +++ b/apps/mobile/src/features/observability/tracing.ts @@ -25,7 +25,7 @@ export function resolveTracingConfig(): TracingConfig | null { export function makeTracingLayer(config: TracingConfig | null, resource: TracingResource) { return makeRelayClientTracingLayer(config, { - serviceName: "t3-mobile-relay-client", + serviceName: "t3code-mobile", serviceVersion: resource.serviceVersion, runtime: "react-native", client: `mobile-${resource.appVariant}`, diff --git a/apps/server/src/bin.test.ts b/apps/server/src/bin.test.ts index 31a96304c3e5..aecf82eeac67 100644 --- a/apps/server/src/bin.test.ts +++ b/apps/server/src/bin.test.ts @@ -106,7 +106,6 @@ const makeCliTestServerConfig = (baseDir: string) => otlpTracesExport: DEFAULT_SIGNAL_EXPORT, otlpMetricsExport: DEFAULT_SIGNAL_EXPORT, otlpLogsExport: DEFAULT_SIGNAL_EXPORT, - otlpServiceName: "t3-server", otelEnvironment: OtelEnvironment.none, mode: "web", port: 0, diff --git a/apps/server/src/cli/config.test.ts b/apps/server/src/cli/config.test.ts index 6b42756ad175..ef7cdd578899 100644 --- a/apps/server/src/cli/config.test.ts +++ b/apps/server/src/cli/config.test.ts @@ -56,7 +56,6 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => { otlpTracesExport: DEFAULT_SIGNAL_EXPORT, otlpMetricsExport: DEFAULT_SIGNAL_EXPORT, otlpLogsExport: DEFAULT_SIGNAL_EXPORT, - otlpServiceName: "t3-server", otelEnvironment: OtelEnvironment.none, devAllowedOrigins: [], } as const; diff --git a/apps/server/src/cli/config.ts b/apps/server/src/cli/config.ts index 9c1ceac82101..d724730c953f 100644 --- a/apps/server/src/cli/config.ts +++ b/apps/server/src/cli/config.ts @@ -108,7 +108,6 @@ const EnvServerConfig = Config.all({ otlpExportIntervalMs: Config.Int("T3CODE_OTLP_EXPORT_INTERVAL_MS").pipe( Config.withDefault(10_000), ), - otlpServiceName: Config.String("T3CODE_OTLP_SERVICE_NAME").pipe(Config.withDefault("t3-server")), otlpHeaders: Config.schema(OtlpHeadersFromString, "T3CODE_OTLP_HEADERS").pipe( Config.option, Config.map(Option.getOrUndefined), @@ -431,7 +430,6 @@ export const resolveServerConfig = ( otlpTracesExport: traces?.export ?? signalExport, otlpMetricsExport: metrics?.export ?? signalExport, otlpLogsExport: logs?.export ?? signalExport, - otlpServiceName: env.otlpServiceName, otelEnvironment: otel, mode, port, diff --git a/apps/server/src/cli/pair.ts b/apps/server/src/cli/pair.ts index 6b29ef60927f..04ce0332c14c 100644 --- a/apps/server/src/cli/pair.ts +++ b/apps/server/src/cli/pair.ts @@ -326,7 +326,6 @@ const makePairServerConfig = Effect.fn(function* (input: { otlpTracesExport: DEFAULT_SIGNAL_EXPORT, otlpMetricsExport: DEFAULT_SIGNAL_EXPORT, otlpLogsExport: DEFAULT_SIGNAL_EXPORT, - otlpServiceName: "t3-server", otelEnvironment: OtelEnvironment.none, mode: "web", port: state.port, diff --git a/apps/server/src/cloud/relayTracing.ts b/apps/server/src/cloud/relayTracing.ts index e35c94545a5e..eeea28a2b68f 100644 --- a/apps/server/src/cloud/relayTracing.ts +++ b/apps/server/src/cloud/relayTracing.ts @@ -7,14 +7,14 @@ const relayClientTracingConfig = resolveRelayClientTracingConfig(); export const headlessRelayClientTracingLayer = makeRelayClientTracingLayer( relayClientTracingConfig, { - serviceName: "t3-headless-relay-client", + serviceName: "t3code-server", runtime: "node", client: "headless-cli", }, ); export const serverRelayBrokerTracingLayer = makeRelayClientTracingLayer(relayClientTracingConfig, { - serviceName: "t3-server", + serviceName: "t3code-server", runtime: "node", client: "environment-server", component: "relay-broker", diff --git a/apps/server/src/config.ts b/apps/server/src/config.ts index 344619c0eb93..74ffde8efef0 100644 --- a/apps/server/src/config.ts +++ b/apps/server/src/config.ts @@ -82,7 +82,6 @@ export class ServerConfig extends Context.Service< readonly otlpTracesExport: SignalExport; readonly otlpMetricsExport: SignalExport; readonly otlpLogsExport: SignalExport; - readonly otlpServiceName: string; readonly otelEnvironment: OtelEnvironment.OtelEnvironment; readonly mode: RuntimeMode; readonly port: number; @@ -119,8 +118,9 @@ export const make = (config: ServerConfig["Service"]) => ServerConfig.of(config) * logs report the same service identity to the collector. */ export const otlpResource = (config: ServerConfig["Service"]) => ({ - serviceName: config.otlpServiceName, + serviceName: "t3code-server", attributes: { + "service.namespace": "t3code", "service.runtime": "t3-server", "service.mode": config.mode, }, @@ -222,7 +222,6 @@ const makeTest = Effect.fn("ServerConfig.makeTest")(function* ( otlpTracesExport: DEFAULT_SIGNAL_EXPORT, otlpMetricsExport: DEFAULT_SIGNAL_EXPORT, otlpLogsExport: DEFAULT_SIGNAL_EXPORT, - otlpServiceName: "t3-server", otelEnvironment: OtelEnvironment.none, cwd, baseDir, diff --git a/apps/server/src/environment/ServerEnvironment.test.ts b/apps/server/src/environment/ServerEnvironment.test.ts index 12ae321c1cf3..6eb95c84b477 100644 --- a/apps/server/src/environment/ServerEnvironment.test.ts +++ b/apps/server/src/environment/ServerEnvironment.test.ts @@ -59,7 +59,6 @@ const makeServerConfig = Effect.fn(function* (baseDir: string) { otlpTracesExport: DEFAULT_SIGNAL_EXPORT, otlpMetricsExport: DEFAULT_SIGNAL_EXPORT, otlpLogsExport: DEFAULT_SIGNAL_EXPORT, - otlpServiceName: "t3-server", otelEnvironment: OtelEnvironment.none, cwd: process.cwd(), baseDir, diff --git a/apps/server/src/persistence/Layers/Sqlite.ts b/apps/server/src/persistence/Layers/Sqlite.ts index 88342cbf1fad..032b44645089 100644 --- a/apps/server/src/persistence/Layers/Sqlite.ts +++ b/apps/server/src/persistence/Layers/Sqlite.ts @@ -32,7 +32,7 @@ export const makeSqlitePersistenceLive = Effect.fn("makeSqlitePersistenceLive")( filename: dbPath, spanAttributes: { "db.name": path.basename(dbPath), - "service.name": "t3-server", + "service.name": "t3code-server", }, }), ); diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts index 12f37780701d..6836d28e25c1 100644 --- a/apps/server/src/server.test.ts +++ b/apps/server/src/server.test.ts @@ -489,7 +489,7 @@ const makeBrowserOtlpPayload = (spanName: string) => url: collector.url, exportInterval: "10 millis", resource: { - serviceName: "t3-web", + serviceName: "t3code-web", attributes: { "service.runtime": "t3-web", "service.mode": "browser", @@ -588,7 +588,6 @@ const buildAppUnderTest = (options?: { otlpTracesExport: DEFAULT_SIGNAL_EXPORT, otlpMetricsExport: DEFAULT_SIGNAL_EXPORT, otlpLogsExport: DEFAULT_SIGNAL_EXPORT, - otlpServiceName: "t3-server", otelEnvironment: OtelEnvironment.none, mode: "desktop", port: 0, @@ -5283,7 +5282,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { attributes: [ { key: "service.name", - value: { stringValue: "t3-web" }, + value: { stringValue: "t3code-web" }, }, ], }, @@ -5425,7 +5424,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { "rpc.method": "server.getSettings", }, resourceAttributes: { - "service.name": "t3-web", + "service.name": "t3code-web", }, scope: { name: "effect", @@ -5556,7 +5555,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { // the stub's utf8 decode even though the surrounding bytes don't. assert.notEqual(forwarded.body[0], "{"); assert.include(forwarded.body, "client.protobuf.test"); - assert.include(forwarded.body, "t3-web"); + assert.include(forwarded.body, "t3code-web"); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); @@ -5657,7 +5656,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { assert.deepEqual(record.links, []); assert.equal(record.scope.name, scopeSpan.scope.name); assert.deepEqual(record.scope.attributes, {}); - assert.equal(record.resourceAttributes["service.name"], "t3-web"); + assert.equal(record.resourceAttributes["service.name"], "t3code-web"); assert.equal(record.status?.code, String(span.status.code)); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); diff --git a/apps/server/src/serverLogger.test.ts b/apps/server/src/serverLogger.test.ts index a5437582d2e3..43843b249eea 100644 --- a/apps/server/src/serverLogger.test.ts +++ b/apps/server/src/serverLogger.test.ts @@ -1,6 +1,7 @@ import * as NodePath from "@effect/platform-node/NodePath"; import { assert, describe, it } from "@effect/vitest"; import * as NodeOS from "node:os"; +import * as ConfigProvider from "effect/ConfigProvider"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Path from "effect/Path"; @@ -57,7 +58,6 @@ const configLayer = (overrides: Partial) = otlpTracesExport: DEFAULT_SIGNAL_EXPORT, otlpMetricsExport: DEFAULT_SIGNAL_EXPORT, otlpLogsExport: DEFAULT_SIGNAL_EXPORT, - otlpServiceName: "t3-server", otelEnvironment: OtelEnvironment.none, cwd: baseDir, baseDir, @@ -146,11 +146,38 @@ describe("ServerLoggerLive", () => { const [request] = requests; assert.strictEqual(request?.url, "https://collector.example.com/v1/logs"); assert.include(request?.body ?? "", "server logger under test"); - assert.include(request?.body ?? "", "t3-server"); + assert.include(request?.body ?? "", "t3code-server"); assert.include(request?.body ?? "", "service.runtime"); }), ); + it.effect("keeps its service name while OTEL resource attributes add dimensions", () => + Effect.gen(function* () { + const requests = yield* logThrough({ + otlpLogsUrl: "https://collector.example.com/v1/logs", + }).pipe( + Effect.provide( + ConfigProvider.layer( + ConfigProvider.fromEnv({ + env: { + OTEL_SERVICE_NAME: "renamed", + OTEL_RESOURCE_ATTRIBUTES: + "service.name=renamed,service.namespace=renamed,deployment.environment.name=development", + }, + }), + ), + ), + ); + + assert.lengthOf(requests, 1); + const body = requests[0]?.body ?? ""; + assert.include(body, '"stringValue":"t3code-server"'); + assert.include(body, "deployment.environment.name"); + assert.include(body, '"key":"service.namespace","value":{"stringValue":"t3code"}'); + assert.notInclude(body, "renamed"); + }), + ); + it.effect("stays off the network when no logs endpoint is configured", () => Effect.gen(function* () { const requests = yield* logThrough({}); diff --git a/apps/web/src/lib/runtime.ts b/apps/web/src/lib/runtime.ts index 866f38875c76..fd4f2b194096 100644 --- a/apps/web/src/lib/runtime.ts +++ b/apps/web/src/lib/runtime.ts @@ -19,7 +19,7 @@ function configuredRelayUrl(): string { const httpClientLayer = remoteHttpClientLayer((input, init) => globalThis.fetch(input, init)); const relayTracingLayer = makeRelayClientTracingLayer(resolveRelayTracingConfig(), { - serviceName: "t3-web-relay-client", + serviceName: "t3code-web", serviceVersion: import.meta.env.APP_VERSION, runtime: "browser", client: typeof window !== "undefined" && window.desktopBridge ? "desktop" : "web", diff --git a/apps/web/src/observability/clientTracing.ts b/apps/web/src/observability/clientTracing.ts index 2471de958615..e8ccd4173d35 100644 --- a/apps/web/src/observability/clientTracing.ts +++ b/apps/web/src/observability/clientTracing.ts @@ -15,8 +15,9 @@ import { APP_VERSION } from "~/branding"; const DEFAULT_EXPORT_INTERVAL_MS = 1_000; const CLIENT_TRACING_RESOURCE = { - serviceName: "t3-web", + serviceName: "t3code-web", attributes: { + "service.namespace": "t3code", "service.runtime": "t3-web", "service.mode": isElectron ? "electron" : "browser", "service.version": APP_VERSION, diff --git a/docs/operations/observability.md b/docs/operations/observability.md index abe82d680b97..86a65702c270 100644 --- a/docs/operations/observability.md +++ b/docs/operations/observability.md @@ -129,7 +129,7 @@ Default Grafana login: export T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces export T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics export T3CODE_OTLP_LOGS_URL=http://localhost:4318/v1/logs -export T3CODE_OTLP_SERVICE_NAME=t3-local +export OTEL_RESOURCE_ATTRIBUTES=deployment.environment.name=development ``` Optional: @@ -169,7 +169,6 @@ macOS app bundle example: T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces \ T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics \ T3CODE_OTLP_LOGS_URL=http://localhost:4318/v1/logs \ -T3CODE_OTLP_SERVICE_NAME=t3-desktop \ "/Applications/T3 Code.app/Contents/MacOS/T3 Code" ``` @@ -179,7 +178,6 @@ Direct binary example: T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces \ T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics \ T3CODE_OTLP_LOGS_URL=http://localhost:4318/v1/logs \ -T3CODE_OTLP_SERVICE_NAME=t3-desktop \ ./path/to/your/desktop-app-binary ``` @@ -311,11 +309,13 @@ Recommended flow in Grafana: 2. Pick the `Tempo` data source. 3. Set the time range to something recent like `Last 15 minutes`. 4. Start broad. Do not begin with a very narrow query. -5. Look for spans from your configured service name, then narrow by span name or attributes. +5. Look for spans from the `t3code-server` or `t3code-desktop` service, then narrow by span name or + attributes. Good first searches: -- service name such as `t3-local`, `t3-dev`, or `t3-desktop` +- service name `t3code-server` or `t3code-desktop`, plus a resource attribute such as + `deployment.environment.name` - span names like `sendTurn` or a Git operation such as `GitVcsDriver.statusDetails.status` - Git spans whose `git.operation` attribute identifies the operation - orchestration spans with attributes like `orchestration.command_type` @@ -525,10 +525,10 @@ It provides: The desktop main process is a second producer, assembled in `apps/desktop/src/app/DesktopObservability.ts`. It reads the same `T3CODE_OTLP_*` names and the same Settings entries as the backend it supervises, and covers work the backend cannot see: app startup, -window and menu handling, backend supervision, and updates. It reports as service `desktop` -regardless of `T3CODE_OTLP_SERVICE_NAME`, so a collector shows it alongside the backend rather than -mixed into it. It exports traces and logs only; the main process records no metrics, so the metrics -endpoint applies to the backend alone. +window and menu handling, backend supervision, and updates. It reports as service +`t3code-desktop`, so a collector shows it alongside the backend rather than mixed into it. It +exports traces and logs only; the main process records no metrics, so the metrics endpoint applies +to the backend alone. ### Env Vars @@ -547,7 +547,6 @@ OTLP export: - `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_HEADERS`: extra headers for all three exporters, same format as `OTEL_EXPORTER_OTLP_HEADERS`: comma-separated `key=value` pairs with percent-encoded values. - `T3CODE_OTLP_PROTOCOL`: `http/json` (default) or `http/protobuf` @@ -565,6 +564,11 @@ an `http` or `https` URL, a protocol other than `http/protobuf` or `http/json` s headers that are not `key=value` pairs with percent-encoded values turn that signal's export off with a startup warning, rather than sending it to the Settings endpoint. +Service names are fixed: `t3code-server` for the backend and `t3code-desktop` for the desktop main +process, both in `service.namespace` `t3code`. `OTEL_SERVICE_NAME` and a `service.name` or +`service.namespace` in `OTEL_RESOURCE_ATTRIBUTES` are ignored. Tell installations apart with other +resource attributes, such as `OTEL_RESOURCE_ATTRIBUTES=deployment.environment.name=development`. + If the OTLP URLs are unset, local tracing still works, metrics stay in-process only, and logs stay on stdout only. diff --git a/infra/relay/src/observability.test.ts b/infra/relay/src/observability.test.ts index 3958b28b95dd..d77c01277f1c 100644 --- a/infra/relay/src/observability.test.ts +++ b/infra/relay/src/observability.test.ts @@ -62,6 +62,11 @@ it.effect("exports schema error fields as span attributes", () => const request = yield* Deferred.await(exportedRequest).pipe(Effect.timeout("1 second")); const payload = (yield* decodeJson(request.body)) as OtlpTracer.TraceData; + const resourceAttributes = Object.fromEntries( + payload.resourceSpans + .flatMap((resourceSpan) => resourceSpan.resource.attributes) + .map((attribute) => [attribute.key, otlpAttributeValue(attribute.value)]), + ); const span = payload.resourceSpans .flatMap((resourceSpan) => resourceSpan.scopeSpans) .flatMap((scopeSpan) => scopeSpan.spans) @@ -75,6 +80,10 @@ it.effect("exports schema error fields as span attributes", () => expect(request.authorization).toBe("Bearer test-token"); expect(request.dataset).toBe("relay-test-traces"); + expect(resourceAttributes).toMatchObject({ + "service.name": "t3code-relay", + "service.namespace": "t3code", + }); expect(attributes).toMatchObject({ "error.type": "EnvironmentConnectNotAuthorized", "error.environmentId": "environment-1", diff --git a/infra/relay/src/observability.ts b/infra/relay/src/observability.ts index ca091d8cce6b..2325d7e0432a 100644 --- a/infra/relay/src/observability.ts +++ b/infra/relay/src/observability.ts @@ -222,8 +222,9 @@ export const makeRelayTraceLayer = (input: { OtlpTracer.make({ url: input.tracesEndpoint, resource: { - serviceName: "t3-code-relay-worker", + serviceName: "t3code-relay", attributes: { + "service.namespace": "t3code", "service.runtime": "cloudflare-worker", "service.component": "relay", }, diff --git a/packages/shared/src/relayTracing.test.ts b/packages/shared/src/relayTracing.test.ts index 3bb7f1ea1ac2..f96dfea8bb3b 100644 --- a/packages/shared/src/relayTracing.test.ts +++ b/packages/shared/src/relayTracing.test.ts @@ -97,6 +97,8 @@ describe("withRelayClientTracing", () => { const payload = new TextDecoder().decode(fetchFn.mock.calls[0]?.[1]?.body as Uint8Array); expect(payload).toContain("relay request failed"); expect(payload).toContain("relay socket closed"); + expect(payload).toContain('"key":"service.name","value":{"stringValue":"relay-test"}'); + expect(payload).toContain('"key":"service.namespace","value":{"stringValue":"t3code"}'); }), ), ); diff --git a/packages/shared/src/relayTracing.ts b/packages/shared/src/relayTracing.ts index 907773bc15a5..76954558eb07 100644 --- a/packages/shared/src/relayTracing.ts +++ b/packages/shared/src/relayTracing.ts @@ -142,6 +142,7 @@ export function makeRelayClientTracingLayer( serviceName: resource.serviceName, serviceVersion: resource.serviceVersion, attributes: { + "service.namespace": "t3code", "service.runtime": resource.runtime, "service.component": resource.component ?? "relay-client", "t3.client.surface": resource.client,