Skip to content

Commit fdfeaa4

Browse files
authored
fix(observability): resource attributes follow OTel semantic conventions (#68)
2 parents 78af4c4 + 7dfbc2b commit fdfeaa4

18 files changed

Lines changed: 251 additions & 30 deletions

File tree

‎apps/desktop/src/app/DesktopObservability.test.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,8 @@ describe("DesktopObservability", () => {
412412
const [request] = requests;
413413
assert.strictEqual(request?.url, "https://collector.example.com/v1/logs");
414414
assert.include(request?.body ?? "", "desktop log export");
415-
assert.include(request?.body ?? "", "service.runtime");
415+
assert.include(request?.body ?? "", "deployment.environment.name");
416+
assert.include(request?.body ?? "", "process.runtime.name");
416417
assert.strictEqual(request?.headers["x-scope"], "desktop");
417418

418419
// The log record is the export now, so the same message must not also
@@ -497,7 +498,7 @@ describe("DesktopObservability", () => {
497498
assert.lengthOf(requests, 1);
498499
const body = requests[0]?.body ?? "";
499500
assert.include(body, '"stringValue":"t3code-desktop"');
500-
assert.include(body, "deployment.environment.name");
501+
assert.include(body, '"key":"deployment.environment.name","value":{"stringValue":"staging"}');
501502
assert.include(body, '"key":"service.namespace","value":{"stringValue":"t3code"}');
502503
assert.notInclude(body, "renamed");
503504
}).pipe(
@@ -511,7 +512,7 @@ describe("DesktopObservability", () => {
511512
env: {
512513
OTEL_SERVICE_NAME: "renamed",
513514
OTEL_RESOURCE_ATTRIBUTES:
514-
"service.name=renamed,service.namespace=renamed,deployment.environment.name=development",
515+
"service.name=renamed,service.namespace=renamed,deployment.environment.name=staging",
515516
},
516517
}),
517518
),

‎apps/desktop/src/app/DesktopObservability.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { PRIMARY_LOCAL_ENVIRONMENT_ID } from "@t3tools/contracts";
22
import {
33
makeLocalFileTracer,
44
makeTraceSink,
5+
nodeProcessResourceAttributes,
56
otlpSerializationLayer,
67
type SignalExport,
78
} from "@t3tools/shared/observability";
@@ -627,10 +628,13 @@ const telemetryLayer = Layer.unwrap(
627628
const endpoints = yield* resolveOtlpEndpoints;
628629
const resource = {
629630
serviceName: "t3code-desktop",
631+
serviceVersion: environment.appVersion,
630632
attributes: {
631633
"service.namespace": "t3code",
632-
"service.runtime": "desktop",
633-
"service.mode": environment.isDevelopment ? "development" : "packaged",
634+
...nodeProcessResourceAttributes({
635+
operatorAttributes: endpoints.resourceAttributes,
636+
isDevelopment: environment.isDevelopment,
637+
}),
634638
},
635639
};
636640

‎apps/mobile/src/features/observability/tracing.test.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ it.effect("exports spans through the scoped mobile OTLP layer", () => {
2727
},
2828
{
2929
appVariant: "test",
30+
serviceInstanceId: "test-instance",
3031
serviceVersion: "1.2.3",
3132
},
3233
).pipe(Layer.provide(remoteHttpClientLayer(fetchFn)));
@@ -47,7 +48,15 @@ it.effect("exports spans through the scoped mobile OTLP layer", () => {
4748
expect(String(url)).toBe("https://api.axiom.test/v1/traces");
4849
expect(new Headers(init?.headers).get("authorization")).toBe("Bearer public-ingest-token");
4950
expect(new Headers(init?.headers).get("x-axiom-dataset")).toBe("mobile-traces");
50-
expect(new TextDecoder().decode(init?.body as Uint8Array)).toContain("mobile.test.span");
51+
const body = new TextDecoder().decode(init?.body as Uint8Array);
52+
expect(body).toContain("mobile.test.span");
53+
expect(body).toContain('"key":"t3code.client.surface","value":{"stringValue":"mobile"}');
54+
expect(body).toContain(
55+
'"key":"deployment.environment.name","value":{"stringValue":"test"}',
56+
);
57+
expect(body).toContain(
58+
'"key":"service.instance.id","value":{"stringValue":"test-instance"}',
59+
);
5160
}),
5261
),
5362
);
@@ -63,6 +72,7 @@ it.effect("does not let OTLP serialization failures alter application effects",
6372
},
6473
{
6574
appVariant: "test",
75+
serviceInstanceId: "test-instance",
6676
serviceVersion: "1.2.3",
6777
},
6878
).pipe(Layer.provide(remoteHttpClientLayer(fetchFn)));

‎apps/mobile/src/features/observability/tracing.ts‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface TracingConfig {
1111

1212
export interface TracingResource {
1313
readonly serviceVersion?: string;
14+
readonly serviceInstanceId: string;
1415
readonly appVariant: string;
1516
}
1617

@@ -27,13 +28,20 @@ export function makeTracingLayer(config: TracingConfig | null, resource: Tracing
2728
return makeRelayClientTracingLayer(config, {
2829
serviceName: "t3code-mobile",
2930
serviceVersion: resource.serviceVersion,
30-
runtime: "react-native",
31-
client: `mobile-${resource.appVariant}`,
31+
serviceInstanceId: resource.serviceInstanceId,
32+
client: "mobile",
33+
attributes: {
34+
"process.runtime.name": "react-native",
35+
...(resource.appVariant !== "unknown" && {
36+
"deployment.environment.name": resource.appVariant,
37+
}),
38+
},
3239
});
3340
}
3441

3542
export const tracingLayer = makeTracingLayer(resolveTracingConfig(), {
3643
serviceVersion: Constants.expoConfig?.version,
44+
serviceInstanceId: Constants.sessionId,
3745
appVariant:
3846
typeof Constants.expoConfig?.extra?.appVariant === "string"
3947
? Constants.expoConfig.extra.appVariant

‎apps/server/src/cloud/relayTracing.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import {
2+
nodeProcessRuntimeAttributes,
3+
processServiceInstanceId,
4+
} from "@t3tools/shared/observability";
15
import { makeRelayClientTracingLayer } from "@t3tools/shared/relayTracing";
26

37
import { resolveRelayClientTracingConfig } from "./publicConfig.ts";
@@ -8,14 +12,16 @@ export const headlessRelayClientTracingLayer = makeRelayClientTracingLayer(
812
relayClientTracingConfig,
913
{
1014
serviceName: "t3code-server",
11-
runtime: "node",
15+
serviceInstanceId: processServiceInstanceId(),
16+
attributes: nodeProcessRuntimeAttributes(),
1217
client: "headless-cli",
1318
},
1419
);
1520

1621
export const serverRelayBrokerTracingLayer = makeRelayClientTracingLayer(relayClientTracingConfig, {
1722
serviceName: "t3code-server",
18-
runtime: "node",
23+
serviceInstanceId: processServiceInstanceId(),
24+
attributes: nodeProcessRuntimeAttributes(),
1925
client: "environment-server",
2026
component: "relay-broker",
2127
});

‎apps/server/src/config.ts‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,13 @@ import * as Path from "effect/Path";
1616
import type * as Redacted from "effect/Redacted";
1717
import * as Schema from "effect/Schema";
1818

19+
import packageJson from "../package.json" with { type: "json" };
1920
import { sweepStalePendingAttachments } from "./attachmentStore.ts";
20-
import { DEFAULT_SIGNAL_EXPORT, type SignalExport } from "@t3tools/shared/observability";
21+
import {
22+
DEFAULT_SIGNAL_EXPORT,
23+
nodeProcessResourceAttributes,
24+
type SignalExport,
25+
} from "@t3tools/shared/observability";
2126
import * as OtelEnvironment from "@t3tools/shared/otelEnvironment";
2227

2328
export const DEFAULT_PORT = 3773;
@@ -118,10 +123,14 @@ export const make = (config: ServerConfig["Service"]) => ServerConfig.of(config)
118123
*/
119124
export const otlpResource = (config: ServerConfig["Service"]) => ({
120125
serviceName: "t3code-server",
126+
serviceVersion: packageJson.version,
121127
attributes: {
122128
"service.namespace": "t3code",
123-
"service.runtime": "t3-server",
124-
"service.mode": config.mode,
129+
"t3code.server.managed_by": config.mode === "desktop" ? "desktop" : "standalone",
130+
...nodeProcessResourceAttributes({
131+
operatorAttributes: config.otelEnvironment.resourceAttributes,
132+
isDevelopment: config.devUrl !== undefined,
133+
}),
125134
},
126135
});
127136

‎apps/server/src/serverLogger.test.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,9 @@ describe("ServerLoggerLive", () => {
147147
assert.strictEqual(request?.url, "https://collector.example.com/v1/logs");
148148
assert.include(request?.body ?? "", "server logger under test");
149149
assert.include(request?.body ?? "", "t3code-server");
150-
assert.include(request?.body ?? "", "service.runtime");
150+
assert.include(request?.body ?? "", "t3code.server.managed_by");
151+
assert.include(request?.body ?? "", "process.runtime.name");
152+
assert.include(request?.body ?? "", "service.instance.id");
151153
}),
152154
);
153155

‎apps/web/src/lib/runtime.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { browserCryptoLayer } from "../cloud/dpop";
1212
import { managedRelayClientLayer } from "../cloud/managedRelayLayer";
1313
import { resolveCloudPublicConfig, resolveRelayTracingConfig } from "../cloud/publicConfig";
1414
import * as ClientTracer from "../observability/clientTracer";
15+
import { serviceInstanceId } from "../observability/serviceInstance";
1516

1617
function configuredRelayUrl(): string {
1718
return resolveCloudPublicConfig().relayUrl ?? "http://relay.invalid";
@@ -21,7 +22,8 @@ const httpClientLayer = remoteHttpClientLayer((input, init) => globalThis.fetch(
2122
const relayTracingLayer = makeRelayClientTracingLayer(resolveRelayTracingConfig(), {
2223
serviceName: "t3code-web",
2324
serviceVersion: import.meta.env.APP_VERSION,
24-
runtime: "browser",
25+
serviceInstanceId,
26+
attributes: { "process.runtime.name": "browser" },
2527
client: typeof window !== "undefined" && window.desktopBridge ? "desktop" : "web",
2628
}).pipe(Layer.provide(httpClientLayer));
2729

‎apps/web/src/observability/clientTracing.ts‎

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,51 @@ import { settleAsyncResult, squashAtomCommandFailure } from "@t3tools/client-run
99
import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors";
1010
import { resolvePrimaryEnvironmentHttpUrl } from "../environments/primary";
1111
import * as ClientTracer from "./clientTracer";
12+
import { serviceInstanceId } from "./serviceInstance";
1213
import { primaryEnvironmentHttpLayer } from "../environments/primary/httpLayer";
1314
import { isElectron } from "../env";
1415
import { APP_VERSION } from "~/branding";
1516

1617
const DEFAULT_EXPORT_INTERVAL_MS = 1_000;
18+
interface NavigatorUserAgentData {
19+
readonly platform: string;
20+
readonly mobile: boolean;
21+
readonly brands: ReadonlyArray<{ readonly brand: string; readonly version: string }>;
22+
}
23+
24+
/**
25+
* The same `browser.*` and `user_agent.original` attributes the OpenTelemetry
26+
* browser resource detector reports, plus the `process.runtime.*` values the
27+
* semantic conventions give a web browser. `userAgentData` exists only in
28+
* Chromium, which includes the desktop app.
29+
*/
30+
const browserResourceAttributes = (): Record<string, unknown> => {
31+
if (typeof navigator === "undefined") return {};
32+
const userAgentData = (navigator as Navigator & { userAgentData?: NavigatorUserAgentData })
33+
.userAgentData;
34+
return {
35+
"user_agent.original": navigator.userAgent,
36+
"process.runtime.name": "browser",
37+
"process.runtime.version": navigator.userAgent,
38+
"browser.language": navigator.language,
39+
...(userAgentData && {
40+
"browser.platform": userAgentData.platform,
41+
"browser.mobile": userAgentData.mobile,
42+
"browser.brands": userAgentData.brands.map(({ brand, version }) => `${brand} ${version}`),
43+
}),
44+
};
45+
};
46+
1747
const CLIENT_TRACING_RESOURCE = {
1848
serviceName: "t3code-web",
49+
serviceVersion: APP_VERSION,
1950
attributes: {
2051
"service.namespace": "t3code",
21-
"service.runtime": "t3-web",
22-
"service.mode": isElectron ? "electron" : "browser",
23-
"service.version": APP_VERSION,
52+
"service.instance.id": serviceInstanceId,
53+
"t3code.client.surface": isElectron ? "desktop" : "web",
54+
...browserResourceAttributes(),
2455
},
25-
} as const;
56+
};
2657

2758
const delegateRuntimeLayer = Layer.mergeAll(
2859
primaryEnvironmentHttpLayer,
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { randomUUID } from "../lib/utils";
2+
3+
/** `service.instance.id` for this page load, shared by every resource the UI exports. */
4+
export const serviceInstanceId = randomUUID();

0 commit comments

Comments
 (0)