Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions apps/desktop/src/app/DesktopObservability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,8 @@ describe("DesktopObservability", () => {
const [request] = requests;
assert.strictEqual(request?.url, "https://collector.example.com/v1/logs");
assert.include(request?.body ?? "", "desktop log export");
assert.include(request?.body ?? "", "service.runtime");
assert.include(request?.body ?? "", "deployment.environment.name");
assert.include(request?.body ?? "", "process.runtime.name");
assert.strictEqual(request?.headers["x-scope"], "desktop");

// The log record is the export now, so the same message must not also
Expand Down Expand Up @@ -497,7 +498,7 @@ describe("DesktopObservability", () => {
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":"deployment.environment.name","value":{"stringValue":"staging"}');
assert.include(body, '"key":"service.namespace","value":{"stringValue":"t3code"}');
assert.notInclude(body, "renamed");
}).pipe(
Expand All @@ -511,7 +512,7 @@ describe("DesktopObservability", () => {
env: {
OTEL_SERVICE_NAME: "renamed",
OTEL_RESOURCE_ATTRIBUTES:
"service.name=renamed,service.namespace=renamed,deployment.environment.name=development",
"service.name=renamed,service.namespace=renamed,deployment.environment.name=staging",
},
}),
),
Expand Down
10 changes: 8 additions & 2 deletions apps/desktop/src/app/DesktopObservability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { PRIMARY_LOCAL_ENVIRONMENT_ID } from "@t3tools/contracts";
import {
makeLocalFileTracer,
makeTraceSink,
nodeProcessRuntimeAttributes,
otlpSerializationLayer,
type SignalExport,
} from "@t3tools/shared/observability";
Expand Down Expand Up @@ -627,10 +628,15 @@ const telemetryLayer = Layer.unwrap(
const endpoints = yield* resolveOtlpEndpoints;
const resource = {
serviceName: "t3code-desktop",
serviceVersion: environment.appVersion,
attributes: {
"service.namespace": "t3code",
"service.runtime": "desktop",
"service.mode": environment.isDevelopment ? "development" : "packaged",
// Effect lets explicit attributes beat `OTEL_RESOURCE_ATTRIBUTES`, so an
// operator's tier has to be carried over by hand to keep winning.
"deployment.environment.name":
endpoints.resourceAttributes["deployment.environment.name"] ??
(environment.isDevelopment ? "development" : "production"),
...nodeProcessRuntimeAttributes(),
},
};

Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/cloud/relayTracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ export const headlessRelayClientTracingLayer = makeRelayClientTracingLayer(
relayClientTracingConfig,
{
serviceName: "t3code-server",
runtime: "node",
runtime: "nodejs",
client: "headless-cli",
},
);

export const serverRelayBrokerTracingLayer = makeRelayClientTracingLayer(relayClientTracingConfig, {
serviceName: "t3code-server",
runtime: "node",
runtime: "nodejs",
client: "environment-server",
component: "relay-broker",
});
12 changes: 9 additions & 3 deletions apps/server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,13 @@ import * as Path from "effect/Path";
import type * as Redacted from "effect/Redacted";
import * as Schema from "effect/Schema";

import packageJson from "../package.json" with { type: "json" };
import { sweepStalePendingAttachments } from "./attachmentStore.ts";
import { DEFAULT_SIGNAL_EXPORT, type SignalExport } from "@t3tools/shared/observability";
import {
DEFAULT_SIGNAL_EXPORT,
nodeProcessRuntimeAttributes,
type SignalExport,
} from "@t3tools/shared/observability";
import * as OtelEnvironment from "@t3tools/shared/otelEnvironment";

export const DEFAULT_PORT = 3773;
Expand Down Expand Up @@ -118,10 +123,11 @@ export const make = (config: ServerConfig["Service"]) => ServerConfig.of(config)
*/
export const otlpResource = (config: ServerConfig["Service"]) => ({
serviceName: "t3code-server",
serviceVersion: packageJson.version,
attributes: {
"service.namespace": "t3code",
"service.runtime": "t3-server",
"service.mode": config.mode,
"t3.server.managed_by": config.mode === "desktop" ? "desktop" : "standalone",
...nodeProcessRuntimeAttributes(),
},
});

Expand Down
3 changes: 1 addition & 2 deletions apps/server/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,8 +496,7 @@ const makeBrowserOtlpPayload = (spanName: string) =>
resource: {
serviceName: "t3code-web",
attributes: {
"service.runtime": "t3-web",
"service.mode": "browser",
"t3.client.surface": "web",
"service.version": "test",
},
},
Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/serverLogger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ describe("ServerLoggerLive", () => {
assert.strictEqual(request?.url, "https://collector.example.com/v1/logs");
assert.include(request?.body ?? "", "server logger under test");
assert.include(request?.body ?? "", "t3code-server");
assert.include(request?.body ?? "", "service.runtime");
assert.include(request?.body ?? "", "t3.server.managed_by");
assert.include(request?.body ?? "", "process.runtime.name");
}),
);

Expand Down
34 changes: 30 additions & 4 deletions apps/web/src/observability/clientTracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,41 @@ import { isElectron } from "../env";
import { APP_VERSION } from "~/branding";

const DEFAULT_EXPORT_INTERVAL_MS = 1_000;
interface NavigatorUserAgentData {
readonly platform: string;
readonly mobile: boolean;
readonly brands: ReadonlyArray<{ readonly brand: string; readonly version: string }>;
}

/**
* The same `browser.*` and `user_agent.original` attributes the OpenTelemetry
* browser resource detector reports. `userAgentData` exists only in Chromium,
* which includes the desktop app.
*/
const browserResourceAttributes = (): Record<string, unknown> => {
if (typeof navigator === "undefined") return {};
const userAgentData = (navigator as Navigator & { userAgentData?: NavigatorUserAgentData })
.userAgentData;
return {
"user_agent.original": navigator.userAgent,
"browser.language": navigator.language,
...(userAgentData && {
"browser.platform": userAgentData.platform,
"browser.mobile": userAgentData.mobile,
"browser.brands": userAgentData.brands.map(({ brand, version }) => `${brand} ${version}`),
}),
};
};

const CLIENT_TRACING_RESOURCE = {
serviceName: "t3code-web",
serviceVersion: APP_VERSION,
attributes: {
"service.namespace": "t3code",
"service.runtime": "t3-web",
"service.mode": isElectron ? "electron" : "browser",
"service.version": APP_VERSION,
"t3.client.surface": isElectron ? "desktop" : "web",
...browserResourceAttributes(),
},
} as const;
};

const delegateRuntimeLayer = Layer.mergeAll(
primaryEnvironmentHttpLayer,
Expand Down
44 changes: 44 additions & 0 deletions docs/fork/0026-telemetry-says-which-app-sent-it.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# 0026: Telemetry says which app sent it

- PR: [TrogonStack/t3code#68](https://github.com/TrogonStack/t3code/pull/68)
- Status: active

## What you can do now

- Filter UI traces to the desktop window or to a browser tab with
`t3.client.surface`, instead of guessing from a key named `service.mode`.
- Tell a server the desktop app launched from one you started yourself with
`t3.server.managed_by`.
- Query every T3 Code signal with the resource attributes a collector,
dashboard, or vendor already understands: `service.version`,
`deployment.environment.name`, `process.runtime.*`, `user_agent.original`,
and `browser.*`.
- Set `deployment.environment.name` through `OTEL_RESOURCE_ATTRIBUTES` and
have the desktop app keep it.

## Why

The desktop window runs the web UI, so its traces arrive as `t3code-web`, and
the only thing separating them from a browser tab was `service.mode`. That
key sat in the `service.*` namespace, which OpenTelemetry reserves for its
own attributes, and it meant something different in each service: where the
UI runs in one, the build type in another, and who launched the process in
the third. Nobody reading a trace could know that without reading the code.

Following the semantic conventions puts every attribute where tooling
already looks for it, and keeps the one question the conventions have no
answer for, which surface of the product sent this, under the `t3.` prefix
the relay tracing already used.

## Upstream considerations

A plausible upstream submission. The attributes came from upstream, and the
change carries no fork-specific intent. Dashboards or saved queries that
filter on `service.mode`, `service.runtime`, or `service.component` have to
move to the new keys, which is the part upstream would want to weigh.

The server's own `mode` values (`web` for any standalone launch) are left
alone, since renaming them touches the CLI flag, `T3CODE_MODE`, and persisted
session data. Telemetry maps `mode` to `t3.server.managed_by` instead. A
sync that takes upstream's copy of any resource definition brings the old
keys back without any test going red outside the ones changed here.
2 changes: 2 additions & 0 deletions docs/fork/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,5 @@ Each entry uses these sections:
active, [#38](https://github.com/TrogonStack/t3code/pull/38)
- **0025** [A test run leaves no processes behind](./0025-a-test-run-leaves-no-processes-behind.md)
active, [#57](https://github.com/TrogonStack/t3code/pull/57)
- **0026** [Telemetry says which app sent it](./0026-telemetry-says-which-app-sent-it.md)
active, [#68](https://github.com/TrogonStack/t3code/pull/68)
16 changes: 12 additions & 4 deletions docs/operations/observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -611,10 +611,18 @@ 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`.
Service names are fixed: `t3code-server` for the backend, `t3code-desktop` for the desktop main
process, and `t3code-web` for the UI, all 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=staging`. The desktop main process reports
`deployment.environment.name` as `development` or `production` on its own, and a value from
`OTEL_RESOURCE_ATTRIBUTES` replaces it.

The UI runs as `t3code-web` in both a browser and the desktop window, since it is the same code. Its
`t3.client.surface` resource attribute is `desktop` or `web`, and `user_agent.original` carries the
full user agent. The server's `t3.server.managed_by` is `desktop` when the desktop app launched it
and `standalone` otherwise.

If the OTLP URLs are unset, local tracing still works, metrics stay in-process only, and logs stay
on stdout only.
Expand Down
13 changes: 13 additions & 0 deletions packages/shared/src/observability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ export interface SignalExport {
readonly exportIntervalMs: number;
}

/**
* `process.runtime.*` resource attributes for a Node process, matching the
* OpenTelemetry Node process detector. Electron embeds Node, so an Electron
* process reports Node and names Electron in the description.
*/
export const nodeProcessRuntimeAttributes = (): Record<string, string> => ({
"process.runtime.name": "nodejs",
"process.runtime.version": process.versions.node,
"process.runtime.description": process.versions.electron
? `Electron ${process.versions.electron}`
: "Node.js",
});

/** What T3 Code exports with when nothing configured a signal. */
export const DEFAULT_SIGNAL_EXPORT: SignalExport = {
protocol: "http/json",
Expand Down
4 changes: 2 additions & 2 deletions packages/shared/src/relayTracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,8 @@ export function makeRelayClientTracingLayer(
serviceVersion: resource.serviceVersion,
attributes: {
"service.namespace": "t3code",
"service.runtime": resource.runtime,
"service.component": resource.component ?? "relay-client",
"process.runtime.name": resource.runtime,
"t3.component": resource.component ?? "relay-client",
"t3.client.surface": resource.client,
},
},
Expand Down
Loading