Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
3 changes: 3 additions & 0 deletions apps/server/src/bin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import * as WorkspacePaths from "./workspace/WorkspacePaths.ts";
import * as ServerSecretStore from "./auth/ServerSecretStore.ts";
import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";
import { environmentAuthenticatedAuthLayer } from "./auth/http.ts";
import * as OtelEnvironment from "./observability/OtelEnvironment.ts";

const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, NetService.layer);
class ProjectCliHttpApi extends HttpApi.make("environment").add(EnvironmentOrchestrationHttpApi) {}
Expand Down Expand Up @@ -75,7 +76,9 @@ const makeCliTestServerConfig = (baseDir: string) =>
otlpTracesUrl: undefined,
otlpMetricsUrl: undefined,
otlpExportIntervalMs: 10_000,
otlpMetricsExportIntervalMs: 10_000,
otlpServiceName: "t3-server",
otelEnvironment: OtelEnvironment.none,
mode: "web",
port: 0,
host: "127.0.0.1",
Expand Down
129 changes: 129 additions & 0 deletions apps/server/src/cli/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import * as NetService from "@t3tools/shared/Net";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { deriveServerPaths } from "../config.ts";
import * as OtelEnvironment from "../observability/OtelEnvironment.ts";
import { resolveServerConfig } from "./config.ts";

const deriveExplicitServerPaths = (baseDir: string, devUrl: URL | undefined) =>
Expand Down Expand Up @@ -49,7 +50,9 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
otlpTracesUrl: undefined,
otlpMetricsUrl: undefined,
otlpExportIntervalMs: 10_000,
otlpMetricsExportIntervalMs: 10_000,
otlpServiceName: "t3-server",
otelEnvironment: OtelEnvironment.none,
devAllowedOrigins: [],
} as const;

Expand Down Expand Up @@ -488,6 +491,132 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
}),
);

const resolveWithEnv = (env: Record<string, string>) =>
resolveServerConfig(
{
mode: Option.some("web"),
port: Option.some(4888),
host: Option.none(),
baseDir: Option.some("/tmp/t3-otel-home"),
cwd: Option.none(),
devUrl: Option.none(),
noBrowser: Option.none(),
bootstrapFd: Option.none(),
autoBootstrapProjectFromCwd: Option.none(),
logWebSocketEvents: Option.none(),
tailscaleServeEnabled: Option.none(),
tailscaleServePort: Option.none(),
},
Option.none(),
).pipe(
Effect.provide(
Layer.mergeAll(ConfigProvider.layer(ConfigProvider.fromEnv({ env })), NetService.layer),
),
);
Comment thread
yordis marked this conversation as resolved.
Outdated

it.effect("exports to the endpoint the rest of the machine already uses", () =>
Effect.gen(function* () {
const resolved = yield* resolveWithEnv({
OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector.example.com",
OTEL_SERVICE_NAME: "t3",
});

expect(resolved.otlpTracesUrl).toBe("https://collector.example.com/v1/traces");
expect(resolved.otlpMetricsUrl).toBe("https://collector.example.com/v1/metrics");
expect(resolved.otlpServiceName).toBe("t3");
}),
);

it.effect("keeps T3 Code's own names as the explicit answer", () =>
Effect.gen(function* () {
const resolved = yield* resolveWithEnv({
OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector.example.com",
OTEL_SERVICE_NAME: "t3",
T3CODE_OTLP_TRACES_URL: "http://localhost:4318/v1/traces",
T3CODE_OTLP_SERVICE_NAME: "t3-local",
});

expect(resolved.otlpTracesUrl).toBe("http://localhost:4318/v1/traces");
expect(resolved.otlpMetricsUrl).toBe("https://collector.example.com/v1/metrics");
expect(resolved.otlpServiceName).toBe("t3-local");
}),
);

it.effect("leaves a T3 Code endpoint alone when the environment names another", () =>
Effect.gen(function* () {
// An ambient endpoint that lost the URL must not keep configuring the
// export around it: its wire format, headers, and batching belong to the
// endpoint it named, not to this one.
const resolved = yield* resolveWithEnv({
OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector.example.com",
T3CODE_OTLP_TRACES_URL: "http://localhost:4318/v1/traces",
});

expect(resolved.otelEnvironment.traces.settings).toBeUndefined();
expect(resolved.otelEnvironment.metrics.settings?.url).toBe(
"https://collector.example.com/v1/metrics",
);
expect(resolved.otlpExportIntervalMs).toBe(10_000);
}),
);

it.effect("keeps an ambient aggregation off a T3 Code metric endpoint", () =>
Effect.gen(function* () {
const resolved = yield* resolveWithEnv({
OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector.example.com",
OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "delta",
T3CODE_OTLP_METRICS_URL: "http://localhost:4318/v1/metrics",
});

expect(resolved.otelEnvironment.metrics.settings).toBeUndefined();
expect(resolved.otelEnvironment.traces.settings?.temporality).toBeUndefined();
}),
);

it.effect("keeps one signal's schedule off the other one", () =>
Effect.gen(function* () {
// Traces take the specification's five second batch delay from the
// ambient endpoint. Metrics went somewhere else and keep T3 Code's own
// interval rather than inheriting a number meant for spans.
const resolved = yield* resolveWithEnv({
OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector.example.com",
T3CODE_OTLP_METRICS_URL: "http://localhost:4318/v1/metrics",
});

expect(resolved.otlpExportIntervalMs).toBe(5_000);
expect(resolved.otlpMetricsExportIntervalMs).toBe(10_000);
}),
);

it.effect("does not report a signal as declined while it is exporting", () =>
Effect.gen(function* () {
// grpc turns off the export these variables asked for, and says nothing
// about a signal whose endpoint came from a T3 Code name.
const resolved = yield* resolveWithEnv({
OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector.example.com",
OTEL_EXPORTER_OTLP_PROTOCOL: "grpc",
T3CODE_OTLP_TRACES_URL: "http://localhost:4318/v1/traces",
});

expect(resolved.otlpTracesUrl).toBe("http://localhost:4318/v1/traces");
expect(resolved.otelEnvironment.traces.declined).toBeUndefined();
expect(resolved.otelEnvironment.metrics.declined).toContain("grpc");
}),
);

it.effect("exports nothing at all once the SDK is switched off", () =>
Effect.gen(function* () {
const resolved = yield* resolveWithEnv({
OTEL_SDK_DISABLED: "true",
OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector.example.com",
T3CODE_OTLP_TRACES_URL: "http://localhost:4318/v1/traces",
});

expect(resolved.otlpTracesUrl).toBeUndefined();
expect(resolved.otlpMetricsUrl).toBeUndefined();
}),
);

it.effect("falls back to persisted observability settings when env vars are absent", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
Expand Down
53 changes: 41 additions & 12 deletions apps/server/src/cli/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Argument, Flag } from "effect/unstable/cli";
import { readBootstrapEnvelope } from "../bootstrap.ts";
import * as ServerConfig from "../config.ts";
import { expandHomePath, resolveBaseDir } from "../os-jank.ts";
import * as OtelEnvironment from "../observability/OtelEnvironment.ts";

export const modeFlag = Flag.choice("mode", ServerConfig.RuntimeMode.literals).pipe(
Flag.withDescription("Runtime mode. `desktop` keeps loopback defaults unless overridden."),
Expand Down Expand Up @@ -95,9 +96,13 @@ const EnvServerConfig = Config.all({
Config.map(Option.getOrUndefined),
),
otlpExportIntervalMs: Config.int("T3CODE_OTLP_EXPORT_INTERVAL_MS").pipe(
Config.withDefault(10_000),
Config.option,
Config.map(Option.getOrUndefined),
),
otlpServiceName: Config.string("T3CODE_OTLP_SERVICE_NAME").pipe(
Config.option,
Config.map(Option.getOrUndefined),
),
otlpServiceName: Config.string("T3CODE_OTLP_SERVICE_NAME").pipe(Config.withDefault("t3-server")),
mode: Config.schema(ServerConfig.RuntimeMode, "T3CODE_MODE").pipe(
Config.option,
Config.map(Option.getOrUndefined),
Expand Down Expand Up @@ -220,6 +225,7 @@ export const resolveServerConfig = (
const path = yield* Path.Path;
const fs = yield* FileSystem.FileSystem;
const env = yield* EnvServerConfig;
const otel = yield* OtelEnvironment.load;
const normalizedFlags = {
mode: flags.mode ?? Option.none(),
port: flags.port ?? Option.none(),
Expand Down Expand Up @@ -349,23 +355,46 @@ export const resolveServerConfig = (
);
const logLevel = Option.getOrElse(cliLogLevel, () => env.logLevel);

// A signal whose endpoint came from somewhere else is not this route's to
// configure. Dropping the whole signal, rather than the endpoint alone,
// is what stops an ambient OTEL_EXPORTER_OTLP_ENDPOINT from changing the
// wire format, headers, batching, or aggregation of an export that a
// T3CODE_OTLP_* name or Settings already answered, and stops startup from
// reporting that signal as declined while it is exporting.
const namedTracesUrl =
env.otlpTracesUrl ?? bootstrap?.otlpTracesUrl ?? persistedObservabilitySettings.otlpTracesUrl;
const namedMetricsUrl =
env.otlpMetricsUrl ??
bootstrap?.otlpMetricsUrl ??
persistedObservabilitySettings.otlpMetricsUrl;
const otelEnvironment = {
...otel,
traces: namedTracesUrl === undefined ? otel.traces : OtelEnvironment.noSignal,
metrics: namedMetricsUrl === undefined ? otel.metrics : OtelEnvironment.noSignal,
} satisfies OtelEnvironment.OtelEnvironment;
Comment thread
yordis marked this conversation as resolved.
Comment thread
yordis marked this conversation as resolved.
Comment thread
yordis marked this conversation as resolved.

const config: ServerConfig.ServerConfig["Service"] = {
logLevel,
traceMinLevel: env.traceMinLevel,
traceTimingEnabled: env.traceTimingEnabled,
traceBatchWindowMs: env.traceBatchWindowMs,
traceMaxBytes: env.traceMaxBytes,
traceMaxFiles: env.traceMaxFiles,
otlpTracesUrl:
env.otlpTracesUrl ??
bootstrap?.otlpTracesUrl ??
persistedObservabilitySettings.otlpTracesUrl,
otlpMetricsUrl:
env.otlpMetricsUrl ??
bootstrap?.otlpMetricsUrl ??
persistedObservabilitySettings.otlpMetricsUrl,
otlpExportIntervalMs: env.otlpExportIntervalMs,
otlpServiceName: env.otlpServiceName,
otlpTracesUrl: otelEnvironment.disabled
? undefined
: (namedTracesUrl ?? otelEnvironment.traces.settings?.url),
otlpMetricsUrl: otelEnvironment.disabled
? undefined
: (namedMetricsUrl ?? otelEnvironment.metrics.settings?.url),
Comment thread
yordis marked this conversation as resolved.
// Each signal gets its own, because the environment names them
// separately and a signal that took its endpoint elsewhere must not
// inherit the other one's schedule.
otlpExportIntervalMs:
env.otlpExportIntervalMs ?? otelEnvironment.traces.settings?.exportIntervalMs ?? 10_000,
otlpMetricsExportIntervalMs:
env.otlpExportIntervalMs ?? otelEnvironment.metrics.settings?.exportIntervalMs ?? 10_000,
Comment thread
yordis marked this conversation as resolved.
Outdated
otlpServiceName: env.otlpServiceName ?? otelEnvironment.resource.serviceName ?? "t3-server",
Comment thread
yordis marked this conversation as resolved.
Outdated
otelEnvironment,
mode,
port,
cwd,
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/cli/pair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
resolveHeadlessConnectionString,
} from "../startupAccess.ts";
import { baseDirFlag, DurationFromString } from "./config.ts";
import * as OtelEnvironment from "../observability/OtelEnvironment.ts";

const WELL_KNOWN_ENVIRONMENT_PATH = "/.well-known/t3/environment";
const PAIR_PROBE_TIMEOUT = Duration.millis(2_500);
Expand Down Expand Up @@ -331,7 +332,9 @@ const makePairServerConfig = Effect.fn(function* (input: {
otlpTracesUrl: undefined,
otlpMetricsUrl: undefined,
otlpExportIntervalMs: 10_000,
otlpMetricsExportIntervalMs: 10_000,
otlpServiceName: "t3-server",
otelEnvironment: OtelEnvironment.none,
mode: "web",
port: state.port,
host: state.host,
Expand Down
12 changes: 12 additions & 0 deletions apps/server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import * as LogLevel from "effect/LogLevel";
import * as Path from "effect/Path";
import * as Schema from "effect/Schema";

import * as OtelEnvironment from "./observability/OtelEnvironment.ts";

export const DEFAULT_PORT = 3773;

export const RuntimeMode = Schema.Literals(["web", "desktop"]);
Expand Down Expand Up @@ -64,7 +66,15 @@ export class ServerConfig extends Context.Service<
readonly otlpTracesUrl: string | undefined;
readonly otlpMetricsUrl: string | undefined;
readonly otlpExportIntervalMs: number;
readonly otlpMetricsExportIntervalMs: number;
readonly otlpServiceName: string;
/**
* What the standard `OTEL_*` variables asked for. The endpoints above are
* already resolved from it; this carries the rest, which T3 Code has no
* names of its own for: headers, wire format, resource attributes, and the
* batching knobs.
*/
readonly otelEnvironment: OtelEnvironment.OtelEnvironment;
readonly mode: RuntimeMode;
readonly port: number;
readonly host: string | undefined;
Expand Down Expand Up @@ -177,7 +187,9 @@ const makeTest = Effect.fn("ServerConfig.makeTest")(function* (
otlpTracesUrl: undefined,
otlpMetricsUrl: undefined,
otlpExportIntervalMs: 10_000,
otlpMetricsExportIntervalMs: 10_000,
otlpServiceName: "t3-server",
otelEnvironment: OtelEnvironment.none,
cwd,
baseDir,
...derivedPaths,
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/environment/ServerEnvironment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from "../cloud/config.ts";
import * as ServerConfig from "../config.ts";
import * as ServerEnvironment from "./ServerEnvironment.ts";
import * as OtelEnvironment from "../observability/OtelEnvironment.ts";

const isServerEnvironmentIdPersistenceError = Schema.is(
ServerEnvironment.ServerEnvironmentIdPersistenceError,
Expand Down Expand Up @@ -51,7 +52,9 @@ const makeServerConfig = Effect.fn(function* (baseDir: string) {
otlpTracesUrl: undefined,
otlpMetricsUrl: undefined,
otlpExportIntervalMs: 10_000,
otlpMetricsExportIntervalMs: 10_000,
otlpServiceName: "t3-server",
otelEnvironment: OtelEnvironment.none,
cwd: process.cwd(),
baseDir,
mode: "web",
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export const otlpTracesProxyRouteLayer = HttpRouter.add(
const request = yield* HttpServerRequest.HttpServerRequest;
const config = yield* ServerConfig.ServerConfig;
const otlpTracesUrl = config.otlpTracesUrl;
const otlpTracesHeaders = config.otelEnvironment.traces.settings?.headers;
const browserTraceCollector = yield* BrowserTraceCollector.BrowserTraceCollector;
const httpClient = yield* HttpClient.HttpClient;
const bodyJson = cast<unknown, OtlpTracer.TraceData>(yield* request.json);
Expand All @@ -171,6 +172,7 @@ export const otlpTracesProxyRouteLayer = HttpRouter.add(
return yield* httpClient
.post(otlpTracesUrl, {
body: HttpBody.jsonUnsafe(bodyJson),
...(otlpTracesHeaders === undefined ? {} : { headers: otlpTracesHeaders }),
Comment thread
cursor[bot] marked this conversation as resolved.
})
.pipe(
Effect.flatMap(HttpClientResponse.filterStatusOk),
Expand Down
Loading
Loading