authorgravatar for yordis.prieto@gmail.comYordis Prieto <yordis.prieto@gmail.com> 2026-09-25 20:49:17-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-25 17:49:17-07:00
log2a9832b8019039f812706639c11ccc24ce4c4e05
tree25faa1b78412c96c4af7c5dd8f6c8b2662ead145
parent595a1e1f537f403eba408817c2aba35e61c2ac29
signature Signed by PGP key B5690EEEBB952194

refactor(observability): name each service after its application (#13699)

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>

20 files changed, 116 insertions(+), 35 deletions(-)

apps/desktop/src/app/DesktopObservability.test.ts+43
......@@ -477,6 +477,49 @@ describe("DesktopObservability", () => {
477477 );
478478 });
479479
480 it.effect("keeps its service name while OTEL resource attributes add dimensions", () => {
481 const requests: Array<ExportedRequest> = [];
482 return Effect.gen(function* () {
483 const fileSystem = yield* FileSystem.FileSystem;
484 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
485 prefix: "t3-desktop-observability-test-",
486 });
487 const environmentLayer = makeEnvironmentLayer(baseDir, true, {
488 T3CODE_OTLP_LOGS_URL: "https://collector.example.com/v1/logs",
489 });
490
491 yield* Effect.scoped(
492 Effect.logInfo("desktop service name").pipe(
493 Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))),
494 ),
495 );
496
497 assert.lengthOf(requests, 1);
498 const body = requests[0]?.body ?? "";
499 assert.include(body, '"stringValue":"t3code-desktop"');
500 assert.include(body, "deployment.environment.name");
501 assert.include(body, '"key":"service.namespace","value":{"stringValue":"t3code"}');
502 assert.notInclude(body, "renamed");
503 }).pipe(
504 Effect.scoped,
505 Effect.provide(
506 Layer.mergeAll(
507 NodeServices.layer,
508 collectorLayer(requests),
509 ConfigProvider.layer(
510 ConfigProvider.fromEnv({
511 env: {
512 OTEL_SERVICE_NAME: "renamed",
513 OTEL_RESOURCE_ATTRIBUTES:
514 "service.name=renamed,service.namespace=renamed,deployment.environment.name=development",
515 },
516 }),
517 ),
518 ),
519 ),
520 );
521 });
522
480523 it.effect("exports nothing to Settings for logs an unusable OTEL endpoint claimed", () => {
481524 const requests: Array<ExportedRequest> = [];
482525 return Effect.gen(function* () {
apps/desktop/src/app/DesktopObservability.ts+2-1
......@@ -626,8 +626,9 @@ const telemetryLayer = Layer.unwrap(
626626 const environment = yield* DesktopEnvironment.DesktopEnvironment;
627627 const endpoints = yield* resolveOtlpEndpoints;
628628 const resource = {
629 serviceName: "desktop",
629 serviceName: "t3code-desktop",
630630 attributes: {
631 "service.namespace": "t3code",
631632 "service.runtime": "desktop",
632633 "service.mode": environment.isDevelopment ? "development" : "packaged",
633634 },
apps/mobile/src/features/observability/tracing.ts+1-1
......@@ -25,7 +25,7 @@ export function resolveTracingConfig(): TracingConfig | null {
2525
2626export function makeTracingLayer(config: TracingConfig | null, resource: TracingResource) {
2727 return makeRelayClientTracingLayer(config, {
28 serviceName: "t3-mobile-relay-client",
28 serviceName: "t3code-mobile",
2929 serviceVersion: resource.serviceVersion,
3030 runtime: "react-native",
3131 client: `mobile-${resource.appVariant}`,
apps/server/src/bin.test.ts-1
......@@ -106,7 +106,6 @@ const makeCliTestServerConfig = (baseDir: string) =>
106106 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,
107107 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,
108108 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,
109 otlpServiceName: "t3-server",
110109 otelEnvironment: OtelEnvironment.none,
111110 mode: "web",
112111 port: 0,
apps/server/src/cli/config.test.ts-1
......@@ -56,7 +56,6 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
5656 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,
5757 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,
5858 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,
59 otlpServiceName: "t3-server",
6059 otelEnvironment: OtelEnvironment.none,
6160 devAllowedOrigins: [],
6261 } as const;
apps/server/src/cli/config.ts-2
......@@ -108,7 +108,6 @@ const EnvServerConfig = Config.all({
108108 otlpExportIntervalMs: Config.Int("T3CODE_OTLP_EXPORT_INTERVAL_MS").pipe(
109109 Config.withDefault(10_000),
110110 ),
111 otlpServiceName: Config.String("T3CODE_OTLP_SERVICE_NAME").pipe(Config.withDefault("t3-server")),
112111 otlpHeaders: Config.schema(OtlpHeadersFromString, "T3CODE_OTLP_HEADERS").pipe(
113112 Config.option,
114113 Config.map(Option.getOrUndefined),
......@@ -431,7 +430,6 @@ export const resolveServerConfig = (
431430 otlpTracesExport: traces?.export ?? signalExport,
432431 otlpMetricsExport: metrics?.export ?? signalExport,
433432 otlpLogsExport: logs?.export ?? signalExport,
434 otlpServiceName: env.otlpServiceName,
435433 otelEnvironment: otel,
436434 mode,
437435 port,
apps/server/src/cli/pair.ts-1
......@@ -326,7 +326,6 @@ const makePairServerConfig = Effect.fn(function* (input: {
326326 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,
327327 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,
328328 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,
329 otlpServiceName: "t3-server",
330329 otelEnvironment: OtelEnvironment.none,
331330 mode: "web",
332331 port: state.port,
apps/server/src/cloud/relayTracing.ts+2-2
......@@ -7,14 +7,14 @@ const relayClientTracingConfig = resolveRelayClientTracingConfig();
77export const headlessRelayClientTracingLayer = makeRelayClientTracingLayer(
88 relayClientTracingConfig,
99 {
10 serviceName: "t3-headless-relay-client",
10 serviceName: "t3code-server",
1111 runtime: "node",
1212 client: "headless-cli",
1313 },
1414);
1515
1616export const serverRelayBrokerTracingLayer = makeRelayClientTracingLayer(relayClientTracingConfig, {
17 serviceName: "t3-server",
17 serviceName: "t3code-server",
1818 runtime: "node",
1919 client: "environment-server",
2020 component: "relay-broker",
apps/server/src/config.ts+2-3
......@@ -82,7 +82,6 @@ export class ServerConfig extends Context.Service<
8282 readonly otlpTracesExport: SignalExport;
8383 readonly otlpMetricsExport: SignalExport;
8484 readonly otlpLogsExport: SignalExport;
85 readonly otlpServiceName: string;
8685 readonly otelEnvironment: OtelEnvironment.OtelEnvironment;
8786 readonly mode: RuntimeMode;
8887 readonly port: number;
......@@ -119,8 +118,9 @@ export const make = (config: ServerConfig["Service"]) => ServerConfig.of(config)
119118 * logs report the same service identity to the collector.
120119 */
121120export const otlpResource = (config: ServerConfig["Service"]) => ({
122 serviceName: config.otlpServiceName,
121 serviceName: "t3code-server",
123122 attributes: {
123 "service.namespace": "t3code",
124124 "service.runtime": "t3-server",
125125 "service.mode": config.mode,
126126 },
......@@ -222,7 +222,6 @@ const makeTest = Effect.fn("ServerConfig.makeTest")(function* (
222222 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,
223223 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,
224224 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,
225 otlpServiceName: "t3-server",
226225 otelEnvironment: OtelEnvironment.none,
227226 cwd,
228227 baseDir,
apps/server/src/environment/ServerEnvironment.test.ts-1
......@@ -59,7 +59,6 @@ const makeServerConfig = Effect.fn(function* (baseDir: string) {
5959 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,
6060 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,
6161 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,
62 otlpServiceName: "t3-server",
6362 otelEnvironment: OtelEnvironment.none,
6463 cwd: process.cwd(),
6564 baseDir,
apps/server/src/persistence/Layers/Sqlite.ts+1-1
......@@ -32,7 +32,7 @@ export const makeSqlitePersistenceLive = Effect.fn("makeSqlitePersistenceLive")(
3232 filename: dbPath,
3333 spanAttributes: {
3434 "db.name": path.basename(dbPath),
35 "service.name": "t3-server",
35 "service.name": "t3code-server",
3636 },
3737 }),
3838 );
apps/server/src/server.test.ts+5-6
......@@ -489,7 +489,7 @@ const makeBrowserOtlpPayload = (spanName: string) =>
489489 url: collector.url,
490490 exportInterval: "10 millis",
491491 resource: {
492 serviceName: "t3-web",
492 serviceName: "t3code-web",
493493 attributes: {
494494 "service.runtime": "t3-web",
495495 "service.mode": "browser",
......@@ -588,7 +588,6 @@ const buildAppUnderTest = (options?: {
588588 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,
589589 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,
590590 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,
591 otlpServiceName: "t3-server",
592591 otelEnvironment: OtelEnvironment.none,
593592 mode: "desktop",
594593 port: 0,
......@@ -5283,7 +5282,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
52835282 attributes: [
52845283 {
52855284 key: "service.name",
5286 value: { stringValue: "t3-web" },
5285 value: { stringValue: "t3code-web" },
52875286 },
52885287 ],
52895288 },
......@@ -5425,7 +5424,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
54255424 "rpc.method": "server.getSettings",
54265425 },
54275426 resourceAttributes: {
5428 "service.name": "t3-web",
5427 "service.name": "t3code-web",
54295428 },
54305429 scope: {
54315430 name: "effect",
......@@ -5556,7 +5555,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
55565555 // the stub's utf8 decode even though the surrounding bytes don't.
55575556 assert.notEqual(forwarded.body[0], "{");
55585557 assert.include(forwarded.body, "client.protobuf.test");
5559 assert.include(forwarded.body, "t3-web");
5558 assert.include(forwarded.body, "t3code-web");
55605559 }).pipe(Effect.provide(NodeHttpServer.layerTest)),
55615560 );
55625561
......@@ -5657,7 +5656,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
56575656 assert.deepEqual(record.links, []);
56585657 assert.equal(record.scope.name, scopeSpan.scope.name);
56595658 assert.deepEqual(record.scope.attributes, {});
5660 assert.equal(record.resourceAttributes["service.name"], "t3-web");
5659 assert.equal(record.resourceAttributes["service.name"], "t3code-web");
56615660 assert.equal(record.status?.code, String(span.status.code));
56625661 }).pipe(Effect.provide(NodeHttpServer.layerTest)),
56635662 );
apps/server/src/serverLogger.test.ts+29-2
......@@ -1,6 +1,7 @@
11import * as NodePath from "@effect/platform-node/NodePath";
22import { assert, describe, it } from "@effect/vitest";
33import * as NodeOS from "node:os";
4import * as ConfigProvider from "effect/ConfigProvider";
45import * as Effect from "effect/Effect";
56import * as Layer from "effect/Layer";
67import * as Path from "effect/Path";
......@@ -57,7 +58,6 @@ const configLayer = (overrides: Partial<ServerConfig.ServerConfig["Service"]>) =
5758 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,
5859 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,
5960 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,
60 otlpServiceName: "t3-server",
6161 otelEnvironment: OtelEnvironment.none,
6262 cwd: baseDir,
6363 baseDir,
......@@ -146,11 +146,38 @@ describe("ServerLoggerLive", () => {
146146 const [request] = requests;
147147 assert.strictEqual(request?.url, "https://collector.example.com/v1/logs");
148148 assert.include(request?.body ?? "", "server logger under test");
149 assert.include(request?.body ?? "", "t3-server");
149 assert.include(request?.body ?? "", "t3code-server");
150150 assert.include(request?.body ?? "", "service.runtime");
151151 }),
152152 );
153153
154 it.effect("keeps its service name while OTEL resource attributes add dimensions", () =>
155 Effect.gen(function* () {
156 const requests = yield* logThrough({
157 otlpLogsUrl: "https://collector.example.com/v1/logs",
158 }).pipe(
159 Effect.provide(
160 ConfigProvider.layer(
161 ConfigProvider.fromEnv({
162 env: {
163 OTEL_SERVICE_NAME: "renamed",
164 OTEL_RESOURCE_ATTRIBUTES:
165 "service.name=renamed,service.namespace=renamed,deployment.environment.name=development",
166 },
167 }),
168 ),
169 ),
170 );
171
172 assert.lengthOf(requests, 1);
173 const body = requests[0]?.body ?? "";
174 assert.include(body, '"stringValue":"t3code-server"');
175 assert.include(body, "deployment.environment.name");
176 assert.include(body, '"key":"service.namespace","value":{"stringValue":"t3code"}');
177 assert.notInclude(body, "renamed");
178 }),
179 );
180
154181 it.effect("stays off the network when no logs endpoint is configured", () =>
155182 Effect.gen(function* () {
156183 const requests = yield* logThrough({});
apps/web/src/lib/runtime.ts+1-1
......@@ -19,7 +19,7 @@ function configuredRelayUrl(): string {
1919
2020const httpClientLayer = remoteHttpClientLayer((input, init) => globalThis.fetch(input, init));
2121const relayTracingLayer = makeRelayClientTracingLayer(resolveRelayTracingConfig(), {
22 serviceName: "t3-web-relay-client",
22 serviceName: "t3code-web",
2323 serviceVersion: import.meta.env.APP_VERSION,
2424 runtime: "browser",
2525 client: typeof window !== "undefined" && window.desktopBridge ? "desktop" : "web",
apps/web/src/observability/clientTracing.ts+2-1
......@@ -15,8 +15,9 @@ import { APP_VERSION } from "~/branding";
1515
1616const DEFAULT_EXPORT_INTERVAL_MS = 1_000;
1717const CLIENT_TRACING_RESOURCE = {
18 serviceName: "t3-web",
18 serviceName: "t3code-web",
1919 attributes: {
20 "service.namespace": "t3code",
2021 "service.runtime": "t3-web",
2122 "service.mode": isElectron ? "electron" : "browser",
2223 "service.version": APP_VERSION,
docs/operations/observability.md+14-10
......@@ -129,7 +129,7 @@ Default Grafana login:
129129export T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces
130130export T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics
131131export T3CODE_OTLP_LOGS_URL=http://localhost:4318/v1/logs
132export T3CODE_OTLP_SERVICE_NAME=t3-local
132export OTEL_RESOURCE_ATTRIBUTES=deployment.environment.name=development
133133```
134134
135135Optional:
......@@ -169,7 +169,6 @@ macOS app bundle example:
169169T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces \
170170T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics \
171171T3CODE_OTLP_LOGS_URL=http://localhost:4318/v1/logs \
172T3CODE_OTLP_SERVICE_NAME=t3-desktop \
173172"/Applications/T3 Code.app/Contents/MacOS/T3 Code"
174173```
175174
......@@ -179,7 +178,6 @@ Direct binary example:
179178T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces \
180179T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics \
181180T3CODE_OTLP_LOGS_URL=http://localhost:4318/v1/logs \
182T3CODE_OTLP_SERVICE_NAME=t3-desktop \
183181./path/to/your/desktop-app-binary
184182```
185183
......@@ -311,11 +309,13 @@ Recommended flow in Grafana:
3113092. Pick the `Tempo` data source.
3123103. Set the time range to something recent like `Last 15 minutes`.
3133114. Start broad. Do not begin with a very narrow query.
3145. Look for spans from your configured service name, then narrow by span name or attributes.
3125. Look for spans from the `t3code-server` or `t3code-desktop` service, then narrow by span name or
313 attributes.
315314
316315Good first searches:
317316
318- service name such as `t3-local`, `t3-dev`, or `t3-desktop`
317- service name `t3code-server` or `t3code-desktop`, plus a resource attribute such as
318 `deployment.environment.name`
319319- span names like `sendTurn` or a Git operation such as `GitVcsDriver.statusDetails.status`
320320- Git spans whose `git.operation` attribute identifies the operation
321321- orchestration spans with attributes like `orchestration.command_type`
......@@ -525,10 +525,10 @@ It provides:
525525The desktop main process is a second producer, assembled in
526526`apps/desktop/src/app/DesktopObservability.ts`. It reads the same `T3CODE_OTLP_*` names and the same
527527Settings entries as the backend it supervises, and covers work the backend cannot see: app startup,
528window and menu handling, backend supervision, and updates. It reports as service `desktop`
529regardless of `T3CODE_OTLP_SERVICE_NAME`, so a collector shows it alongside the backend rather than
530mixed into it. It exports traces and logs only; the main process records no metrics, so the metrics
531endpoint applies to the backend alone.
528window and menu handling, backend supervision, and updates. It reports as service
529`t3code-desktop`, so a collector shows it alongside the backend rather than mixed into it. It
530exports traces and logs only; the main process records no metrics, so the metrics endpoint applies
531to the backend alone.
532532
533533### Env Vars
534534
......@@ -547,7 +547,6 @@ OTLP export:
547547- `T3CODE_OTLP_METRICS_URL`: OTLP metric endpoint
548548- `T3CODE_OTLP_LOGS_URL`: OTLP log endpoint
549549- `T3CODE_OTLP_EXPORT_INTERVAL_MS`: export interval, default `10000`
550- `T3CODE_OTLP_SERVICE_NAME`: service name, default `t3-server`
551550- `T3CODE_OTLP_HEADERS`: extra headers for all three exporters, same format as
552551 `OTEL_EXPORTER_OTLP_HEADERS`: comma-separated `key=value` pairs with percent-encoded values.
553552- `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
565564headers that are not `key=value` pairs with percent-encoded values turn that signal's export off
566565with a startup warning, rather than sending it to the Settings endpoint.
567566
567Service names are fixed: `t3code-server` for the backend and `t3code-desktop` for the desktop main
568process, both in `service.namespace` `t3code`. `OTEL_SERVICE_NAME` and a `service.name` or
569`service.namespace` in `OTEL_RESOURCE_ATTRIBUTES` are ignored. Tell installations apart with other
570resource attributes, such as `OTEL_RESOURCE_ATTRIBUTES=deployment.environment.name=development`.
571
568572If the OTLP URLs are unset, local tracing still works, metrics stay in-process only, and logs stay
569573on stdout only.
570574
infra/relay/src/observability.test.ts+9
......@@ -62,6 +62,11 @@ it.effect("exports schema error fields as span attributes", () =>
6262
6363 const request = yield* Deferred.await(exportedRequest).pipe(Effect.timeout("1 second"));
6464 const payload = (yield* decodeJson(request.body)) as OtlpTracer.TraceData;
65 const resourceAttributes = Object.fromEntries(
66 payload.resourceSpans
67 .flatMap((resourceSpan) => resourceSpan.resource.attributes)
68 .map((attribute) => [attribute.key, otlpAttributeValue(attribute.value)]),
69 );
6570 const span = payload.resourceSpans
6671 .flatMap((resourceSpan) => resourceSpan.scopeSpans)
6772 .flatMap((scopeSpan) => scopeSpan.spans)
......@@ -75,6 +80,10 @@ it.effect("exports schema error fields as span attributes", () =>
7580
7681 expect(request.authorization).toBe("Bearer test-token");
7782 expect(request.dataset).toBe("relay-test-traces");
83 expect(resourceAttributes).toMatchObject({
84 "service.name": "t3code-relay",
85 "service.namespace": "t3code",
86 });
7887 expect(attributes).toMatchObject({
7988 "error.type": "EnvironmentConnectNotAuthorized",
8089 "error.environmentId": "environment-1",
infra/relay/src/observability.ts+2-1
......@@ -222,8 +222,9 @@ export const makeRelayTraceLayer = (input: {
222222 OtlpTracer.make({
223223 url: input.tracesEndpoint,
224224 resource: {
225 serviceName: "t3-code-relay-worker",
225 serviceName: "t3code-relay",
226226 attributes: {
227 "service.namespace": "t3code",
227228 "service.runtime": "cloudflare-worker",
228229 "service.component": "relay",
229230 },
packages/shared/src/relayTracing.test.ts+2
......@@ -97,6 +97,8 @@ describe("withRelayClientTracing", () => {
9797 const payload = new TextDecoder().decode(fetchFn.mock.calls[0]?.[1]?.body as Uint8Array);
9898 expect(payload).toContain("relay request failed");
9999 expect(payload).toContain("relay socket closed");
100 expect(payload).toContain('"key":"service.name","value":{"stringValue":"relay-test"}');
101 expect(payload).toContain('"key":"service.namespace","value":{"stringValue":"t3code"}');
100102 }),
101103 ),
102104 );
packages/shared/src/relayTracing.ts+1
......@@ -142,6 +142,7 @@ export function makeRelayClientTracingLayer(
142142 serviceName: resource.serviceName,
143143 serviceVersion: resource.serviceVersion,
144144 attributes: {
145 "service.namespace": "t3code",
145146 "service.runtime": resource.runtime,
146147 "service.component": resource.component ?? "relay-client",
147148 "t3.client.surface": resource.client,