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
2 changes: 2 additions & 0 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,
processServiceInstanceId,
type SignalExport,
} from "@t3tools/shared/observability";
import * as HostProcess from "@t3tools/shared/HostProcess";
Expand Down Expand Up @@ -603,6 +604,7 @@ const layerTelemetry = Layer.unwrap(
"service.namespace": "t3code",
"service.runtime": "desktop",
"service.mode": environment.isDevelopment ? "development" : "packaged",
"service.instance.id": processServiceInstanceId(endpoints.resourceAttributes),
"host.name": endpoints.resourceAttributes["host.name"] || (yield* HostProcess.Hostname),
},
};
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/features/observability/tracing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ it.effect("does not let OTLP serialization failures alter application effects",
},
{
appVariant: "test",
serviceInstanceId: "test-instance",
serviceVersion: "1.2.3",
},
).pipe(Layer.provide(layerRemoteHttpClient(fetchFn)));
Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/src/features/observability/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface TracingConfig {

export interface TracingResource {
readonly serviceVersion?: string;
readonly serviceInstanceId: string;
readonly appVariant: string;
}

Expand All @@ -28,12 +29,14 @@ export function layerFromConfig(config: TracingConfig | null, resource: TracingR
serviceName: "t3code-mobile",
serviceVersion: resource.serviceVersion,
runtime: "react-native",
serviceInstanceId: resource.serviceInstanceId,
client: `mobile-${resource.appVariant}`,
});
}

export const layer = layerFromConfig(resolveTracingConfig(), {
serviceVersion: Constants.expoConfig?.version,
serviceInstanceId: Constants.sessionId,
appVariant:
typeof Constants.expoConfig?.extra?.appVariant === "string"
? Constants.expoConfig.extra.appVariant
Expand Down
18 changes: 16 additions & 2 deletions apps/server/src/cloud/relayTracing.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
import { processServiceInstanceId } from "@t3tools/shared/observability";
import * as RelayTracing from "@t3tools/shared/relayTracing";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";

import * as ServerConfig from "../config.ts";
import { resolveRelayClientTracingConfig } from "./publicConfig.ts";

const relayClientTracingConfig = resolveRelayClientTracingConfig();

export const layerHeadlessRelayClient = RelayTracing.layer(relayClientTracingConfig, {
const relayLayer = (resource: Omit<RelayTracing.RelayClientTracingResource, "serviceInstanceId">) =>
Layer.unwrap(
Effect.map(ServerConfig.ServerConfig, (config) =>
RelayTracing.layer(relayClientTracingConfig, {
...resource,
serviceInstanceId: processServiceInstanceId(config.otelEnvironment.resourceAttributes),
}),
),
);

export const layerHeadlessRelayClient = relayLayer({
serviceName: "t3code-server",
runtime: "node",
client: "headless-cli",
});

export const layerServerRelayBroker = RelayTracing.layer(relayClientTracingConfig, {
export const layerServerRelayBroker = relayLayer({
serviceName: "t3code-server",
runtime: "node",
client: "environment-server",
Expand Down
7 changes: 6 additions & 1 deletion apps/server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import * as Schema from "effect/Schema";

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

Expand Down Expand Up @@ -114,6 +118,7 @@ export const otlpResource = (
"service.namespace": "t3code",
"service.runtime": "t3-server",
"t3code.server.managed_by": config.mode === "desktop" ? "desktop" : "standalone",
"service.instance.id": processServiceInstanceId(config.otelEnvironment.resourceAttributes),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes the server's exported telemetry resource, but the focused tests cover only the ID helper, desktop, and mobile; none checks the server resource. Could you add a focused test for otlpResource that verifies the generated instance ID and the operator-provided override? That will protect the server's resource-attribute wiring. The fix requires a test-file change, so there is no single-hunk suggestion here.

Posted via Macroscope — Effect Service Conventions

"host.name": config.otelEnvironment.resourceAttributes["host.name"] || hostName,
},
});
Expand Down
15 changes: 15 additions & 0 deletions apps/server/src/serverLogger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,21 @@ const printThroughConsole = (fields: object) =>
});

describe("ServerLoggerLive", () => {
it.effect("exports the operator's service instance id instead of a generated one", () =>
Effect.gen(function* () {
const { requests } = yield* logInSpanThrough({
otlpLogsUrl: "https://collector.example.com/v1/logs",
otelEnvironment: {
...OtelEnvironment.none,
resourceAttributes: { "service.instance.id": "pod-7" },
},
});

const body = requests.map((request) => request.body).join("\n");
assert.include(body, '"key":"service.instance.id","value":{"stringValue":"pod-7"}');
}),
);

it.effect("exports a resource that names whether the desktop app launched the server", () =>
Effect.gen(function* () {
const { requests } = yield* logInSpanThrough({
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/lib/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const layerRelayTracing = RelayTracing.layer(resolveRelayTracingConfig(), {
serviceName: "t3code-web",
serviceVersion: import.meta.env.APP_VERSION,
runtime: "browser",
serviceInstanceId: ClientTracer.serviceInstanceId,
client: typeof window !== "undefined" && window.desktopBridge ? "desktop" : "web",
}).pipe(Layer.provide(layerHttpClient));

Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/observability/clientTracer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import * as Layer from "effect/Layer";
import * as Tracer from "effect/Tracer";

import { randomUUID } from "../lib/utils";

/** `service.instance.id` for this page load, shared by every resource the UI exports. */
export const serviceInstanceId = randomUUID();

let delegate: Tracer.Tracer | null = null;

export function setDelegate(next: Tracer.Tracer | null): void {
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/observability/clientTracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const CLIENT_TRACING_RESOURCE = {
serviceName: "t3code-web",
attributes: {
"service.namespace": "t3code",
"service.instance.id": ClientTracer.serviceInstanceId,
"deployment.environment.name": import.meta.env.DEV ? "development" : "production",
"service.runtime": "t3-web",
"t3code.client.surface": isElectron ? "desktop" : "web",
Expand Down
4 changes: 4 additions & 0 deletions docs/operations/observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,10 @@ browser and the desktop window, since it is the same code, and `t3code.client.su
or `web`. The server's `t3code.server.managed_by` is `desktop` when the desktop app launched it and
`standalone` otherwise.

Every resource also carries a `service.instance.id`: random per process for the server and desktop,
per page load for the UI, and the Expo session on mobile. A `service.instance.id` in
`OTEL_RESOURCE_ATTRIBUTES` replaces the generated one on the server and desktop.

If the OTLP URLs are unset, local tracing still works, metrics stay in-process only, and logs stay
on stdout only.

Expand Down
8 changes: 8 additions & 0 deletions packages/shared/src/observability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,16 @@ import {
type TraceRecord,
OtlpHeadersFromString,
truncateTraceAttributes,
processServiceInstanceId,
} from "./observability.ts";

describe("processServiceInstanceId", () => {
it("stays the same for the process unless the operator set one", () => {
expect(processServiceInstanceId()).toBe(processServiceInstanceId({}));
expect(processServiceInstanceId({ "service.instance.id": "pod-7" })).toBe("pod-7");
});
});

describe("errorTag", () => {
it("reports structural tags without retaining arbitrary values", () => {
assert.equal(errorTag({ _tag: "AcpRequestError" }), "AcpRequestError");
Expand Down
15 changes: 15 additions & 0 deletions packages/shared/src/observability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,21 @@ export interface SignalExport {
readonly exportIntervalMs: number;
}

let generatedServiceInstanceId: string | undefined;

/**
* `service.instance.id` shared by every resource this process exports. Effect lets code
* attributes beat `OTEL_RESOURCE_ATTRIBUTES`, so callers pass the operator's in to keep it winning.
*/
export const processServiceInstanceId = (
operatorAttributes: Readonly<Record<string, string>> = {},
): string => {
if (operatorAttributes["service.instance.id"]) return operatorAttributes["service.instance.id"];
// @effect-diagnostics-next-line cryptoRandomUUID:off -- Resources are built synchronously, outside any Effect.
generatedServiceInstanceId ??= crypto.randomUUID();
return generatedServiceInstanceId;
};

export const DEFAULT_SIGNAL_EXPORT: SignalExport = {
protocol: "http/json",
headers: undefined,
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/relayTracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface RelayClientTracingResource {
readonly serviceName: string;
readonly serviceVersion?: string;
readonly runtime: string;
readonly serviceInstanceId: string;
readonly client: string;
readonly component?: string;
}
Expand Down Expand Up @@ -165,6 +166,7 @@ export function layer(
serviceVersion: resource.serviceVersion,
attributes: {
"service.namespace": "t3code",
"service.instance.id": resource.serviceInstanceId,
"service.runtime": resource.runtime,
"t3code.component": resource.component ?? "relay-client",
"t3code.client.surface": resource.client,
Expand Down
Loading