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", () => {...@@ -477,6 +477,49 @@ describe("DesktopObservability", () => {
477 );477 );
478 });478 });
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
480 it.effect("exports nothing to Settings for logs an unusable OTEL endpoint claimed", () => {523 it.effect("exports nothing to Settings for logs an unusable OTEL endpoint claimed", () => {
481 const requests: Array<ExportedRequest> = [];524 const requests: Array<ExportedRequest> = [];
482 return Effect.gen(function* () {525 return Effect.gen(function* () {
apps/desktop/src/app/DesktopObservability.ts+2-1
...@@ -626,8 +626,9 @@ const telemetryLayer = Layer.unwrap(...@@ -626,8 +626,9 @@ const telemetryLayer = Layer.unwrap(
626 const environment = yield* DesktopEnvironment.DesktopEnvironment;626 const environment = yield* DesktopEnvironment.DesktopEnvironment;
627 const endpoints = yield* resolveOtlpEndpoints;627 const endpoints = yield* resolveOtlpEndpoints;
628 const resource = {628 const resource = {
629 serviceName: "desktop",629 serviceName: "t3code-desktop",
630 attributes: {630 attributes: {
631 "service.namespace": "t3code",
631 "service.runtime": "desktop",632 "service.runtime": "desktop",
632 "service.mode": environment.isDevelopment ? "development" : "packaged",633 "service.mode": environment.isDevelopment ? "development" : "packaged",
633 },634 },
apps/mobile/src/features/observability/tracing.ts+1-1
...@@ -25,7 +25,7 @@ export function resolveTracingConfig(): TracingConfig | null {...@@ -25,7 +25,7 @@ export function resolveTracingConfig(): TracingConfig | null {
2525
26export function makeTracingLayer(config: TracingConfig | null, resource: TracingResource) {26export function makeTracingLayer(config: TracingConfig | null, resource: TracingResource) {
27 return makeRelayClientTracingLayer(config, {27 return makeRelayClientTracingLayer(config, {
28 serviceName: "t3-mobile-relay-client",28 serviceName: "t3code-mobile",
29 serviceVersion: resource.serviceVersion,29 serviceVersion: resource.serviceVersion,
30 runtime: "react-native",30 runtime: "react-native",
31 client: `mobile-${resource.appVariant}`,31 client: `mobile-${resource.appVariant}`,
apps/server/src/bin.test.ts-1
...@@ -106,7 +106,6 @@ const makeCliTestServerConfig = (baseDir: string) =>...@@ -106,7 +106,6 @@ const makeCliTestServerConfig = (baseDir: string) =>
106 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,106 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,
107 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,107 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,
108 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,108 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,
109 otlpServiceName: "t3-server",
110 otelEnvironment: OtelEnvironment.none,109 otelEnvironment: OtelEnvironment.none,
111 mode: "web",110 mode: "web",
112 port: 0,111 port: 0,
apps/server/src/cli/config.test.ts-1
...@@ -56,7 +56,6 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {...@@ -56,7 +56,6 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
56 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,56 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,
57 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,57 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,
58 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,58 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,
59 otlpServiceName: "t3-server",
60 otelEnvironment: OtelEnvironment.none,59 otelEnvironment: OtelEnvironment.none,
61 devAllowedOrigins: [],60 devAllowedOrigins: [],
62 } as const;61 } as const;
apps/server/src/cli/config.ts-2
...@@ -108,7 +108,6 @@ const EnvServerConfig = Config.all({...@@ -108,7 +108,6 @@ const EnvServerConfig = Config.all({
108 otlpExportIntervalMs: Config.Int("T3CODE_OTLP_EXPORT_INTERVAL_MS").pipe(108 otlpExportIntervalMs: Config.Int("T3CODE_OTLP_EXPORT_INTERVAL_MS").pipe(
109 Config.withDefault(10_000),109 Config.withDefault(10_000),
110 ),110 ),
111 otlpServiceName: Config.String("T3CODE_OTLP_SERVICE_NAME").pipe(Config.withDefault("t3-server")),
112 otlpHeaders: Config.schema(OtlpHeadersFromString, "T3CODE_OTLP_HEADERS").pipe(111 otlpHeaders: Config.schema(OtlpHeadersFromString, "T3CODE_OTLP_HEADERS").pipe(
113 Config.option,112 Config.option,
114 Config.map(Option.getOrUndefined),113 Config.map(Option.getOrUndefined),
...@@ -431,7 +430,6 @@ export const resolveServerConfig = (...@@ -431,7 +430,6 @@ export const resolveServerConfig = (
431 otlpTracesExport: traces?.export ?? signalExport,430 otlpTracesExport: traces?.export ?? signalExport,
432 otlpMetricsExport: metrics?.export ?? signalExport,431 otlpMetricsExport: metrics?.export ?? signalExport,
433 otlpLogsExport: logs?.export ?? signalExport,432 otlpLogsExport: logs?.export ?? signalExport,
434 otlpServiceName: env.otlpServiceName,
435 otelEnvironment: otel,433 otelEnvironment: otel,
436 mode,434 mode,
437 port,435 port,
apps/server/src/cli/pair.ts-1
...@@ -326,7 +326,6 @@ const makePairServerConfig = Effect.fn(function* (input: {...@@ -326,7 +326,6 @@ const makePairServerConfig = Effect.fn(function* (input: {
326 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,326 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,
327 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,327 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,
328 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,328 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,
329 otlpServiceName: "t3-server",
330 otelEnvironment: OtelEnvironment.none,329 otelEnvironment: OtelEnvironment.none,
331 mode: "web",330 mode: "web",
332 port: state.port,331 port: state.port,
apps/server/src/cloud/relayTracing.ts+2-2
...@@ -7,14 +7,14 @@ const relayClientTracingConfig = resolveRelayClientTracingConfig();...@@ -7,14 +7,14 @@ const relayClientTracingConfig = resolveRelayClientTracingConfig();
7export const headlessRelayClientTracingLayer = makeRelayClientTracingLayer(7export const headlessRelayClientTracingLayer = makeRelayClientTracingLayer(
8 relayClientTracingConfig,8 relayClientTracingConfig,
9 {9 {
10 serviceName: "t3-headless-relay-client",10 serviceName: "t3code-server",
11 runtime: "node",11 runtime: "node",
12 client: "headless-cli",12 client: "headless-cli",
13 },13 },
14);14);
1515
16export const serverRelayBrokerTracingLayer = makeRelayClientTracingLayer(relayClientTracingConfig, {16export const serverRelayBrokerTracingLayer = makeRelayClientTracingLayer(relayClientTracingConfig, {
17 serviceName: "t3-server",17 serviceName: "t3code-server",
18 runtime: "node",18 runtime: "node",
19 client: "environment-server",19 client: "environment-server",
20 component: "relay-broker",20 component: "relay-broker",
apps/server/src/config.ts+2-3
...@@ -82,7 +82,6 @@ export class ServerConfig extends Context.Service<...@@ -82,7 +82,6 @@ export class ServerConfig extends Context.Service<
82 readonly otlpTracesExport: SignalExport;82 readonly otlpTracesExport: SignalExport;
83 readonly otlpMetricsExport: SignalExport;83 readonly otlpMetricsExport: SignalExport;
84 readonly otlpLogsExport: SignalExport;84 readonly otlpLogsExport: SignalExport;
85 readonly otlpServiceName: string;
86 readonly otelEnvironment: OtelEnvironment.OtelEnvironment;85 readonly otelEnvironment: OtelEnvironment.OtelEnvironment;
87 readonly mode: RuntimeMode;86 readonly mode: RuntimeMode;
88 readonly port: number;87 readonly port: number;
...@@ -119,8 +118,9 @@ export const make = (config: ServerConfig["Service"]) => ServerConfig.of(config)...@@ -119,8 +118,9 @@ export const make = (config: ServerConfig["Service"]) => ServerConfig.of(config)
119 * logs report the same service identity to the collector.118 * logs report the same service identity to the collector.
120 */119 */
121export const otlpResource = (config: ServerConfig["Service"]) => ({120export const otlpResource = (config: ServerConfig["Service"]) => ({
122 serviceName: config.otlpServiceName,121 serviceName: "t3code-server",
123 attributes: {122 attributes: {
123 "service.namespace": "t3code",
124 "service.runtime": "t3-server",124 "service.runtime": "t3-server",
125 "service.mode": config.mode,125 "service.mode": config.mode,
126 },126 },
...@@ -222,7 +222,6 @@ const makeTest = Effect.fn("ServerConfig.makeTest")(function* (...@@ -222,7 +222,6 @@ const makeTest = Effect.fn("ServerConfig.makeTest")(function* (
222 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,222 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,
223 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,223 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,
224 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,224 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,
225 otlpServiceName: "t3-server",
226 otelEnvironment: OtelEnvironment.none,225 otelEnvironment: OtelEnvironment.none,
227 cwd,226 cwd,
228 baseDir,227 baseDir,
apps/server/src/environment/ServerEnvironment.test.ts-1
...@@ -59,7 +59,6 @@ const makeServerConfig = Effect.fn(function* (baseDir: string) {...@@ -59,7 +59,6 @@ const makeServerConfig = Effect.fn(function* (baseDir: string) {
59 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,59 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,
60 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,60 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,
61 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,61 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,
62 otlpServiceName: "t3-server",
63 otelEnvironment: OtelEnvironment.none,62 otelEnvironment: OtelEnvironment.none,
64 cwd: process.cwd(),63 cwd: process.cwd(),
65 baseDir,64 baseDir,
apps/server/src/persistence/Layers/Sqlite.ts+1-1
...@@ -32,7 +32,7 @@ export const makeSqlitePersistenceLive = Effect.fn("makeSqlitePersistenceLive")(...@@ -32,7 +32,7 @@ export const makeSqlitePersistenceLive = Effect.fn("makeSqlitePersistenceLive")(
32 filename: dbPath,32 filename: dbPath,
33 spanAttributes: {33 spanAttributes: {
34 "db.name": path.basename(dbPath),34 "db.name": path.basename(dbPath),
35 "service.name": "t3-server",35 "service.name": "t3code-server",
36 },36 },
37 }),37 }),
38 );38 );
apps/server/src/server.test.ts+5-6
...@@ -489,7 +489,7 @@ const makeBrowserOtlpPayload = (spanName: string) =>...@@ -489,7 +489,7 @@ const makeBrowserOtlpPayload = (spanName: string) =>
489 url: collector.url,489 url: collector.url,
490 exportInterval: "10 millis",490 exportInterval: "10 millis",
491 resource: {491 resource: {
492 serviceName: "t3-web",492 serviceName: "t3code-web",
493 attributes: {493 attributes: {
494 "service.runtime": "t3-web",494 "service.runtime": "t3-web",
495 "service.mode": "browser",495 "service.mode": "browser",
...@@ -588,7 +588,6 @@ const buildAppUnderTest = (options?: {...@@ -588,7 +588,6 @@ const buildAppUnderTest = (options?: {
588 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,588 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,
589 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,589 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,
590 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,590 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,
591 otlpServiceName: "t3-server",
592 otelEnvironment: OtelEnvironment.none,591 otelEnvironment: OtelEnvironment.none,
593 mode: "desktop",592 mode: "desktop",
594 port: 0,593 port: 0,
...@@ -5283,7 +5282,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -5283,7 +5282,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
5283 attributes: [5282 attributes: [
5284 {5283 {
5285 key: "service.name",5284 key: "service.name",
5286 value: { stringValue: "t3-web" },5285 value: { stringValue: "t3code-web" },
5287 },5286 },
5288 ],5287 ],
5289 },5288 },
...@@ -5425,7 +5424,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -5425,7 +5424,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
5425 "rpc.method": "server.getSettings",5424 "rpc.method": "server.getSettings",
5426 },5425 },
5427 resourceAttributes: {5426 resourceAttributes: {
5428 "service.name": "t3-web",5427 "service.name": "t3code-web",
5429 },5428 },
5430 scope: {5429 scope: {
5431 name: "effect",5430 name: "effect",
...@@ -5556,7 +5555,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -5556,7 +5555,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
5556 // the stub's utf8 decode even though the surrounding bytes don't.5555 // the stub's utf8 decode even though the surrounding bytes don't.
5557 assert.notEqual(forwarded.body[0], "{");5556 assert.notEqual(forwarded.body[0], "{");
5558 assert.include(forwarded.body, "client.protobuf.test");5557 assert.include(forwarded.body, "client.protobuf.test");
5559 assert.include(forwarded.body, "t3-web");5558 assert.include(forwarded.body, "t3code-web");
5560 }).pipe(Effect.provide(NodeHttpServer.layerTest)),5559 }).pipe(Effect.provide(NodeHttpServer.layerTest)),
5561 );5560 );
55625561
...@@ -5657,7 +5656,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -5657,7 +5656,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
5657 assert.deepEqual(record.links, []);5656 assert.deepEqual(record.links, []);
5658 assert.equal(record.scope.name, scopeSpan.scope.name);5657 assert.equal(record.scope.name, scopeSpan.scope.name);
5659 assert.deepEqual(record.scope.attributes, {});5658 assert.deepEqual(record.scope.attributes, {});
5660 assert.equal(record.resourceAttributes["service.name"], "t3-web");5659 assert.equal(record.resourceAttributes["service.name"], "t3code-web");
5661 assert.equal(record.status?.code, String(span.status.code));5660 assert.equal(record.status?.code, String(span.status.code));
5662 }).pipe(Effect.provide(NodeHttpServer.layerTest)),5661 }).pipe(Effect.provide(NodeHttpServer.layerTest)),
5663 );5662 );
apps/server/src/serverLogger.test.ts+29-2
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1import * as NodePath from "@effect/platform-node/NodePath";1import * as NodePath from "@effect/platform-node/NodePath";
2import { assert, describe, it } from "@effect/vitest";2import { assert, describe, it } from "@effect/vitest";
3import * as NodeOS from "node:os";3import * as NodeOS from "node:os";
4import * as ConfigProvider from "effect/ConfigProvider";
4import * as Effect from "effect/Effect";5import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";6import * as Layer from "effect/Layer";
6import * as Path from "effect/Path";7import * as Path from "effect/Path";
...@@ -57,7 +58,6 @@ const configLayer = (overrides: Partial<ServerConfig.ServerConfig["Service"]>) =...@@ -57,7 +58,6 @@ const configLayer = (overrides: Partial<ServerConfig.ServerConfig["Service"]>) =
57 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,58 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,
58 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,59 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,
59 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,60 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,
60 otlpServiceName: "t3-server",
61 otelEnvironment: OtelEnvironment.none,61 otelEnvironment: OtelEnvironment.none,
62 cwd: baseDir,62 cwd: baseDir,
63 baseDir,63 baseDir,
...@@ -146,11 +146,38 @@ describe("ServerLoggerLive", () => {...@@ -146,11 +146,38 @@ describe("ServerLoggerLive", () => {
146 const [request] = requests;146 const [request] = requests;
147 assert.strictEqual(request?.url, "https://collector.example.com/v1/logs");147 assert.strictEqual(request?.url, "https://collector.example.com/v1/logs");
148 assert.include(request?.body ?? "", "server logger under test");148 assert.include(request?.body ?? "", "server logger under test");
149 assert.include(request?.body ?? "", "t3-server");149 assert.include(request?.body ?? "", "t3code-server");
150 assert.include(request?.body ?? "", "service.runtime");150 assert.include(request?.body ?? "", "service.runtime");
151 }),151 }),
152 );152 );
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
154 it.effect("stays off the network when no logs endpoint is configured", () =>181 it.effect("stays off the network when no logs endpoint is configured", () =>
155 Effect.gen(function* () {182 Effect.gen(function* () {
156 const requests = yield* logThrough({});183 const requests = yield* logThrough({});
apps/web/src/lib/runtime.ts+1-1
...@@ -19,7 +19,7 @@ function configuredRelayUrl(): string {...@@ -19,7 +19,7 @@ function configuredRelayUrl(): string {
1919
20const httpClientLayer = remoteHttpClientLayer((input, init) => globalThis.fetch(input, init));20const httpClientLayer = remoteHttpClientLayer((input, init) => globalThis.fetch(input, init));
21const relayTracingLayer = makeRelayClientTracingLayer(resolveRelayTracingConfig(), {21const relayTracingLayer = makeRelayClientTracingLayer(resolveRelayTracingConfig(), {
22 serviceName: "t3-web-relay-client",22 serviceName: "t3code-web",
23 serviceVersion: import.meta.env.APP_VERSION,23 serviceVersion: import.meta.env.APP_VERSION,
24 runtime: "browser",24 runtime: "browser",
25 client: typeof window !== "undefined" && window.desktopBridge ? "desktop" : "web",25 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";...@@ -15,8 +15,9 @@ import { APP_VERSION } from "~/branding";
1515
16const DEFAULT_EXPORT_INTERVAL_MS = 1_000;16const DEFAULT_EXPORT_INTERVAL_MS = 1_000;
17const CLIENT_TRACING_RESOURCE = {17const CLIENT_TRACING_RESOURCE = {
18 serviceName: "t3-web",18 serviceName: "t3code-web",
19 attributes: {19 attributes: {
20 "service.namespace": "t3code",
20 "service.runtime": "t3-web",21 "service.runtime": "t3-web",
21 "service.mode": isElectron ? "electron" : "browser",22 "service.mode": isElectron ? "electron" : "browser",
22 "service.version": APP_VERSION,23 "service.version": APP_VERSION,
docs/operations/observability.md+14-10
...@@ -129,7 +129,7 @@ Default Grafana login:...@@ -129,7 +129,7 @@ Default Grafana login:
129export T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces129export T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces
130export T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics130export T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics
131export T3CODE_OTLP_LOGS_URL=http://localhost:4318/v1/logs131export T3CODE_OTLP_LOGS_URL=http://localhost:4318/v1/logs
132export T3CODE_OTLP_SERVICE_NAME=t3-local132export OTEL_RESOURCE_ATTRIBUTES=deployment.environment.name=development
133```133```
134134
135Optional:135Optional:
...@@ -169,7 +169,6 @@ macOS app bundle example:...@@ -169,7 +169,6 @@ macOS app bundle example:
169T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces \169T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces \
170T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics \170T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics \
171T3CODE_OTLP_LOGS_URL=http://localhost:4318/v1/logs \171T3CODE_OTLP_LOGS_URL=http://localhost:4318/v1/logs \
172T3CODE_OTLP_SERVICE_NAME=t3-desktop \
173"/Applications/T3 Code.app/Contents/MacOS/T3 Code"172"/Applications/T3 Code.app/Contents/MacOS/T3 Code"
174```173```
175174
...@@ -179,7 +178,6 @@ Direct binary example:...@@ -179,7 +178,6 @@ Direct binary example:
179T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces \178T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces \
180T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics \179T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics \
181T3CODE_OTLP_LOGS_URL=http://localhost:4318/v1/logs \180T3CODE_OTLP_LOGS_URL=http://localhost:4318/v1/logs \
182T3CODE_OTLP_SERVICE_NAME=t3-desktop \
183./path/to/your/desktop-app-binary181./path/to/your/desktop-app-binary
184```182```
185183
...@@ -311,11 +309,13 @@ Recommended flow in Grafana:...@@ -311,11 +309,13 @@ Recommended flow in Grafana:
3112. Pick the `Tempo` data source.3092. Pick the `Tempo` data source.
3123. Set the time range to something recent like `Last 15 minutes`.3103. Set the time range to something recent like `Last 15 minutes`.
3134. Start broad. Do not begin with a very narrow query.3114. 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
316Good first searches:315Good 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`
319- span names like `sendTurn` or a Git operation such as `GitVcsDriver.statusDetails.status`319- span names like `sendTurn` or a Git operation such as `GitVcsDriver.statusDetails.status`
320- Git spans whose `git.operation` attribute identifies the operation320- Git spans whose `git.operation` attribute identifies the operation
321- orchestration spans with attributes like `orchestration.command_type`321- orchestration spans with attributes like `orchestration.command_type`
...@@ -525,10 +525,10 @@ It provides:...@@ -525,10 +525,10 @@ It provides:
525The desktop main process is a second producer, assembled in525The desktop main process is a second producer, assembled in
526`apps/desktop/src/app/DesktopObservability.ts`. It reads the same `T3CODE_OTLP_*` names and the same526`apps/desktop/src/app/DesktopObservability.ts`. It reads the same `T3CODE_OTLP_*` names and the same
527Settings entries as the backend it supervises, and covers work the backend cannot see: app startup,527Settings 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`528window and menu handling, backend supervision, and updates. It reports as service
529regardless of `T3CODE_OTLP_SERVICE_NAME`, so a collector shows it alongside the backend rather than529`t3code-desktop`, so a collector shows it alongside the backend rather than mixed into it. It
530mixed into it. It exports traces and logs only; the main process records no metrics, so the metrics530exports traces and logs only; the main process records no metrics, so the metrics endpoint applies
531endpoint applies to the backend alone.531to the backend alone.
532532
533### Env Vars533### Env Vars
534534
...@@ -547,7 +547,6 @@ OTLP export:...@@ -547,7 +547,6 @@ OTLP export:
547- `T3CODE_OTLP_METRICS_URL`: OTLP metric endpoint547- `T3CODE_OTLP_METRICS_URL`: OTLP metric endpoint
548- `T3CODE_OTLP_LOGS_URL`: OTLP log endpoint548- `T3CODE_OTLP_LOGS_URL`: OTLP log endpoint
549- `T3CODE_OTLP_EXPORT_INTERVAL_MS`: export interval, default `10000`549- `T3CODE_OTLP_EXPORT_INTERVAL_MS`: export interval, default `10000`
550- `T3CODE_OTLP_SERVICE_NAME`: service name, default `t3-server`
551- `T3CODE_OTLP_HEADERS`: extra headers for all three exporters, same format as550- `T3CODE_OTLP_HEADERS`: extra headers for all three exporters, same format as
552 `OTEL_EXPORTER_OTLP_HEADERS`: comma-separated `key=value` pairs with percent-encoded values.551 `OTEL_EXPORTER_OTLP_HEADERS`: comma-separated `key=value` pairs with percent-encoded values.
553- `T3CODE_OTLP_PROTOCOL`: `http/json` (default) or `http/protobuf`552- `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...@@ -565,6 +564,11 @@ an `http` or `https` URL, a protocol other than `http/protobuf` or `http/json` s
565headers that are not `key=value` pairs with percent-encoded values turn that signal's export off564headers that are not `key=value` pairs with percent-encoded values turn that signal's export off
566with a startup warning, rather than sending it to the Settings endpoint.565with 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
568If the OTLP URLs are unset, local tracing still works, metrics stay in-process only, and logs stay572If the OTLP URLs are unset, local tracing still works, metrics stay in-process only, and logs stay
569on stdout only.573on stdout only.
570574
infra/relay/src/observability.test.ts+9
...@@ -62,6 +62,11 @@ it.effect("exports schema error fields as span attributes", () =>...@@ -62,6 +62,11 @@ it.effect("exports schema error fields as span attributes", () =>
6262
63 const request = yield* Deferred.await(exportedRequest).pipe(Effect.timeout("1 second"));63 const request = yield* Deferred.await(exportedRequest).pipe(Effect.timeout("1 second"));
64 const payload = (yield* decodeJson(request.body)) as OtlpTracer.TraceData;64 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 );
65 const span = payload.resourceSpans70 const span = payload.resourceSpans
66 .flatMap((resourceSpan) => resourceSpan.scopeSpans)71 .flatMap((resourceSpan) => resourceSpan.scopeSpans)
67 .flatMap((scopeSpan) => scopeSpan.spans)72 .flatMap((scopeSpan) => scopeSpan.spans)
...@@ -75,6 +80,10 @@ it.effect("exports schema error fields as span attributes", () =>...@@ -75,6 +80,10 @@ it.effect("exports schema error fields as span attributes", () =>
7580
76 expect(request.authorization).toBe("Bearer test-token");81 expect(request.authorization).toBe("Bearer test-token");
77 expect(request.dataset).toBe("relay-test-traces");82 expect(request.dataset).toBe("relay-test-traces");
83 expect(resourceAttributes).toMatchObject({
84 "service.name": "t3code-relay",
85 "service.namespace": "t3code",
86 });
78 expect(attributes).toMatchObject({87 expect(attributes).toMatchObject({
79 "error.type": "EnvironmentConnectNotAuthorized",88 "error.type": "EnvironmentConnectNotAuthorized",
80 "error.environmentId": "environment-1",89 "error.environmentId": "environment-1",
infra/relay/src/observability.ts+2-1
...@@ -222,8 +222,9 @@ export const makeRelayTraceLayer = (input: {...@@ -222,8 +222,9 @@ export const makeRelayTraceLayer = (input: {
222 OtlpTracer.make({222 OtlpTracer.make({
223 url: input.tracesEndpoint,223 url: input.tracesEndpoint,
224 resource: {224 resource: {
225 serviceName: "t3-code-relay-worker",225 serviceName: "t3code-relay",
226 attributes: {226 attributes: {
227 "service.namespace": "t3code",
227 "service.runtime": "cloudflare-worker",228 "service.runtime": "cloudflare-worker",
228 "service.component": "relay",229 "service.component": "relay",
229 },230 },
packages/shared/src/relayTracing.test.ts+2
...@@ -97,6 +97,8 @@ describe("withRelayClientTracing", () => {...@@ -97,6 +97,8 @@ describe("withRelayClientTracing", () => {
97 const payload = new TextDecoder().decode(fetchFn.mock.calls[0]?.[1]?.body as Uint8Array);97 const payload = new TextDecoder().decode(fetchFn.mock.calls[0]?.[1]?.body as Uint8Array);
98 expect(payload).toContain("relay request failed");98 expect(payload).toContain("relay request failed");
99 expect(payload).toContain("relay socket closed");99 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"}');
100 }),102 }),
101 ),103 ),
102 );104 );
packages/shared/src/relayTracing.ts+1
...@@ -142,6 +142,7 @@ export function makeRelayClientTracingLayer(...@@ -142,6 +142,7 @@ export function makeRelayClientTracingLayer(
142 serviceName: resource.serviceName,142 serviceName: resource.serviceName,
143 serviceVersion: resource.serviceVersion,143 serviceVersion: resource.serviceVersion,
144 attributes: {144 attributes: {
145 "service.namespace": "t3code",
145 "service.runtime": resource.runtime,146 "service.runtime": resource.runtime,
146 "service.component": resource.component ?? "relay-client",147 "service.component": resource.component ?? "relay-client",
147 "t3.client.surface": resource.client,148 "t3.client.surface": resource.client,