authorgravatar for yordis.prieto@gmail.comYordis Prieto <yordis.prieto@gmail.com> 2026-09-20 15:56:26-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-20 12:56:26-07:00
logfa1e1715538f6f8ac076761f7821f68479f616ab
treee22554ddb368fcd2d2d12a649fa3662ca59afd86
parentc14f6015bfe479d313355cb234af1a5c16dbb15f
signature Signed by PGP key B5690EEEBB952194

refactor(observability): hold OTLP export settings per signal (#12657)

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>

12 files changed, 104 insertions(+), 49 deletions(-)

apps/server/src/bin.test.ts+4-3
...@@ -15,6 +15,7 @@ import {...@@ -15,6 +15,7 @@ import {
15} from "@t3tools/contracts";15} from "@t3tools/contracts";
16import * as NetService from "@t3tools/shared/Net";16import * as NetService from "@t3tools/shared/Net";
17import { HostProcessEnvironment } from "@t3tools/shared/hostProcess";17import { HostProcessEnvironment } from "@t3tools/shared/hostProcess";
18import { DEFAULT_SIGNAL_EXPORT } from "@t3tools/shared/observability";
18import { assert, it } from "@effect/vitest";19import { assert, it } from "@effect/vitest";
19import * as Effect from "effect/Effect";20import * as Effect from "effect/Effect";
20import * as DateTime from "effect/DateTime";21import * as DateTime from "effect/DateTime";
...@@ -101,10 +102,10 @@ const makeCliTestServerConfig = (baseDir: string) =>...@@ -101,10 +102,10 @@ const makeCliTestServerConfig = (baseDir: string) =>
101 otlpTracesUrl: undefined,102 otlpTracesUrl: undefined,
102 otlpMetricsUrl: undefined,103 otlpMetricsUrl: undefined,
103 otlpLogsUrl: undefined,104 otlpLogsUrl: undefined,
104 otlpExportIntervalMs: 10_000,105 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,
106 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,
107 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,
105 otlpServiceName: "t3-server",108 otlpServiceName: "t3-server",
106 otlpHeaders: undefined,
107 otlpProtocol: "http/json",
108 mode: "web",109 mode: "web",
109 port: 0,110 port: 0,
110 host: "127.0.0.1",111 host: "127.0.0.1",
apps/server/src/cli/config.test.ts+12-7
...@@ -17,6 +17,7 @@ import {...@@ -17,6 +17,7 @@ import {
17 type DesktopBackendBootstrap as DesktopBackendBootstrapValue,17 type DesktopBackendBootstrap as DesktopBackendBootstrapValue,
18} from "@t3tools/contracts";18} from "@t3tools/contracts";
19import * as NetService from "@t3tools/shared/Net";19import * as NetService from "@t3tools/shared/Net";
20import { DEFAULT_SIGNAL_EXPORT } from "@t3tools/shared/observability";
20import * as NodeServices from "@effect/platform-node/NodeServices";21import * as NodeServices from "@effect/platform-node/NodeServices";
21import { deriveServerPaths } from "../config.ts";22import { deriveServerPaths } from "../config.ts";
22import { resolveServerConfig } from "./config.ts";23import { resolveServerConfig } from "./config.ts";
...@@ -51,10 +52,10 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {...@@ -51,10 +52,10 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
51 otlpTracesUrl: undefined,52 otlpTracesUrl: undefined,
52 otlpMetricsUrl: undefined,53 otlpMetricsUrl: undefined,
53 otlpLogsUrl: undefined,54 otlpLogsUrl: undefined,
54 otlpExportIntervalMs: 10_000,55 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,
56 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,
57 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,
55 otlpServiceName: "t3-server",58 otlpServiceName: "t3-server",
56 otlpHeaders: undefined,
57 otlpProtocol: "http/json",
58 devAllowedOrigins: [],59 devAllowedOrigins: [],
59 } as const;60 } as const;
6061
...@@ -764,7 +765,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {...@@ -764,7 +765,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
764 ),765 ),
765 );766 );
766767
767 expect(resolved.otlpHeaders).toEqual({768 expect(resolved.otlpTracesExport.headers).toEqual({
768 authorization: "Basic abc==",769 authorization: "Basic abc==",
769 "x-tenant": "t3",770 "x-tenant": "t3",
770 });771 });
...@@ -808,7 +809,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {...@@ -808,7 +809,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
808 ),809 ),
809 );810 );
810811
811 expect(resolved.otlpHeaders).toEqual({812 expect(resolved.otlpTracesExport.headers).toEqual({
812 authorization: "Bearer abc==",813 authorization: "Bearer abc==",
813 "x-tenant": "t3",814 "x-tenant": "t3",
814 });815 });
...@@ -816,7 +817,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {...@@ -816,7 +817,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
816 }),817 }),
817 );818 );
818819
819 it.effect("reads the OTLP protocol from env", () =>820 it.effect("gives every signal the protocol named without one", () =>
820 Effect.gen(function* () {821 Effect.gen(function* () {
821 const { join } = yield* Path.Path;822 const { join } = yield* Path.Path;
822 const baseDir = join(NodeOS.tmpdir(), "t3-cli-config-otlp-protocol-base");823 const baseDir = join(NodeOS.tmpdir(), "t3-cli-config-otlp-protocol-base");
...@@ -848,7 +849,11 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {...@@ -848,7 +849,11 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
848 ),849 ),
849 );850 );
850851
851 expect(resolved.otlpProtocol).toBe("http/protobuf");852 expect([
853 resolved.otlpTracesExport.protocol,
854 resolved.otlpMetricsExport.protocol,
855 resolved.otlpLogsExport.protocol,
856 ]).toEqual(["http/protobuf", "http/protobuf", "http/protobuf"]);
852 }),857 }),
853 );858 );
854859
apps/server/src/cli/config.ts+16-4
...@@ -1,5 +1,9 @@...@@ -1,5 +1,9 @@
1import * as NetService from "@t3tools/shared/Net";1import * as NetService from "@t3tools/shared/Net";
2import { OtlpHeadersFromString, OtlpProtocol } from "@t3tools/shared/observability";2import {
3 OtlpHeadersFromString,
4 OtlpProtocol,
5 type SignalExport,
6} from "@t3tools/shared/observability";
3import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings";7import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings";
4import { DesktopBackendBootstrap, PortSchema } from "@t3tools/contracts";8import { DesktopBackendBootstrap, PortSchema } from "@t3tools/contracts";
5import * as Config from "effect/Config";9import * as Config from "effect/Config";
...@@ -382,6 +386,14 @@ export const resolveServerConfig = (...@@ -382,6 +386,14 @@ export const resolveServerConfig = (
382 );386 );
383 const logLevel = Option.getOrElse(cliLogLevel, () => env.logLevel);387 const logLevel = Option.getOrElse(cliLogLevel, () => env.logLevel);
384388
389 // T3 Code's own OTLP variables name no signal, so the one answer they give
390 // is the answer for all three.
391 const signalExport: SignalExport = {
392 protocol: env.otlpProtocol,
393 headers: env.otlpHeaders,
394 exportIntervalMs: env.otlpExportIntervalMs,
395 };
396
385 const config: ServerConfig.ServerConfig["Service"] = {397 const config: ServerConfig.ServerConfig["Service"] = {
386 logLevel,398 logLevel,
387 traceMinLevel: env.traceMinLevel,399 traceMinLevel: env.traceMinLevel,
...@@ -399,10 +411,10 @@ export const resolveServerConfig = (...@@ -399,10 +411,10 @@ export const resolveServerConfig = (
399 persistedObservabilitySettings.otlpMetricsUrl,411 persistedObservabilitySettings.otlpMetricsUrl,
400 otlpLogsUrl:412 otlpLogsUrl:
401 env.otlpLogsUrl ?? bootstrap?.otlpLogsUrl ?? persistedObservabilitySettings.otlpLogsUrl,413 env.otlpLogsUrl ?? bootstrap?.otlpLogsUrl ?? persistedObservabilitySettings.otlpLogsUrl,
402 otlpExportIntervalMs: env.otlpExportIntervalMs,414 otlpTracesExport: signalExport,
415 otlpMetricsExport: signalExport,
416 otlpLogsExport: signalExport,
403 otlpServiceName: env.otlpServiceName,417 otlpServiceName: env.otlpServiceName,
404 otlpHeaders: env.otlpHeaders,
405 otlpProtocol: env.otlpProtocol,
406 mode,418 mode,
407 port,419 port,
408 cwd,420 cwd,
apps/server/src/cli/pair.ts+4-3
...@@ -15,6 +15,7 @@ import {...@@ -15,6 +15,7 @@ import {
15 PortSchema,15 PortSchema,
16} from "@t3tools/contracts";16} from "@t3tools/contracts";
17import { resolveWorktreeT3Home } from "@t3tools/shared/devHome";17import { resolveWorktreeT3Home } from "@t3tools/shared/devHome";
18import { DEFAULT_SIGNAL_EXPORT } from "@t3tools/shared/observability";
18import {19import {
19 buildTailscaleHttpsBaseUrl,20 buildTailscaleHttpsBaseUrl,
20 DEFAULT_TAILSCALE_SERVE_PORT,21 DEFAULT_TAILSCALE_SERVE_PORT,
...@@ -321,10 +322,10 @@ const makePairServerConfig = Effect.fn(function* (input: {...@@ -321,10 +322,10 @@ const makePairServerConfig = Effect.fn(function* (input: {
321 otlpTracesUrl: undefined,322 otlpTracesUrl: undefined,
322 otlpMetricsUrl: undefined,323 otlpMetricsUrl: undefined,
323 otlpLogsUrl: undefined,324 otlpLogsUrl: undefined,
324 otlpExportIntervalMs: 10_000,325 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,
326 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,
327 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,
325 otlpServiceName: "t3-server",328 otlpServiceName: "t3-server",
326 otlpHeaders: undefined,
327 otlpProtocol: "http/json",
328 mode: "web",329 mode: "web",
329 port: state.port,330 port: state.port,
330 host: state.host,331 host: state.host,
apps/server/src/config.ts+12-7
...@@ -17,7 +17,7 @@ import type * as Redacted from "effect/Redacted";...@@ -17,7 +17,7 @@ import type * as Redacted from "effect/Redacted";
17import * as Schema from "effect/Schema";17import * as Schema from "effect/Schema";
1818
19import { sweepStalePendingAttachments } from "./attachmentStore.ts";19import { sweepStalePendingAttachments } from "./attachmentStore.ts";
20import { OtlpProtocol } from "@t3tools/shared/observability";20import { DEFAULT_SIGNAL_EXPORT, type SignalExport } from "@t3tools/shared/observability";
2121
22export const DEFAULT_PORT = 3773;22export const DEFAULT_PORT = 3773;
2323
...@@ -73,10 +73,15 @@ export class ServerConfig extends Context.Service<...@@ -73,10 +73,15 @@ export class ServerConfig extends Context.Service<
73 readonly otlpTracesUrl: string | undefined;73 readonly otlpTracesUrl: string | undefined;
74 readonly otlpMetricsUrl: string | undefined;74 readonly otlpMetricsUrl: string | undefined;
75 readonly otlpLogsUrl: string | undefined;75 readonly otlpLogsUrl: string | undefined;
76 readonly otlpExportIntervalMs: number;76 /**
77 * How each signal is exported. Read instead of a process-wide setting so
78 * the wire format, credential, and schedule travel with the endpoint they
79 * were configured beside.
80 */
81 readonly otlpTracesExport: SignalExport;
82 readonly otlpMetricsExport: SignalExport;
83 readonly otlpLogsExport: SignalExport;
77 readonly otlpServiceName: string;84 readonly otlpServiceName: string;
78 readonly otlpHeaders: Readonly<Record<string, string>> | undefined;
79 readonly otlpProtocol: OtlpProtocol;
80 readonly mode: RuntimeMode;85 readonly mode: RuntimeMode;
81 readonly port: number;86 readonly port: number;
82 readonly host: string | undefined;87 readonly host: string | undefined;
...@@ -212,10 +217,10 @@ const makeTest = Effect.fn("ServerConfig.makeTest")(function* (...@@ -212,10 +217,10 @@ const makeTest = Effect.fn("ServerConfig.makeTest")(function* (
212 otlpTracesUrl: undefined,217 otlpTracesUrl: undefined,
213 otlpMetricsUrl: undefined,218 otlpMetricsUrl: undefined,
214 otlpLogsUrl: undefined,219 otlpLogsUrl: undefined,
215 otlpExportIntervalMs: 10_000,220 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,
221 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,
222 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,
216 otlpServiceName: "t3-server",223 otlpServiceName: "t3-server",
217 otlpHeaders: undefined,
218 otlpProtocol: "http/json",
219 cwd,224 cwd,
220 baseDir,225 baseDir,
221 ...derivedPaths,226 ...derivedPaths,
apps/server/src/environment/ServerEnvironment.test.ts+5-3
...@@ -10,6 +10,8 @@ import * as Option from "effect/Option";...@@ -10,6 +10,8 @@ import * as Option from "effect/Option";
10import * as PlatformError from "effect/PlatformError";10import * as PlatformError from "effect/PlatformError";
11import * as Schema from "effect/Schema";11import * as Schema from "effect/Schema";
1212
13import { DEFAULT_SIGNAL_EXPORT } from "@t3tools/shared/observability";
14
13import * as ServerSecretStore from "../auth/ServerSecretStore.ts";15import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
14import {16import {
15 PUBLISH_AGENT_ACTIVITY_SECRET,17 PUBLISH_AGENT_ACTIVITY_SECRET,
...@@ -54,10 +56,10 @@ const makeServerConfig = Effect.fn(function* (baseDir: string) {...@@ -54,10 +56,10 @@ const makeServerConfig = Effect.fn(function* (baseDir: string) {
54 otlpTracesUrl: undefined,56 otlpTracesUrl: undefined,
55 otlpMetricsUrl: undefined,57 otlpMetricsUrl: undefined,
56 otlpLogsUrl: undefined,58 otlpLogsUrl: undefined,
57 otlpExportIntervalMs: 10_000,59 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,
60 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,
61 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,
58 otlpServiceName: "t3-server",62 otlpServiceName: "t3-server",
59 otlpHeaders: undefined,
60 otlpProtocol: "http/json",
61 cwd: process.cwd(),63 cwd: process.cwd(),
62 baseDir,64 baseDir,
63 mode: "web",65 mode: "web",
apps/server/src/http.ts+1-1
...@@ -322,7 +322,7 @@ export const otlpTracesProxyRouteLayer = HttpRouter.add(...@@ -322,7 +322,7 @@ export const otlpTracesProxyRouteLayer = HttpRouter.add(
322 const request = yield* HttpServerRequest.HttpServerRequest;322 const request = yield* HttpServerRequest.HttpServerRequest;
323 const config = yield* ServerConfig.ServerConfig;323 const config = yield* ServerConfig.ServerConfig;
324 const otlpTracesUrl = config.otlpTracesUrl;324 const otlpTracesUrl = config.otlpTracesUrl;
325 const otlpHeaders = config.otlpHeaders;325 const otlpHeaders = config.otlpTracesExport.headers;
326 const browserTraceCollector = yield* BrowserTraceCollector.BrowserTraceCollector;326 const browserTraceCollector = yield* BrowserTraceCollector.BrowserTraceCollector;
327 const httpClient = yield* HttpClient.HttpClient;327 const httpClient = yield* HttpClient.HttpClient;
328 const serialization = yield* OtlpSerialization.OtlpSerialization;328 const serialization = yield* OtlpSerialization.OtlpSerialization;
apps/server/src/observability/Layers/Observability.ts+10-6
...@@ -20,7 +20,11 @@ import * as BrowserTraceCollector from "../BrowserTraceCollector.ts";...@@ -20,7 +20,11 @@ import * as BrowserTraceCollector from "../BrowserTraceCollector.ts";
20export const ObservabilityLive = Layer.unwrap(20export const ObservabilityLive = Layer.unwrap(
21 Effect.gen(function* () {21 Effect.gen(function* () {
22 const config = yield* ServerConfig.ServerConfig;22 const config = yield* ServerConfig.ServerConfig;
23 const serializationLayer = otlpSerializationLayer(config.otlpProtocol);23 const traces = config.otlpTracesExport;
24 const metrics = config.otlpMetricsExport;
25 // The trace serializer stays in the returned context because the browser
26 // trace forwarder exports on the same signal.
27 const serializationLayer = otlpSerializationLayer(traces.protocol);
24 const resource = ServerConfig.otlpResource(config);28 const resource = ServerConfig.otlpResource(config);
25 const attribution = yield* ResourceAttribution.ResourceAttribution;29 const attribution = yield* ResourceAttribution.ResourceAttribution;
2630
...@@ -51,8 +55,8 @@ export const ObservabilityLive = Layer.unwrap(...@@ -51,8 +55,8 @@ export const ObservabilityLive = Layer.unwrap(
51 ? undefined55 ? undefined
52 : yield* OtlpTracer.make({56 : yield* OtlpTracer.make({
53 url: config.otlpTracesUrl,57 url: config.otlpTracesUrl,
54 exportInterval: `${config.otlpExportIntervalMs} millis`,58 exportInterval: `${traces.exportIntervalMs} millis`,
55 headers: config.otlpHeaders,59 headers: traces.headers,
56 resource,60 resource,
57 });61 });
5862
...@@ -77,10 +81,10 @@ export const ObservabilityLive = Layer.unwrap(...@@ -77,10 +81,10 @@ export const ObservabilityLive = Layer.unwrap(
77 ? Layer.empty81 ? Layer.empty
78 : OtlpMetrics.layer({82 : OtlpMetrics.layer({
79 url: config.otlpMetricsUrl,83 url: config.otlpMetricsUrl,
80 exportInterval: `${config.otlpExportIntervalMs} millis`,84 exportInterval: `${metrics.exportIntervalMs} millis`,
81 headers: config.otlpHeaders,85 headers: metrics.headers,
82 resource,86 resource,
83 }).pipe(Layer.provideMerge(serializationLayer));87 }).pipe(Layer.provide(otlpSerializationLayer(metrics.protocol)));
8488
85 return Layer.mergeAll(ServerLoggerLive, traceReferencesLayer, tracerLayer, metricsLayer);89 return Layer.mergeAll(ServerLoggerLive, traceReferencesLayer, tracerLayer, metricsLayer);
86 }),90 }),
apps/server/src/server.test.ts+6-6
...@@ -218,7 +218,7 @@ import {...@@ -218,7 +218,7 @@ import {
218 transferBudgetViolations,218 transferBudgetViolations,
219} from "../integration/TransferBudgetReport.integration.ts";219} from "../integration/TransferBudgetReport.integration.ts";
220import { symlinksSupported } from "@t3tools/shared/testing/symlinks";220import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
221import { otlpSerializationLayer } from "@t3tools/shared/observability";221import { DEFAULT_SIGNAL_EXPORT, otlpSerializationLayer } from "@t3tools/shared/observability";
222222
223const defaultProjectId = ProjectId.make("project-default");223const defaultProjectId = ProjectId.make("project-default");
224const defaultThreadId = ThreadId.make("thread-default");224const defaultThreadId = ThreadId.make("thread-default");
...@@ -578,10 +578,10 @@ const buildAppUnderTest = (options?: {...@@ -578,10 +578,10 @@ const buildAppUnderTest = (options?: {
578 otlpTracesUrl: undefined,578 otlpTracesUrl: undefined,
579 otlpMetricsUrl: undefined,579 otlpMetricsUrl: undefined,
580 otlpLogsUrl: undefined,580 otlpLogsUrl: undefined,
581 otlpExportIntervalMs: 10_000,581 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,
582 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,
583 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,
582 otlpServiceName: "t3-server",584 otlpServiceName: "t3-server",
583 otlpHeaders: undefined,
584 otlpProtocol: "http/json",
585 mode: "desktop",585 mode: "desktop",
586 port: 0,586 port: 0,
587 host: "127.0.0.1",587 host: "127.0.0.1",
...@@ -1076,7 +1076,7 @@ const buildAppUnderTest = (options?: {...@@ -1076,7 +1076,7 @@ const buildAppUnderTest = (options?: {
1076 ...options?.layers?.browserTraceCollector,1076 ...options?.layers?.browserTraceCollector,
1077 }),1077 }),
1078 ),1078 ),
1079 Layer.provide(otlpSerializationLayer(config.otlpProtocol)),1079 Layer.provide(otlpSerializationLayer(config.otlpTracesExport.protocol)),
1080 Layer.provide(1080 Layer.provide(
1081 Layer.mock(ServerLifecycleEvents.ServerLifecycleEvents)({1081 Layer.mock(ServerLifecycleEvents.ServerLifecycleEvents)({
1082 publish: (event) => Effect.succeed({ ...(event as any), sequence: 1 }),1082 publish: (event) => Effect.succeed({ ...(event as any), sequence: 1 }),
...@@ -5299,7 +5299,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -5299,7 +5299,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
5299 yield* buildAppUnderTest({5299 yield* buildAppUnderTest({
5300 config: {5300 config: {
5301 otlpTracesUrl: collector.url,5301 otlpTracesUrl: collector.url,
5302 otlpProtocol: "http/protobuf",5302 otlpTracesExport: { ...DEFAULT_SIGNAL_EXPORT, protocol: "http/protobuf" },
5303 },5303 },
5304 layers: {5304 layers: {
5305 browserTraceCollector: {5305 browserTraceCollector: {
apps/server/src/serverLogger.test.ts+11-6
...@@ -8,6 +8,8 @@ import * as Tracer from "effect/Tracer";...@@ -8,6 +8,8 @@ import * as Tracer from "effect/Tracer";
8import * as HttpClient from "effect/unstable/http/HttpClient";8import * as HttpClient from "effect/unstable/http/HttpClient";
9import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";9import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
1010
11import { DEFAULT_SIGNAL_EXPORT } from "@t3tools/shared/observability";
12
11import * as ServerConfig from "./config.ts";13import * as ServerConfig from "./config.ts";
12import { ServerLoggerLive } from "./serverLogger.ts";14import { ServerLoggerLive } from "./serverLogger.ts";
1315
...@@ -51,10 +53,10 @@ const configLayer = (overrides: Partial<ServerConfig.ServerConfig["Service"]>) =...@@ -51,10 +53,10 @@ const configLayer = (overrides: Partial<ServerConfig.ServerConfig["Service"]>) =
51 otlpTracesUrl: undefined,53 otlpTracesUrl: undefined,
52 otlpMetricsUrl: undefined,54 otlpMetricsUrl: undefined,
53 otlpLogsUrl: undefined,55 otlpLogsUrl: undefined,
54 otlpExportIntervalMs: 10_000,56 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,
57 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,
58 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,
55 otlpServiceName: "t3-server",59 otlpServiceName: "t3-server",
56 otlpHeaders: undefined,
57 otlpProtocol: "http/json",
58 cwd: baseDir,60 cwd: baseDir,
59 baseDir,61 baseDir,
60 ...derivedPaths,62 ...derivedPaths,
...@@ -155,12 +157,15 @@ describe("ServerLoggerLive", () => {...@@ -155,12 +157,15 @@ describe("ServerLoggerLive", () => {
155 }),157 }),
156 );158 );
157159
158 it.effect("sends the headers and wire format the rest of OTLP export already uses", () =>160 it.effect("sends the headers and wire format the log signal asked for", () =>
159 Effect.gen(function* () {161 Effect.gen(function* () {
160 const requests = yield* logThrough({162 const requests = yield* logThrough({
161 otlpLogsUrl: "https://collector.example.com/v1/logs",163 otlpLogsUrl: "https://collector.example.com/v1/logs",
162 otlpProtocol: "http/protobuf",164 otlpLogsExport: {
163 otlpHeaders: { "x-scope": "logs" },165 ...DEFAULT_SIGNAL_EXPORT,
166 protocol: "http/protobuf",
167 headers: { "x-scope": "logs" },
168 },
164 });169 });
165170
166 assert.lengthOf(requests, 1);171 assert.lengthOf(requests, 1);
apps/server/src/serverLogger.ts+4-3
...@@ -12,13 +12,14 @@ export const ServerLoggerLive = Effect.gen(function* () {...@@ -12,13 +12,14 @@ export const ServerLoggerLive = Effect.gen(function* () {
12 const config = yield* ServerConfig;12 const config = yield* ServerConfig;
13 const minimumLogLevelLayer = Layer.succeed(References.MinimumLogLevel, config.logLevel);13 const minimumLogLevelLayer = Layer.succeed(References.MinimumLogLevel, config.logLevel);
1414
15 const logs = config.otlpLogsExport;
15 const otlpLogger =16 const otlpLogger =
16 config.otlpLogsUrl === undefined17 config.otlpLogsUrl === undefined
17 ? undefined18 ? undefined
18 : OtlpLogger.make({19 : OtlpLogger.make({
19 url: config.otlpLogsUrl,20 url: config.otlpLogsUrl,
20 exportInterval: `${config.otlpExportIntervalMs} millis`,21 exportInterval: `${logs.exportIntervalMs} millis`,
21 headers: config.otlpHeaders,22 headers: logs.headers,
22 resource: otlpResource(config),23 resource: otlpResource(config),
23 });24 });
2425
...@@ -41,7 +42,7 @@ export const ServerLoggerLive = Effect.gen(function* () {...@@ -41,7 +42,7 @@ export const ServerLoggerLive = Effect.gen(function* () {
41 { mergeWithExisting: false },42 { mergeWithExisting: false },
42 ).pipe(43 ).pipe(
43 Layer.provide(OtlpExporter.layerFlusher),44 Layer.provide(OtlpExporter.layerFlusher),
44 Layer.provide(otlpSerializationLayer(config.otlpProtocol)),45 Layer.provide(otlpSerializationLayer(logs.protocol)),
45 );46 );
4647
47 return Layer.mergeAll(loggerLayer, minimumLogLevelLayer);48 return Layer.mergeAll(loggerLayer, minimumLogLevelLayer);
packages/shared/src/observability.ts+19
...@@ -16,6 +16,25 @@ export type OtlpProtocol = typeof OtlpProtocol.Type;...@@ -16,6 +16,25 @@ export type OtlpProtocol = typeof OtlpProtocol.Type;
16export const otlpSerializationLayer = (protocol: OtlpProtocol) =>16export const otlpSerializationLayer = (protocol: OtlpProtocol) =>
17 protocol === "http/protobuf" ? OtlpSerialization.layerProtobuf : OtlpSerialization.layerJson;17 protocol === "http/protobuf" ? OtlpSerialization.layerProtobuf : OtlpSerialization.layerJson;
1818
19/**
20 * How one signal is exported, once whichever source named that signal's
21 * endpoint has been resolved. Held per signal rather than per process, so a
22 * wire format or a credential cannot be paired by hand with an endpoint that
23 * came from somewhere else.
24 */
25export interface SignalExport {
26 readonly protocol: OtlpProtocol;
27 readonly headers: Readonly<Record<string, string>> | undefined;
28 readonly exportIntervalMs: number;
29}
30
31/** What T3 Code exports with when nothing configured a signal. */
32export const DEFAULT_SIGNAL_EXPORT: SignalExport = {
33 protocol: "http/json",
34 headers: undefined,
35 exportIntervalMs: 10_000,
36};
37
19const FLUSH_BUFFER_THRESHOLD = 256;38const FLUSH_BUFFER_THRESHOLD = 256;
20const textEncoder = new TextEncoder();39const textEncoder = new TextEncoder();
2140