authorgravatar for yordis.prieto@gmail.comYordis Prieto <yordis.prieto@gmail.com> 2026-09-18 18:55:15-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-18 15:55:15-07:00
log3bb06ad910389fcfc886bd5156f41dc73f307f80
tree73176194566be393263a33233f6b47f1a141c7d4
parent82cd1d1aabe8baa4a5ae23f3c7b08b3fce092e08
signature Signed by PGP key B5690EEEBB952194

feat(server): export log records over OTLP (#12493)

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

25 files changed, 503 insertions(+), 26 deletions(-)

apps/desktop/src/app/DesktopConfig.ts+2
......@@ -46,6 +46,8 @@ export const DesktopConfig = Config.all({
4646 desktopLanHostOverride: trimmedString("T3CODE_DESKTOP_LAN_HOST"),
4747 desktopHttpsEndpointUrls: commaSeparatedStrings("T3CODE_DESKTOP_HTTPS_ENDPOINTS"),
4848 otlpTracesUrl: trimmedString("T3CODE_OTLP_TRACES_URL"),
49 otlpMetricsUrl: trimmedString("T3CODE_OTLP_METRICS_URL"),
50 otlpLogsUrl: trimmedString("T3CODE_OTLP_LOGS_URL"),
4951 otlpExportIntervalMs: Config.Int("T3CODE_OTLP_EXPORT_INTERVAL_MS").pipe(
5052 Config.withDefault(10_000),
5153 ),
apps/desktop/src/app/DesktopEnvironment.test.ts+4
......@@ -51,6 +51,8 @@ describe("DesktopEnvironment", () => {
5151 VITE_DEV_SERVER_URL: "http://localhost:5173",
5252 T3CODE_DEV_REMOTE_T3_SERVER_ENTRY_PATH: " /remote/server.mjs ",
5353 T3CODE_OTLP_TRACES_URL: " http://127.0.0.1:4318/v1/traces ",
54 T3CODE_OTLP_METRICS_URL: " http://127.0.0.1:4318/v1/metrics ",
55 T3CODE_OTLP_LOGS_URL: " http://127.0.0.1:4318/v1/logs ",
5456 T3CODE_OTLP_EXPORT_INTERVAL_MS: "2500",
5557 T3CODE_OTLP_HEADERS: "authorization=Basic%20abc%3D%3D,x-tenant=t3",
5658 T3CODE_OTLP_PROTOCOL: "http/protobuf",
......@@ -86,6 +88,8 @@ describe("DesktopEnvironment", () => {
8688 assert.deepEqual(environment.configuredBackendPort, Option.some(4949));
8789 assert.deepEqual(environment.commitHashOverride, Option.some("0123456789abcdef"));
8890 assert.deepEqual(environment.otlpTracesUrl, Option.some("http://127.0.0.1:4318/v1/traces"));
91 assert.deepEqual(environment.otlpMetricsUrl, Option.some("http://127.0.0.1:4318/v1/metrics"));
92 assert.deepEqual(environment.otlpLogsUrl, Option.some("http://127.0.0.1:4318/v1/logs"));
8993 assert.equal(environment.otlpExportIntervalMs, 2500);
9094 assert.deepEqual(
9195 environment.otlpHeaders,
apps/desktop/src/app/DesktopEnvironment.ts+4
......@@ -72,6 +72,8 @@ export class DesktopEnvironment extends Context.Service<
7272 readonly configuredBackendPort: Option.Option<number>;
7373 readonly commitHashOverride: Option.Option<string>;
7474 readonly otlpTracesUrl: Option.Option<string>;
75 readonly otlpMetricsUrl: Option.Option<string>;
76 readonly otlpLogsUrl: Option.Option<string>;
7577 readonly otlpExportIntervalMs: number;
7678 readonly otlpHeaders: Option.Option<Record<string, string>>;
7779 readonly otlpProtocol: OtlpProtocol;
......@@ -227,6 +229,8 @@ const make = Effect.fn("desktop.environment.make")(function* (
227229 configuredBackendPort: config.configuredBackendPort,
228230 commitHashOverride: config.commitHashOverride,
229231 otlpTracesUrl: config.otlpTracesUrl,
232 otlpMetricsUrl: config.otlpMetricsUrl,
233 otlpLogsUrl: config.otlpLogsUrl,
230234 otlpExportIntervalMs: config.otlpExportIntervalMs,
231235 otlpHeaders: config.otlpHeaders,
232236 otlpProtocol: config.otlpProtocol,
apps/desktop/src/backend/DesktopBackendConfiguration.test.ts+103
......@@ -23,6 +23,7 @@ const PersistedServerObservabilitySettingsDocument = Schema.Struct({
2323 observability: Schema.Struct({
2424 otlpTracesUrl: Schema.String,
2525 otlpMetricsUrl: Schema.String,
26 otlpLogsUrl: Schema.String,
2627 }),
2728});
2829
......@@ -60,6 +61,9 @@ function makeEnvironmentLayer(
6061 readonly resourcesPath?: string;
6162 readonly appVersion?: string;
6263 readonly processArch?: NodeJS.Architecture;
64 readonly otlpTracesUrl?: string;
65 readonly otlpMetricsUrl?: string;
66 readonly otlpLogsUrl?: string;
6367 },
6468) {
6569 return DesktopEnvironment.layer({
......@@ -82,6 +86,9 @@ function makeEnvironmentLayer(
8286 T3CODE_MODE: "desktop",
8387 T3CODE_DESKTOP_LAN_HOST: "192.168.1.50",
8488 VITE_DEV_SERVER_URL: options?.devServerUrl,
89 T3CODE_OTLP_TRACES_URL: options?.otlpTracesUrl,
90 T3CODE_OTLP_METRICS_URL: options?.otlpMetricsUrl,
91 T3CODE_OTLP_LOGS_URL: options?.otlpLogsUrl,
8592 }),
8693 ),
8794 ),
......@@ -730,6 +737,7 @@ describe("DesktopBackendConfiguration", () => {
730737 observability: {
731738 otlpTracesUrl: " http://127.0.0.1:4318/v1/traces ",
732739 otlpMetricsUrl: " http://127.0.0.1:4318/v1/metrics ",
740 otlpLogsUrl: " http://127.0.0.1:4318/v1/logs ",
733741 },
734742 }),
735743 );
......@@ -737,6 +745,7 @@ describe("DesktopBackendConfiguration", () => {
737745 const config = yield* configuration.resolvePrimary;
738746 assert.equal(config.bootstrap.otlpTracesUrl, "http://127.0.0.1:4318/v1/traces");
739747 assert.equal(config.bootstrap.otlpMetricsUrl, "http://127.0.0.1:4318/v1/metrics");
748 assert.equal(config.bootstrap.otlpLogsUrl, "http://127.0.0.1:4318/v1/logs");
740749 }),
741750 ),
742751 );
......@@ -749,10 +758,103 @@ describe("DesktopBackendConfiguration", () => {
749758
750759 assert.isUndefined(config.bootstrap.otlpTracesUrl);
751760 assert.isUndefined(config.bootstrap.otlpMetricsUrl);
761 assert.isUndefined(config.bootstrap.otlpLogsUrl);
752762 }),
753763 ),
754764 );
755765
766 it.effect("resolveWsl carries environment-configured observability endpoints", () =>
767 Effect.gen(function* () {
768 const fileSystem = yield* FileSystem.FileSystem;
769 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
770 prefix: "t3-desktop-backend-config-test-",
771 });
772
773 yield* Effect.gen(function* () {
774 const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration;
775 const config = yield* configuration.resolveWsl({ port: 5050, distro: null });
776
777 // No settings.json exists here: the endpoints come from the desktop
778 // process's env, which a WSL child cannot inherit, so the bootstrap
779 // has to carry them or log export stays off inside the distro.
780 assert.equal(config.bootstrap.otlpTracesUrl, "http://127.0.0.1:4318/v1/traces");
781 assert.equal(config.bootstrap.otlpMetricsUrl, "http://127.0.0.1:4318/v1/metrics");
782 assert.equal(config.bootstrap.otlpLogsUrl, "http://127.0.0.1:4318/v1/logs");
783 assert.notInclude(config.env.WSLENV ?? "", "T3CODE_OTLP_LOGS_URL");
784 }).pipe(
785 Effect.provide(
786 DesktopBackendConfiguration.layer.pipe(
787 Layer.provideMerge(serverExposureLayer),
788 Layer.provideMerge(DesktopAppSettings.layerTest()),
789 Layer.provideMerge(DesktopWslServerTree.layerTest()),
790 Layer.provideMerge(
791 DesktopWslEnvironment.layerTest({
792 isAvailable: true,
793 windowsToWslPath: () => Option.some("/mnt/c/repo/apps/server/src/index.ts"),
794 getDistroIp: () => Option.some("172.27.0.99"),
795 }),
796 ),
797 Layer.provideMerge(
798 makeEnvironmentLayer(baseDir, {
799 platform: "win32",
800 otlpTracesUrl: " http://127.0.0.1:4318/v1/traces ",
801 otlpMetricsUrl: " http://127.0.0.1:4318/v1/metrics ",
802 otlpLogsUrl: " http://127.0.0.1:4318/v1/logs ",
803 }),
804 ),
805 ),
806 ),
807 );
808 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
809 );
810
811 it.effect("environment observability endpoints win over the persisted settings file", () =>
812 Effect.gen(function* () {
813 const fileSystem = yield* FileSystem.FileSystem;
814 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
815 prefix: "t3-desktop-backend-config-test-",
816 });
817
818 yield* Effect.gen(function* () {
819 const environment = yield* DesktopEnvironment.DesktopEnvironment;
820 const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration;
821
822 yield* fileSystem.makeDirectory(environment.path.dirname(environment.serverSettingsPath), {
823 recursive: true,
824 });
825 yield* fileSystem.writeFileString(
826 environment.serverSettingsPath,
827 yield* encodePersistedServerObservabilitySettingsDocument({
828 observability: {
829 otlpTracesUrl: "http://persisted:4318/v1/traces",
830 otlpMetricsUrl: "http://persisted:4318/v1/metrics",
831 otlpLogsUrl: "http://persisted:4318/v1/logs",
832 },
833 }),
834 );
835
836 const config = yield* configuration.resolvePrimary;
837 assert.equal(config.bootstrap.otlpLogsUrl, "http://env:4318/v1/logs");
838 // Only the logs endpoint is set in env, so the other two still come
839 // from the settings file rather than being dropped together.
840 assert.equal(config.bootstrap.otlpTracesUrl, "http://persisted:4318/v1/traces");
841 assert.equal(config.bootstrap.otlpMetricsUrl, "http://persisted:4318/v1/metrics");
842 }).pipe(
843 Effect.provide(
844 DesktopBackendConfiguration.layer.pipe(
845 Layer.provideMerge(serverExposureLayer),
846 Layer.provideMerge(DesktopAppSettings.layerTest()),
847 Layer.provideMerge(DesktopWslServerTree.layerTest()),
848 Layer.provideMerge(DesktopWslEnvironment.layerTest()),
849 Layer.provideMerge(
850 makeEnvironmentLayer(baseDir, { otlpLogsUrl: "http://env:4318/v1/logs" }),
851 ),
852 ),
853 ),
854 );
855 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
856 );
857
756858 it.effect("logs structured context when persisted observability settings cannot be read", () =>
757859 Effect.gen(function* () {
758860 const fileSystem = yield* FileSystem.FileSystem;
......@@ -799,6 +901,7 @@ describe("DesktopBackendConfiguration", () => {
799901
800902 assert.isUndefined(config.bootstrap.otlpTracesUrl);
801903 assert.isUndefined(config.bootstrap.otlpMetricsUrl);
904 assert.isUndefined(config.bootstrap.otlpLogsUrl);
802905
803906 const error = messages
804907 .flatMap((message) => (Array.isArray(message) ? message : [message]))
apps/desktop/src/backend/DesktopBackendConfiguration.ts+24-1
......@@ -67,11 +67,13 @@ export class DesktopBackendConfiguration extends Context.Service<
6767interface BackendObservabilitySettings {
6868 readonly otlpTracesUrl: Option.Option<string>;
6969 readonly otlpMetricsUrl: Option.Option<string>;
70 readonly otlpLogsUrl: Option.Option<string>;
7071}
7172
7273const emptyBackendObservabilitySettings: BackendObservabilitySettings = {
7374 otlpTracesUrl: Option.none(),
7475 otlpMetricsUrl: Option.none(),
76 otlpLogsUrl: Option.none(),
7577};
7678
7779const DESKTOP_BACKEND_ENV_NAMES = [
......@@ -212,9 +214,26 @@ const readPersistedBackendObservabilitySettings = Effect.gen(function* () {
212214 return {
213215 otlpTracesUrl: Option.fromNullishOr(parsed.otlpTracesUrl),
214216 otlpMetricsUrl: Option.fromNullishOr(parsed.otlpMetricsUrl),
217 otlpLogsUrl: Option.fromNullishOr(parsed.otlpLogsUrl),
215218 };
216219});
217220
221// The bootstrap is the only channel that carries an OTLP endpoint to every
222// backend. A Windows-native child inherits the desktop process's env, but a
223// WSL child gets nothing across wsl.exe that WSLENV does not declare, and
224// WSLENV translation of URL-shaped values is unreliable, so the endpoints are
225// deliberately not forwarded that way. Env beats the persisted settings file,
226// matching the precedence resolveServerConfig and DesktopObservability apply.
227const readBackendObservabilitySettings = Effect.gen(function* () {
228 const environment = yield* DesktopEnvironment.DesktopEnvironment;
229 const persisted = yield* readPersistedBackendObservabilitySettings;
230 return {
231 otlpTracesUrl: Option.orElse(environment.otlpTracesUrl, () => persisted.otlpTracesUrl),
232 otlpMetricsUrl: Option.orElse(environment.otlpMetricsUrl, () => persisted.otlpMetricsUrl),
233 otlpLogsUrl: Option.orElse(environment.otlpLogsUrl, () => persisted.otlpLogsUrl),
234 } satisfies BackendObservabilitySettings;
235});
236
218237interface SharedBootstrapInput {
219238 readonly bootstrapToken: string;
220239 readonly observabilitySettings: BackendObservabilitySettings;
......@@ -488,6 +507,10 @@ const buildObservabilityFragment = (observabilitySettings: BackendObservabilityS
488507 onNone: () => ({}),
489508 onSome: (otlpMetricsUrl) => ({ otlpMetricsUrl }),
490509 }),
510 ...Option.match(observabilitySettings.otlpLogsUrl, {
511 onNone: () => ({}),
512 onSome: (otlpLogsUrl) => ({ otlpLogsUrl }),
513 }),
491514});
492515
493516const resolvePrimaryStartConfig = Effect.fn("desktop.backendConfiguration.resolvePrimary")(
......@@ -804,7 +827,7 @@ export const make = Effect.gen(function* () {
804827 // restart cycle without having to bounce the desktop process.
805828 const sharedInputs = Effect.gen(function* () {
806829 const bootstrapToken = yield* getOrCreateBootstrapToken;
807 const observabilitySettings = yield* readPersistedBackendObservabilitySettings.pipe(
830 const observabilitySettings = yield* readBackendObservabilitySettings.pipe(
808831 Effect.provideService(FileSystem.FileSystem, fileSystem),
809832 Effect.provideService(DesktopEnvironment.DesktopEnvironment, environment),
810833 );
apps/server/src/bin.test.ts+1
......@@ -100,6 +100,7 @@ const makeCliTestServerConfig = (baseDir: string) =>
100100 traceMaxFiles: 10,
101101 otlpTracesUrl: undefined,
102102 otlpMetricsUrl: undefined,
103 otlpLogsUrl: undefined,
103104 otlpExportIntervalMs: 10_000,
104105 otlpServiceName: "t3-server",
105106 otlpHeaders: undefined,
apps/server/src/cli/config.test.ts+44
......@@ -50,6 +50,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
5050 traceMaxFiles: 10,
5151 otlpTracesUrl: undefined,
5252 otlpMetricsUrl: undefined,
53 otlpLogsUrl: undefined,
5354 otlpExportIntervalMs: 10_000,
5455 otlpServiceName: "t3-server",
5556 otlpHeaders: undefined,
......@@ -398,6 +399,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
398399 tailscaleServePort: 443,
399400 otlpTracesUrl: "http://localhost:4318/v1/traces",
400401 otlpMetricsUrl: "http://localhost:4318/v1/metrics",
402 otlpLogsUrl: "http://localhost:4318/v1/logs",
401403 }),
402404 );
403405 const derivedPaths = yield* deriveServerPaths(baseDir, undefined);
......@@ -438,6 +440,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
438440 ...defaultObservabilityConfig,
439441 otlpTracesUrl: "http://localhost:4318/v1/traces",
440442 otlpMetricsUrl: "http://localhost:4318/v1/metrics",
443 otlpLogsUrl: "http://localhost:4318/v1/logs",
441444 mode: "desktop",
442445 port: 4888,
443446 cwd: process.cwd(),
......@@ -604,6 +607,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
604607 observability: {
605608 otlpTracesUrl: "http://localhost:4318/v1/traces",
606609 otlpMetricsUrl: "http://localhost:4318/v1/metrics",
610 otlpLogsUrl: "http://localhost:4318/v1/logs",
607611 },
608612 })}\n`,
609613 );
......@@ -635,11 +639,13 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
635639
636640 expect(resolved.otlpTracesUrl).toBe("http://localhost:4318/v1/traces");
637641 expect(resolved.otlpMetricsUrl).toBe("http://localhost:4318/v1/metrics");
642 expect(resolved.otlpLogsUrl).toBe("http://localhost:4318/v1/logs");
638643 expect(resolved).toEqual({
639644 logLevel: "Info",
640645 ...defaultObservabilityConfig,
641646 otlpTracesUrl: "http://localhost:4318/v1/traces",
642647 otlpMetricsUrl: "http://localhost:4318/v1/metrics",
648 otlpLogsUrl: "http://localhost:4318/v1/logs",
643649 mode: "desktop",
644650 port: 4888,
645651 cwd: process.cwd(),
......@@ -845,4 +851,42 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
845851 expect(resolved.otlpProtocol).toBe("http/protobuf");
846852 }),
847853 );
854
855 it.effect("reads the OTLP logs URL from env", () =>
856 Effect.gen(function* () {
857 const { join } = yield* Path.Path;
858 const baseDir = join(NodeOS.tmpdir(), "t3-cli-config-otlp-logs-url-base");
859
860 const resolved = yield* resolveServerConfig(
861 {
862 mode: Option.some("web"),
863 port: Option.some(3773),
864 host: Option.none(),
865 baseDir: Option.some(baseDir),
866 cwd: Option.none(),
867 devUrl: Option.none(),
868 noBrowser: Option.none(),
869 bootstrapFd: Option.none(),
870 autoBootstrapProjectFromCwd: Option.none(),
871 logWebSocketEvents: Option.none(),
872 tailscaleServeEnabled: Option.none(),
873 tailscaleServePort: Option.none(),
874 },
875 Option.none(),
876 ).pipe(
877 Effect.provide(
878 Layer.mergeAll(
879 ConfigProvider.layer(
880 ConfigProvider.fromEnv({
881 env: { T3CODE_OTLP_LOGS_URL: "http://collector.internal:4318/v1/logs" },
882 }),
883 ),
884 NetService.layer,
885 ),
886 ),
887 );
888
889 expect(resolved.otlpLogsUrl).toBe("http://collector.internal:4318/v1/logs");
890 }),
891 );
848892});
apps/server/src/cli/config.ts+7-1
......@@ -96,6 +96,10 @@ const EnvServerConfig = Config.all({
9696 Config.option,
9797 Config.map(Option.getOrUndefined),
9898 ),
99 otlpLogsUrl: Config.String("T3CODE_OTLP_LOGS_URL").pipe(
100 Config.option,
101 Config.map(Option.getOrUndefined),
102 ),
99103 otlpExportIntervalMs: Config.Int("T3CODE_OTLP_EXPORT_INTERVAL_MS").pipe(
100104 Config.withDefault(10_000),
101105 ),
......@@ -227,7 +231,7 @@ const loadPersistedObservabilitySettings = Effect.fn(function* (settingsPath: st
227231 const fs = yield* FileSystem.FileSystem;
228232 const exists = yield* fs.exists(settingsPath).pipe(Effect.orElseSucceed(() => false));
229233 if (!exists) {
230 return { otlpTracesUrl: undefined, otlpMetricsUrl: undefined };
234 return { otlpTracesUrl: undefined, otlpMetricsUrl: undefined, otlpLogsUrl: undefined };
231235 }
232236
233237 const raw = yield* fs.readFileString(settingsPath).pipe(Effect.orElseSucceed(() => ""));
......@@ -393,6 +397,8 @@ export const resolveServerConfig = (
393397 env.otlpMetricsUrl ??
394398 bootstrap?.otlpMetricsUrl ??
395399 persistedObservabilitySettings.otlpMetricsUrl,
400 otlpLogsUrl:
401 env.otlpLogsUrl ?? bootstrap?.otlpLogsUrl ?? persistedObservabilitySettings.otlpLogsUrl,
396402 otlpExportIntervalMs: env.otlpExportIntervalMs,
397403 otlpServiceName: env.otlpServiceName,
398404 otlpHeaders: env.otlpHeaders,
apps/server/src/cli/pair.ts+1
......@@ -320,6 +320,7 @@ const makePairServerConfig = Effect.fn(function* (input: {
320320 traceMaxFiles: 10,
321321 otlpTracesUrl: undefined,
322322 otlpMetricsUrl: undefined,
323 otlpLogsUrl: undefined,
323324 otlpExportIntervalMs: 10_000,
324325 otlpServiceName: "t3-server",
325326 otlpHeaders: undefined,
apps/server/src/config.ts+14
......@@ -72,6 +72,7 @@ export class ServerConfig extends Context.Service<
7272 readonly traceMaxFiles: number;
7373 readonly otlpTracesUrl: string | undefined;
7474 readonly otlpMetricsUrl: string | undefined;
75 readonly otlpLogsUrl: string | undefined;
7576 readonly otlpExportIntervalMs: number;
7677 readonly otlpServiceName: string;
7778 readonly otlpHeaders: Readonly<Record<string, string>> | undefined;
......@@ -106,6 +107,18 @@ export class ServerConfig extends Context.Service<
106107
107108export const make = (config: ServerConfig["Service"]) => ServerConfig.of(config);
108109
110/**
111 * Resource attributes shared by every OTLP exporter, so traces, metrics, and
112 * logs report the same service identity to the collector.
113 */
114export const otlpResource = (config: ServerConfig["Service"]) => ({
115 serviceName: config.otlpServiceName,
116 attributes: {
117 "service.runtime": "t3-server",
118 "service.mode": config.mode,
119 },
120});
121
109122export const layer = (config: ServerConfig["Service"]) => Layer.succeed(ServerConfig, make(config));
110123
111124export const deriveServerPaths = Effect.fn(function* (
......@@ -198,6 +211,7 @@ const makeTest = Effect.fn("ServerConfig.makeTest")(function* (
198211 traceMaxFiles: 10,
199212 otlpTracesUrl: undefined,
200213 otlpMetricsUrl: undefined,
214 otlpLogsUrl: undefined,
201215 otlpExportIntervalMs: 10_000,
202216 otlpServiceName: "t3-server",
203217 otlpHeaders: undefined,
apps/server/src/environment/ServerEnvironment.test.ts+1
......@@ -53,6 +53,7 @@ const makeServerConfig = Effect.fn(function* (baseDir: string) {
5353 traceMaxFiles: 10,
5454 otlpTracesUrl: undefined,
5555 otlpMetricsUrl: undefined,
56 otlpLogsUrl: undefined,
5657 otlpExportIntervalMs: 10_000,
5758 otlpServiceName: "t3-server",
5859 otlpHeaders: undefined,
apps/server/src/observability/Layers/Observability.ts+3-14
......@@ -21,6 +21,7 @@ export const ObservabilityLive = Layer.unwrap(
2121 Effect.gen(function* () {
2222 const config = yield* ServerConfig.ServerConfig;
2323 const serializationLayer = otlpSerializationLayer(config.otlpProtocol);
24 const resource = ServerConfig.otlpResource(config);
2425 const attribution = yield* ResourceAttribution.ResourceAttribution;
2526
2627 const traceReferencesLayer = Layer.mergeAll(
......@@ -52,13 +53,7 @@ export const ObservabilityLive = Layer.unwrap(
5253 url: config.otlpTracesUrl,
5354 exportInterval: `${config.otlpExportIntervalMs} millis`,
5455 headers: config.otlpHeaders,
55 resource: {
56 serviceName: config.otlpServiceName,
57 attributes: {
58 "service.runtime": "t3-server",
59 "service.mode": config.mode,
60 },
61 },
56 resource,
6257 });
6358
6459 const tracer = yield* makeLocalFileTracer({
......@@ -84,13 +79,7 @@ export const ObservabilityLive = Layer.unwrap(
8479 url: config.otlpMetricsUrl,
8580 exportInterval: `${config.otlpExportIntervalMs} millis`,
8681 headers: config.otlpHeaders,
87 resource: {
88 serviceName: config.otlpServiceName,
89 attributes: {
90 "service.runtime": "t3-server",
91 "service.mode": config.mode,
92 },
93 },
82 resource,
9483 }).pipe(Layer.provideMerge(serializationLayer));
9584
9685 return Layer.mergeAll(ServerLoggerLive, traceReferencesLayer, tracerLayer, metricsLayer);
apps/server/src/server.test.ts+4
......@@ -580,6 +580,7 @@ const buildAppUnderTest = (options?: {
580580 traceMaxFiles: 10,
581581 otlpTracesUrl: undefined,
582582 otlpMetricsUrl: undefined,
583 otlpLogsUrl: undefined,
583584 otlpExportIntervalMs: 10_000,
584585 otlpServiceName: "t3-server",
585586 otlpHeaders: undefined,
......@@ -6291,6 +6292,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
62916292 config: {
62926293 otlpTracesUrl: "http://localhost:4318/v1/traces",
62936294 otlpMetricsUrl: "http://localhost:4318/v1/metrics",
6295 otlpLogsUrl: "http://localhost:4318/v1/logs",
62946296 },
62956297 layers: {
62966298 keybindings: {
......@@ -6326,6 +6328,8 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
63266328 assert.equal(first.config.observability.otlpTracesEnabled, true);
63276329 assert.equal(first.config.observability.otlpMetricsUrl, "http://localhost:4318/v1/metrics");
63286330 assert.equal(first.config.observability.otlpMetricsEnabled, true);
6331 assert.equal(first.config.observability.otlpLogsUrl, "http://localhost:4318/v1/logs");
6332 assert.equal(first.config.observability.otlpLogsEnabled, true);
63296333 assert.deepEqual(first.config.settings, DEFAULT_SERVER_SETTINGS);
63306334 }
63316335 assert.deepEqual(second, {
apps/server/src/serverLogger.test.ts created+197
......@@ -0,0 +1,197 @@
1import * as NodePath from "@effect/platform-node/NodePath";
2import { assert, describe, it } from "@effect/vitest";
3import * as NodeOS from "node:os";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Path from "effect/Path";
7import * as Tracer from "effect/Tracer";
8import * as HttpClient from "effect/unstable/http/HttpClient";
9import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
10
11import * as ServerConfig from "./config.ts";
12import { ServerLoggerLive } from "./serverLogger.ts";
13
14interface ExportedRequest {
15 readonly url: string;
16 readonly headers: Readonly<Record<string, string>>;
17 readonly body: string;
18}
19
20/** Answers every export with a 200 and keeps what was posted for assertions. */
21const collectorLayer = (requests: Array<ExportedRequest>) =>
22 Layer.succeed(
23 HttpClient.HttpClient,
24 HttpClient.make((request) =>
25 Effect.sync(() => {
26 requests.push({
27 url: request.url,
28 headers: request.headers,
29 body:
30 request.body._tag === "Uint8Array" ? new TextDecoder().decode(request.body.body) : "",
31 });
32 return HttpClientResponse.fromWeb(request, new Response(null, { status: 200 }));
33 }),
34 ),
35 );
36
37const configLayer = (overrides: Partial<ServerConfig.ServerConfig["Service"]>) =>
38 Layer.effect(
39 ServerConfig.ServerConfig,
40 Effect.gen(function* () {
41 const path = yield* Path.Path;
42 const baseDir = path.join(NodeOS.tmpdir(), "t3-server-logger-test");
43 const derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, undefined);
44 return ServerConfig.make({
45 logLevel: "Info",
46 traceMinLevel: "Info",
47 traceTimingEnabled: false,
48 traceBatchWindowMs: 200,
49 traceMaxBytes: 1024,
50 traceMaxFiles: 1,
51 otlpTracesUrl: undefined,
52 otlpMetricsUrl: undefined,
53 otlpLogsUrl: undefined,
54 otlpExportIntervalMs: 10_000,
55 otlpServiceName: "t3-server",
56 otlpHeaders: undefined,
57 otlpProtocol: "http/json",
58 cwd: baseDir,
59 baseDir,
60 ...derivedPaths,
61 mode: "web",
62 autoBootstrapProjectFromCwd: false,
63 logWebSocketEvents: false,
64 tailscaleServeEnabled: false,
65 tailscaleServePort: 443,
66 port: 0,
67 host: undefined,
68 desktopBootstrapToken: undefined,
69 desktopTelemetryFd: undefined,
70 desktopTelemetryControlFd: undefined,
71 resourceMonitorPath: undefined,
72 staticDir: undefined,
73 devUrl: undefined,
74 devAllowedOrigins: [],
75 noBrowser: false,
76 startupPresentation: "browser",
77 ...overrides,
78 });
79 }),
80 ).pipe(Layer.provide(NodePath.layer));
81
82/**
83 * Logs once with the server's own logger set installed, then reports what the
84 * collector received. The export is asserted after the layer's scope closes,
85 * which is where the exporter flushes whatever the interval did not.
86 */
87const logThrough = (overrides: Partial<ServerConfig.ServerConfig["Service"]>) =>
88 Effect.gen(function* () {
89 const requests: Array<ExportedRequest> = [];
90 yield* Effect.log("server logger under test").pipe(
91 Effect.provide(
92 ServerLoggerLive.pipe(
93 Layer.provide(configLayer(overrides)),
94 Layer.provide(collectorLayer(requests)),
95 ),
96 ),
97 );
98 return requests;
99 });
100
101/**
102 * Logs inside a span so the tracer logger has somewhere to attach an event,
103 * then reports both what the collector received and what landed on the span.
104 */
105const logInSpanThrough = (overrides: Partial<ServerConfig.ServerConfig["Service"]>) =>
106 Effect.gen(function* () {
107 const requests: Array<ExportedRequest> = [];
108 const spans: Array<Tracer.NativeSpan> = [];
109 const tracerLayer = Layer.succeed(
110 Tracer.Tracer,
111 Tracer.make({
112 span: (spanOptions) => {
113 const span = new Tracer.NativeSpan(spanOptions);
114 spans.push(span);
115 return span;
116 },
117 }),
118 );
119 yield* Effect.log("server logger under test").pipe(
120 Effect.withSpan("server-logger-test"),
121 Effect.provide(
122 Layer.mergeAll(
123 ServerLoggerLive.pipe(
124 Layer.provide(configLayer(overrides)),
125 Layer.provide(collectorLayer(requests)),
126 ),
127 tracerLayer,
128 ),
129 ),
130 );
131 return { requests, spans };
132 });
133
134describe("ServerLoggerLive", () => {
135 it.effect("exports log records to the configured logs endpoint", () =>
136 Effect.gen(function* () {
137 const requests = yield* logThrough({
138 otlpLogsUrl: "https://collector.example.com/v1/logs",
139 });
140
141 assert.lengthOf(requests, 1);
142 const [request] = requests;
143 assert.strictEqual(request?.url, "https://collector.example.com/v1/logs");
144 assert.include(request?.body ?? "", "server logger under test");
145 assert.include(request?.body ?? "", "t3-server");
146 assert.include(request?.body ?? "", "service.runtime");
147 }),
148 );
149
150 it.effect("stays off the network when no logs endpoint is configured", () =>
151 Effect.gen(function* () {
152 const requests = yield* logThrough({});
153
154 assert.lengthOf(requests, 0);
155 }),
156 );
157
158 it.effect("sends the headers and wire format the rest of OTLP export already uses", () =>
159 Effect.gen(function* () {
160 const requests = yield* logThrough({
161 otlpLogsUrl: "https://collector.example.com/v1/logs",
162 otlpProtocol: "http/protobuf",
163 otlpHeaders: { "x-scope": "logs" },
164 });
165
166 assert.lengthOf(requests, 1);
167 assert.strictEqual(requests[0]?.headers["x-scope"], "logs");
168 assert.strictEqual(requests[0]?.headers["content-type"], "application/x-protobuf");
169 }),
170 );
171
172 it.effect("attaches log messages to the active span when no logs endpoint is configured", () =>
173 Effect.gen(function* () {
174 const { requests, spans } = yield* logInSpanThrough({});
175
176 assert.lengthOf(requests, 0);
177 assert.lengthOf(spans, 1);
178 assert.deepEqual(
179 spans[0]?.events.map(([name]) => name),
180 ["server logger under test"],
181 );
182 }),
183 );
184
185 it.effect("stops duplicating messages onto the span once log records are exported", () =>
186 Effect.gen(function* () {
187 const { requests, spans } = yield* logInSpanThrough({
188 otlpLogsUrl: "https://collector.example.com/v1/logs",
189 });
190
191 assert.lengthOf(requests, 1);
192 assert.include(requests[0]?.body ?? "", "server logger under test");
193 assert.lengthOf(spans, 1);
194 assert.lengthOf(spans[0]?.events ?? [], 0);
195 }),
196 );
197});
apps/server/src/serverLogger.ts+37-5
......@@ -1,16 +1,48 @@
1import { otlpSerializationLayer } from "@t3tools/shared/observability";
12import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
24import * as Logger from "effect/Logger";
35import * as References from "effect/References";
4import * as Layer from "effect/Layer";
6import * as OtlpExporter from "effect/unstable/observability/OtlpExporter";
7import * as OtlpLogger from "effect/unstable/observability/OtlpLogger";
58
6import { ServerConfig } from "./config.ts";
9import { otlpResource, ServerConfig } from "./config.ts";
710
811export const ServerLoggerLive = Effect.gen(function* () {
912 const config = yield* ServerConfig;
1013 const minimumLogLevelLayer = Layer.succeed(References.MinimumLogLevel, config.logLevel);
11 const loggerLayer = Logger.layer([Logger.consolePretty(), Logger.tracerLogger], {
12 mergeWithExisting: false,
13 });
14
15 const otlpLogger =
16 config.otlpLogsUrl === undefined
17 ? undefined
18 : OtlpLogger.make({
19 url: config.otlpLogsUrl,
20 exportInterval: `${config.otlpExportIntervalMs} millis`,
21 headers: config.otlpHeaders,
22 resource: otlpResource(config),
23 });
24
25 // `Logger.layer` writes the whole logger set rather than adding to it, so
26 // every logger the server wants has to be named in this one call.
27 //
28 // `Logger.tracerLogger` reaches a collector by attaching each message to the
29 // active span as a span event, which covers only messages logged inside a
30 // recorded span and files them under traces. The OTLP logger carries the same
31 // messages as log records stamped with their trace and span ids, so it is a
32 // superset: keeping both would export every in-span message twice.
33 //
34 // Recording events on spans is also the shape OpenTelemetry is deprecating,
35 // in favor of the log-based events this logger emits:
36 // https://opentelemetry.io/blog/2026/deprecating-span-events/
37 const loggerLayer = Logger.layer(
38 otlpLogger === undefined
39 ? [Logger.consolePretty(), Logger.tracerLogger]
40 : [Logger.consolePretty(), otlpLogger],
41 { mergeWithExisting: false },
42 ).pipe(
43 Layer.provide(OtlpExporter.layerFlusher),
44 Layer.provide(otlpSerializationLayer(config.otlpProtocol)),
45 );
1446
1547 return Layer.mergeAll(loggerLayer, minimumLogLevelLayer);
1648}).pipe(Layer.unwrap);
apps/server/src/serverSettings.test.ts+2
......@@ -969,6 +969,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
969969 observability: {
970970 otlpTracesUrl: " http://localhost:4318/v1/traces ",
971971 otlpMetricsUrl: " http://localhost:4318/v1/metrics ",
972 otlpLogsUrl: " http://localhost:4318/v1/logs ",
972973 },
973974 });
974975
......@@ -976,6 +977,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
976977 assert.deepEqual(next.observability, {
977978 otlpTracesUrl: "http://localhost:4318/v1/traces",
978979 otlpMetricsUrl: "http://localhost:4318/v1/metrics",
980 otlpLogsUrl: "http://localhost:4318/v1/logs",
979981 });
980982 }).pipe(Effect.provide(makeServerSettingsLayer())),
981983 );
apps/server/src/ws.ts+2
......@@ -1808,6 +1808,8 @@ const makeWsRpcLayer = (
18081808 ? { otlpMetricsUrl: config.otlpMetricsUrl }
18091809 : {}),
18101810 otlpMetricsEnabled: config.otlpMetricsUrl !== undefined,
1811 ...(config.otlpLogsUrl !== undefined ? { otlpLogsUrl: config.otlpLogsUrl } : {}),
1812 otlpLogsEnabled: config.otlpLogsUrl !== undefined,
18111813 },
18121814 settings,
18131815 shellResumeCompletionMarker: true,
docs/operations/observability.md+16-3
......@@ -6,7 +6,7 @@ T3 Code has one server-side observability model:
66
77- pretty logs go to stdout for humans
88- completed spans go to a local NDJSON trace file
9- traces and metrics can also be exported over OTLP to a real backend like Grafana LGTM
9- traces, metrics, and logs can also be exported over OTLP to a real backend like Grafana LGTM
1010
1111The local trace file is the persisted source of truth for normal local launches. Those launches do not
1212write a separate server log file, but SSH-managed launches also persist the remote process's
......@@ -22,9 +22,16 @@ Logs are human-facing:
2222- format: `Logger.consolePretty()`
2323- normal local persistence: none
2424- SSH-managed launch persistence: `~/.t3/ssh-launch/<state>/server.log`
25- remote export: OTLP only, when configured
2526
2627If you want a log message to show up in the trace file, emit it inside an active span with `Effect.log...`. `Logger.tracerLogger` will attach it as a span event.
2728
29Configuring a logs endpoint takes over that job. The server then exports log records, which cover
30every message instead of only the ones inside an active span and carry the trace and span ids so
31they still line up with the trace. `Logger.tracerLogger` is dropped in that mode, so the same
32message is not exported twice and the trace file stops carrying log messages. stdout output and
33SSH-managed launch persistence stay unchanged either way.
34
2835### Traces
2936
3037Completed spans are written as NDJSON records to `serverTracePath`. The default depends on how the
......@@ -121,6 +128,7 @@ Default Grafana login:
121128```bash
122129export T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces
123130export T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics
131export T3CODE_OTLP_LOGS_URL=http://localhost:4318/v1/logs
124132export T3CODE_OTLP_SERVICE_NAME=t3-local
125133```
126134
......@@ -160,6 +168,7 @@ macOS app bundle example:
160168```bash
161169T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces \
162170T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics \
171T3CODE_OTLP_LOGS_URL=http://localhost:4318/v1/logs \
163172T3CODE_OTLP_SERVICE_NAME=t3-desktop \
164173"/Applications/T3 Code.app/Contents/MacOS/T3 Code"
165174```
......@@ -169,6 +178,7 @@ Direct binary example:
169178```bash
170179T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces \
171180T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics \
181T3CODE_OTLP_LOGS_URL=http://localhost:4318/v1/logs \
172182T3CODE_OTLP_SERVICE_NAME=t3-desktop \
173183./path/to/your/desktop-app-binary
174184```
......@@ -509,6 +519,7 @@ It provides:
509519- local NDJSON tracer
510520- optional OTLP trace exporter
511521- optional OTLP metrics exporter
522- optional OTLP log exporter
512523- Effect trace-level and timing refs
513524
514525### Env Vars
......@@ -526,13 +537,15 @@ OTLP export:
526537
527538- `T3CODE_OTLP_TRACES_URL`: OTLP trace endpoint
528539- `T3CODE_OTLP_METRICS_URL`: OTLP metric endpoint
540- `T3CODE_OTLP_LOGS_URL`: OTLP log endpoint
529541- `T3CODE_OTLP_EXPORT_INTERVAL_MS`: export interval, default `10000`
530542- `T3CODE_OTLP_SERVICE_NAME`: service name, default `t3-server`
531- `T3CODE_OTLP_HEADERS`: extra headers for both exporters, same format as
543- `T3CODE_OTLP_HEADERS`: extra headers for all three exporters, same format as
532544 `OTEL_EXPORTER_OTLP_HEADERS`: comma-separated `key=value` pairs with percent-encoded values.
533545- `T3CODE_OTLP_PROTOCOL`: `http/json` (default) or `http/protobuf`
534546
535If the OTLP URLs are unset, local tracing still works and metrics stay in-process only.
547If the OTLP URLs are unset, local tracing still works, metrics stay in-process only, and logs stay
548on stdout only.
536549
537550### What Is Instrumented Today
538551
packages/client-runtime/src/rpc/session.test.ts+1
......@@ -149,6 +149,7 @@ const SERVER_CONFIG: ServerConfigType = {
149149 localTracingEnabled: false,
150150 otlpTracesEnabled: false,
151151 otlpMetricsEnabled: false,
152 otlpLogsEnabled: false,
152153 },
153154 settings: DEFAULT_SERVER_SETTINGS,
154155};
packages/contracts/src/desktopBootstrap.ts+1
......@@ -16,6 +16,7 @@ export const DesktopBackendBootstrap = Schema.Struct({
1616 tailscaleServePort: PortSchema,
1717 otlpTracesUrl: Schema.optional(Schema.String),
1818 otlpMetricsUrl: Schema.optional(Schema.String),
19 otlpLogsUrl: Schema.optional(Schema.String),
1920 desktopTelemetryFd: Schema.optionalKey(PositiveInt),
2021 desktopTelemetryControlFd: Schema.optionalKey(PositiveInt),
2122 resourceMonitorPath: Schema.optionalKey(TrimmedNonEmptyString),
packages/contracts/src/server.test.ts+17
......@@ -5,6 +5,7 @@ import { ExecutionEnvironmentDescriptor } from "./environment.ts";
55import {
66 resolveEnvironmentMachineKind,
77 ServerConfig,
8 ServerObservability,
89 ServerProvider,
910 ServerProviders,
1011 ServerUpsertKeybindingResult,
......@@ -13,6 +14,7 @@ import { ServerSettings } from "./settings.ts";
1314
1415const decodeServerProvider = Schema.decodeUnknownSync(ServerProvider);
1516const decodeServerProviders = Schema.decodeUnknownSync(ServerProviders);
17const decodeServerObservability = Schema.decodeUnknownSync(ServerObservability);
1618const decodeUpsertKeybindingResult = Schema.decodeUnknownSync(ServerUpsertKeybindingResult);
1719const decodeAvailableEditors = Schema.decodeUnknownSync(ServerConfig.fields.availableEditors);
1820
......@@ -177,6 +179,21 @@ describe("server config forward compatibility", () => {
177179 });
178180});
179181
182describe("ServerObservability", () => {
183 it("reads a server from before the log signal as exporting no logs", () => {
184 const parsed = decodeServerObservability({
185 logsDirectoryPath: "/tmp/t3/logs",
186 localTracingEnabled: true,
187 otlpTracesUrl: "https://collector.example.com/v1/traces",
188 otlpTracesEnabled: true,
189 otlpMetricsEnabled: false,
190 });
191
192 expect(parsed.otlpLogsEnabled).toBe(false);
193 expect(parsed.otlpLogsUrl).toBeUndefined();
194 });
195});
196
180197describe("resolveEnvironmentMachineKind", () => {
181198 const decodeDescriptor = Schema.decodeUnknownSync(ExecutionEnvironmentDescriptor);
182199 const decodeSettings = Schema.decodeUnknownSync(ServerSettings);
packages/contracts/src/server.ts+4
......@@ -261,6 +261,10 @@ export const ServerObservability = Schema.Struct({
261261 otlpTracesEnabled: Schema.Boolean,
262262 otlpMetricsUrl: Schema.optional(TrimmedNonEmptyString),
263263 otlpMetricsEnabled: Schema.Boolean,
264 otlpLogsUrl: Schema.optional(TrimmedNonEmptyString),
265 // Absent on servers from before the log signal shipped, so a newer client
266 // reads those as having no log export rather than rejecting the whole config.
267 otlpLogsEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
264268});
265269export type ServerObservability = typeof ServerObservability.Type;
266270
packages/contracts/src/settings.ts+2
......@@ -889,6 +889,7 @@ export type UsageLimitSourceConfig = typeof UsageLimitSourceConfig.Type;
889889export const ObservabilitySettings = Schema.Struct({
890890 otlpTracesUrl: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed(""))),
891891 otlpMetricsUrl: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed(""))),
892 otlpLogsUrl: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed(""))),
892893});
893894export type ObservabilitySettings = typeof ObservabilitySettings.Type;
894895
......@@ -1481,6 +1482,7 @@ export const ServerSettingsPatch = Schema.Struct({
14811482 Schema.Struct({
14821483 otlpTracesUrl: Schema.optionalKey(TrimmedString),
14831484 otlpMetricsUrl: Schema.optionalKey(TrimmedString),
1485 otlpLogsUrl: Schema.optionalKey(TrimmedString),
14841486 }),
14851487 ),
14861488 providers: Schema.optionalKey(
packages/shared/src/serverSettings.test.ts+8-1
......@@ -224,14 +224,18 @@ describe("serverSettings helpers", () => {
224224 expect(parsePersistedServerObservabilitySettings("{}")).toEqual({
225225 otlpTracesUrl: undefined,
226226 otlpMetricsUrl: undefined,
227 otlpLogsUrl: undefined,
227228 });
228229 expect(
229230 parsePersistedServerObservabilitySettings(
230 JSON.stringify({ observability: { otlpTracesUrl: " ", otlpMetricsUrl: "" } }),
231 JSON.stringify({
232 observability: { otlpTracesUrl: " ", otlpMetricsUrl: "", otlpLogsUrl: " " },
233 }),
231234 ),
232235 ).toEqual({
233236 otlpTracesUrl: undefined,
234237 otlpMetricsUrl: undefined,
238 otlpLogsUrl: undefined,
235239 });
236240 });
237241
......@@ -242,12 +246,14 @@ describe("serverSettings helpers", () => {
242246 observability: {
243247 otlpTracesUrl: " http://localhost:4318/v1/traces ",
244248 otlpMetricsUrl: " http://localhost:4318/v1/metrics ",
249 otlpLogsUrl: " http://localhost:4318/v1/logs ",
245250 },
246251 }),
247252 ),
248253 ).toEqual({
249254 otlpTracesUrl: "http://localhost:4318/v1/traces",
250255 otlpMetricsUrl: "http://localhost:4318/v1/metrics",
256 otlpLogsUrl: "http://localhost:4318/v1/logs",
251257 });
252258 });
253259
......@@ -255,6 +261,7 @@ describe("serverSettings helpers", () => {
255261 expect(parsePersistedServerObservabilitySettings("{")).toEqual({
256262 otlpTracesUrl: undefined,
257263 otlpMetricsUrl: undefined,
264 otlpLogsUrl: undefined,
258265 });
259266 });
260267
packages/shared/src/serverSettings.ts+4-1
......@@ -102,6 +102,7 @@ export function resolveSourceControlWriterModelSelection(
102102export interface PersistedServerObservabilitySettings {
103103 readonly otlpTracesUrl: string | undefined;
104104 readonly otlpMetricsUrl: string | undefined;
105 readonly otlpLogsUrl: string | undefined;
105106}
106107
107108function normalizePersistedServerSettingString(
......@@ -115,11 +116,13 @@ function extractPersistedServerObservabilitySettings(input: {
115116 readonly observability?: {
116117 readonly otlpTracesUrl?: string;
117118 readonly otlpMetricsUrl?: string;
119 readonly otlpLogsUrl?: string;
118120 };
119121}): PersistedServerObservabilitySettings {
120122 return {
121123 otlpTracesUrl: normalizePersistedServerSettingString(input.observability?.otlpTracesUrl),
122124 otlpMetricsUrl: normalizePersistedServerSettingString(input.observability?.otlpMetricsUrl),
125 otlpLogsUrl: normalizePersistedServerSettingString(input.observability?.otlpLogsUrl),
123126 };
124127}
125128
......@@ -130,7 +133,7 @@ export function parsePersistedServerObservabilitySettings(
130133 if (Option.isSome(decoded)) {
131134 return extractPersistedServerObservabilitySettings(decoded.value);
132135 }
133 return { otlpTracesUrl: undefined, otlpMetricsUrl: undefined };
136 return { otlpTracesUrl: undefined, otlpMetricsUrl: undefined, otlpLogsUrl: undefined };
134137}
135138
136139function shouldReplaceTextGenerationModelSelection(