Skip to content
Merged
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
43 changes: 43 additions & 0 deletions apps/desktop/src/app/DesktopObservability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,49 @@ describe("DesktopObservability", () => {
);
});

it.effect("keeps its service name while OTEL resource attributes add dimensions", () => {
const requests: Array<ExportedRequest> = [];
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<ExportedRequest> = [];
return Effect.gen(function* () {
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/app/DesktopObservability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/features/observability/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Comment thread
yordis marked this conversation as resolved.
serviceVersion: resource.serviceVersion,
runtime: "react-native",
client: `mobile-${resource.appVariant}`,
Expand Down
1 change: 0 additions & 1 deletion apps/server/src/bin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 0 additions & 1 deletion apps/server/src/cli/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 0 additions & 2 deletions apps/server/src/cli/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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,
Expand Down
1 change: 0 additions & 1 deletion apps/server/src/cli/pair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
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 @@ -7,14 +7,14 @@ const relayClientTracingConfig = resolveRelayClientTracingConfig();
export const headlessRelayClientTracingLayer = makeRelayClientTracingLayer(
relayClientTracingConfig,
{
serviceName: "t3-headless-relay-client",
serviceName: "t3code-server",
Comment thread
yordis marked this conversation as resolved.
runtime: "node",
client: "headless-cli",
},
Comment thread
yordis marked this conversation as resolved.
);

export const serverRelayBrokerTracingLayer = makeRelayClientTracingLayer(relayClientTracingConfig, {
serviceName: "t3-server",
serviceName: "t3code-server",
runtime: "node",
client: "environment-server",
component: "relay-broker",
Expand Down
5 changes: 2 additions & 3 deletions apps/server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
},
Expand Down Expand Up @@ -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,
Expand Down
1 change: 0 additions & 1 deletion apps/server/src/environment/ServerEnvironment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/persistence/Layers/Sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
}),
);
Expand Down
11 changes: 5 additions & 6 deletions apps/server/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -5283,7 +5282,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
attributes: [
{
key: "service.name",
value: { stringValue: "t3-web" },
value: { stringValue: "t3code-web" },
},
],
},
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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)),
);

Expand Down Expand Up @@ -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)),
);
Expand Down
31 changes: 29 additions & 2 deletions apps/server/src/serverLogger.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -57,7 +58,6 @@ const configLayer = (overrides: Partial<ServerConfig.ServerConfig["Service"]>) =
otlpTracesExport: DEFAULT_SIGNAL_EXPORT,
otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,
otlpLogsExport: DEFAULT_SIGNAL_EXPORT,
otlpServiceName: "t3-server",
otelEnvironment: OtelEnvironment.none,
cwd: baseDir,
baseDir,
Expand Down Expand Up @@ -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({});
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/lib/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Comment thread
yordis marked this conversation as resolved.
runtime: "browser",
client: typeof window !== "undefined" && window.desktopBridge ? "desktop" : "web",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/observability/clientTracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
24 changes: 14 additions & 10 deletions docs/operations/observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"
```

Expand All @@ -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
```

Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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

Expand All @@ -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`
Expand All @@ -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.

Expand Down
9 changes: 9 additions & 0 deletions infra/relay/src/observability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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",
Expand Down
Loading
Loading