Skip to content

Commit dd7b25e

Browse files
authored
Merge pull request #44 from aaditagrawal/merge/upstream-main-2026-04-03
Merge upstream pingdotgg/t3code: 11 commits (tracing, IntelliJ, pagination, bug fixes)
2 parents b35e2a2 + b0e22f5 commit dd7b25e

File tree

82 files changed

+5629
-1125
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+5629
-1125
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ We are very very early in this project. Expect bugs.
5555

5656
We are not accepting contributions yet.
5757

58+
Observability guide: [docs/observability.md](./docs/observability.md)
59+
5860
## If you REALLY want to contribute still.... read this first
5961

6062
Read [CONTRIBUTING.md](./CONTRIBUTING.md) before opening an issue or PR.

apps/desktop/src/main.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { autoUpdater } from "electron-updater";
2727
import type { ContextMenuItem } from "@t3tools/contracts";
2828
import { NetService } from "@t3tools/shared/Net";
2929
import { RotatingFileSink } from "@t3tools/shared/logging";
30+
import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings";
3031
import { showDesktopConfirmDialog } from "./confirmDialog";
3132
import { syncShellEnvironment } from "./syncShellEnvironment";
3233
import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState";
@@ -79,6 +80,7 @@ const LOG_DIR = Path.join(STATE_DIR, "logs");
7980
const LOG_FILE_MAX_BYTES = 10 * 1024 * 1024;
8081
const LOG_FILE_MAX_FILES = 10;
8182
const APP_RUN_ID = Crypto.randomBytes(6).toString("hex");
83+
const SERVER_SETTINGS_PATH = Path.join(STATE_DIR, "settings.json");
8284
const AUTO_UPDATE_STARTUP_DELAY_MS = 15_000;
8385
const AUTO_UPDATE_POLL_INTERVAL_MS = 4 * 60 * 60 * 1000;
8486
const DESKTOP_UPDATE_CHANNEL = "latest";
@@ -102,8 +104,10 @@ let aboutCommitHashCache: string | null | undefined;
102104
let desktopLogSink: RotatingFileSink | null = null;
103105
let backendLogSink: RotatingFileSink | null = null;
104106
let restoreStdIoCapture: (() => void) | null = null;
107+
let backendObservabilitySettings = readPersistedBackendObservabilitySettings();
105108

106109
let destructiveMenuIconCache: Electron.NativeImage | null | undefined;
110+
const expectedBackendExitChildren = new WeakSet<ChildProcess.ChildProcess>();
107111
const desktopRuntimeInfo = resolveDesktopRuntimeInfo({
108112
platform: process.platform,
109113
processArch: process.arch,
@@ -124,6 +128,21 @@ function sanitizeLogValue(value: string): string {
124128
return value.replace(/\s+/g, " ").trim();
125129
}
126130

131+
function readPersistedBackendObservabilitySettings(): {
132+
readonly otlpTracesUrl: string | undefined;
133+
readonly otlpMetricsUrl: string | undefined;
134+
} {
135+
try {
136+
if (!FS.existsSync(SERVER_SETTINGS_PATH)) {
137+
return { otlpTracesUrl: undefined, otlpMetricsUrl: undefined };
138+
}
139+
return parsePersistedServerObservabilitySettings(FS.readFileSync(SERVER_SETTINGS_PATH, "utf8"));
140+
} catch (error) {
141+
console.warn("[desktop] failed to read persisted backend observability settings", error);
142+
return { otlpTracesUrl: undefined, otlpMetricsUrl: undefined };
143+
}
144+
}
145+
127146
function backendChildEnv(): NodeJS.ProcessEnv {
128147
const env = { ...process.env };
129148
delete env.T3CODE_PORT;
@@ -964,6 +983,7 @@ function scheduleBackendRestart(reason: string): void {
964983
function startBackend(): void {
965984
if (isQuitting || backendProcess) return;
966985

986+
backendObservabilitySettings = readPersistedBackendObservabilitySettings();
967987
const backendEntry = resolveBackendEntry();
968988
if (!FS.existsSync(backendEntry)) {
969989
scheduleBackendRestart(`missing server entry at ${backendEntry}`);
@@ -992,6 +1012,12 @@ function startBackend(): void {
9921012
port: backendPort,
9931013
t3Home: BASE_DIR,
9941014
authToken: backendAuthToken,
1015+
...(backendObservabilitySettings.otlpTracesUrl
1016+
? { otlpTracesUrl: backendObservabilitySettings.otlpTracesUrl }
1017+
: {}),
1018+
...(backendObservabilitySettings.otlpMetricsUrl
1019+
? { otlpMetricsUrl: backendObservabilitySettings.otlpMetricsUrl }
1020+
: {}),
9951021
})}\n`,
9961022
);
9971023
bootstrapStream.end();
@@ -1018,21 +1044,26 @@ function startBackend(): void {
10181044
});
10191045

10201046
child.on("error", (error) => {
1047+
const wasExpected = expectedBackendExitChildren.has(child);
10211048
if (backendProcess === child) {
10221049
backendProcess = null;
10231050
}
10241051
closeBackendSession(`pid=${child.pid ?? "unknown"} error=${error.message}`);
1052+
if (wasExpected) {
1053+
return;
1054+
}
10251055
scheduleBackendRestart(error.message);
10261056
});
10271057

10281058
child.on("exit", (code, signal) => {
1059+
const wasExpected = expectedBackendExitChildren.has(child);
10291060
if (backendProcess === child) {
10301061
backendProcess = null;
10311062
}
10321063
closeBackendSession(
10331064
`pid=${child.pid ?? "unknown"} code=${code ?? "null"} signal=${signal ?? "null"}`,
10341065
);
1035-
if (isQuitting) return;
1066+
if (isQuitting || wasExpected) return;
10361067
const reason = `code=${code ?? "null"} signal=${signal ?? "null"}`;
10371068
scheduleBackendRestart(reason);
10381069
});
@@ -1049,6 +1080,7 @@ function stopBackend(): void {
10491080
if (!child) return;
10501081

10511082
if (child.exitCode === null && child.signalCode === null) {
1083+
expectedBackendExitChildren.add(child);
10521084
child.kill("SIGTERM");
10531085
setTimeout(() => {
10541086
if (child.exitCode === null && child.signalCode === null) {
@@ -1069,6 +1101,7 @@ async function stopBackendAndWaitForExit(timeoutMs = 5_000): Promise<void> {
10691101
if (!child) return;
10701102
const backendChild = child;
10711103
if (backendChild.exitCode !== null || backendChild.signalCode !== null) return;
1104+
expectedBackendExitChildren.add(backendChild);
10721105

10731106
await new Promise<void>((resolve) => {
10741107
let settled = false;

apps/server/src/cli-config.test.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@ import { deriveServerPaths } from "./config";
99
import { resolveServerConfig } from "./cli";
1010

1111
it.layer(NodeServices.layer)("cli config resolution", (it) => {
12+
const defaultObservabilityConfig = {
13+
traceMinLevel: "Info",
14+
traceTimingEnabled: true,
15+
traceBatchWindowMs: 200,
16+
traceMaxBytes: 10 * 1024 * 1024,
17+
traceMaxFiles: 10,
18+
otlpTracesUrl: undefined,
19+
otlpMetricsUrl: undefined,
20+
otlpExportIntervalMs: 10_000,
21+
otlpServiceName: "t3-server",
22+
} as const;
23+
1224
const openBootstrapFd = Effect.fn(function* (payload: Record<string, unknown>) {
1325
const fs = yield* FileSystem.FileSystem;
1426
const filePath = yield* fs.makeTempFileScoped({ prefix: "t3-bootstrap-", suffix: ".ndjson" });
@@ -62,6 +74,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
6274

6375
expect(resolved).toEqual({
6476
logLevel: "Warn",
77+
...defaultObservabilityConfig,
6578
mode: "desktop",
6679
port: 4001,
6780
cwd: process.cwd(),
@@ -123,6 +136,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
123136

124137
expect(resolved).toEqual({
125138
logLevel: "Debug",
139+
...defaultObservabilityConfig,
126140
mode: "web",
127141
port: 8788,
128142
cwd: process.cwd(),
@@ -153,6 +167,8 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
153167
authToken: "bootstrap-token",
154168
autoBootstrapProjectFromCwd: false,
155169
logWebSocketEvents: true,
170+
otlpTracesUrl: "http://localhost:4318/v1/traces",
171+
otlpMetricsUrl: "http://localhost:4318/v1/metrics",
156172
});
157173
const derivedPaths = yield* deriveServerPaths(baseDir, new URL("http://127.0.0.1:5173"));
158174

@@ -187,6 +203,9 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
187203

188204
expect(resolved).toEqual({
189205
logLevel: "Info",
206+
...defaultObservabilityConfig,
207+
otlpTracesUrl: "http://localhost:4318/v1/traces",
208+
otlpMetricsUrl: "http://localhost:4318/v1/metrics",
190209
mode: "desktop",
191210
port: 4888,
192211
cwd: process.cwd(),
@@ -241,6 +260,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
241260
resolved.attachmentsDir,
242261
resolved.worktreesDir,
243262
path.dirname(resolved.serverLogPath),
263+
path.dirname(resolved.serverTracePath),
244264
]) {
245265
expect(yield* fs.exists(directory)).toBe(true);
246266
}
@@ -300,6 +320,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
300320

301321
expect(resolved).toEqual({
302322
logLevel: "Debug",
323+
...defaultObservabilityConfig,
303324
mode: "web",
304325
port: 8788,
305326
cwd: process.cwd(),
@@ -315,4 +336,67 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
315336
});
316337
}),
317338
);
339+
340+
it.effect("falls back to persisted observability settings when env vars are absent", () =>
341+
Effect.gen(function* () {
342+
const fs = yield* FileSystem.FileSystem;
343+
const path = yield* Path.Path;
344+
const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-cli-config-settings-" });
345+
const derivedPaths = yield* deriveServerPaths(baseDir, undefined);
346+
yield* fs.makeDirectory(path.dirname(derivedPaths.settingsPath), { recursive: true });
347+
yield* fs.writeFileString(
348+
derivedPaths.settingsPath,
349+
`${JSON.stringify({
350+
observability: {
351+
otlpTracesUrl: "http://localhost:4318/v1/traces",
352+
otlpMetricsUrl: "http://localhost:4318/v1/metrics",
353+
},
354+
})}\n`,
355+
);
356+
357+
const resolved = yield* resolveServerConfig(
358+
{
359+
mode: Option.some("desktop"),
360+
port: Option.some(4888),
361+
host: Option.none(),
362+
baseDir: Option.some(baseDir),
363+
devUrl: Option.none(),
364+
noBrowser: Option.none(),
365+
authToken: Option.none(),
366+
bootstrapFd: Option.none(),
367+
autoBootstrapProjectFromCwd: Option.none(),
368+
logWebSocketEvents: Option.none(),
369+
},
370+
Option.none(),
371+
).pipe(
372+
Effect.provide(
373+
Layer.mergeAll(
374+
ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })),
375+
NetService.layer,
376+
),
377+
),
378+
);
379+
380+
expect(resolved.otlpTracesUrl).toBe("http://localhost:4318/v1/traces");
381+
expect(resolved.otlpMetricsUrl).toBe("http://localhost:4318/v1/metrics");
382+
expect(resolved).toEqual({
383+
logLevel: "Info",
384+
...defaultObservabilityConfig,
385+
otlpTracesUrl: "http://localhost:4318/v1/traces",
386+
otlpMetricsUrl: "http://localhost:4318/v1/metrics",
387+
mode: "desktop",
388+
port: 4888,
389+
cwd: process.cwd(),
390+
baseDir,
391+
...derivedPaths,
392+
host: "127.0.0.1",
393+
staticDir: resolved.staticDir,
394+
devUrl: undefined,
395+
noBrowser: true,
396+
authToken: undefined,
397+
autoBootstrapProjectFromCwd: false,
398+
logWebSocketEvents: false,
399+
});
400+
}),
401+
);
318402
});

apps/server/src/cli.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NetService } from "@t3tools/shared/Net";
2-
import { Config, Effect, LogLevel, Option, Schema } from "effect";
2+
import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings";
3+
import { Config, Effect, FileSystem, LogLevel, Option, Path, Schema } from "effect";
34
import { Command, Flag, GlobalFlag } from "effect/unstable/cli";
45

56
import {
@@ -27,6 +28,8 @@ const BootstrapEnvelopeSchema = Schema.Struct({
2728
authToken: Schema.optional(Schema.String),
2829
autoBootstrapProjectFromCwd: Schema.optional(Schema.Boolean),
2930
logWebSocketEvents: Schema.optional(Schema.Boolean),
31+
otlpTracesUrl: Schema.optional(Schema.String),
32+
otlpMetricsUrl: Schema.optional(Schema.String),
3033
});
3134

3235
const modeFlag = Flag.choice("mode", RuntimeMode.literals).pipe(
@@ -81,6 +84,27 @@ const logWebSocketEventsFlag = Flag.boolean("log-websocket-events").pipe(
8184

8285
const EnvServerConfig = Config.all({
8386
logLevel: Config.logLevel("T3CODE_LOG_LEVEL").pipe(Config.withDefault("Info")),
87+
traceMinLevel: Config.logLevel("T3CODE_TRACE_MIN_LEVEL").pipe(Config.withDefault("Info")),
88+
traceTimingEnabled: Config.boolean("T3CODE_TRACE_TIMING_ENABLED").pipe(Config.withDefault(true)),
89+
traceFile: Config.string("T3CODE_TRACE_FILE").pipe(
90+
Config.option,
91+
Config.map(Option.getOrUndefined),
92+
),
93+
traceMaxBytes: Config.int("T3CODE_TRACE_MAX_BYTES").pipe(Config.withDefault(10 * 1024 * 1024)),
94+
traceMaxFiles: Config.int("T3CODE_TRACE_MAX_FILES").pipe(Config.withDefault(10)),
95+
traceBatchWindowMs: Config.int("T3CODE_TRACE_BATCH_WINDOW_MS").pipe(Config.withDefault(200)),
96+
otlpTracesUrl: Config.string("T3CODE_OTLP_TRACES_URL").pipe(
97+
Config.option,
98+
Config.map(Option.getOrUndefined),
99+
),
100+
otlpMetricsUrl: Config.string("T3CODE_OTLP_METRICS_URL").pipe(
101+
Config.option,
102+
Config.map(Option.getOrUndefined),
103+
),
104+
otlpExportIntervalMs: Config.int("T3CODE_OTLP_EXPORT_INTERVAL_MS").pipe(
105+
Config.withDefault(10_000),
106+
),
107+
otlpServiceName: Config.string("T3CODE_OTLP_SERVICE_NAME").pipe(Config.withDefault("t3-server")),
84108
mode: Config.schema(RuntimeMode, "T3CODE_MODE").pipe(
85109
Config.option,
86110
Config.map(Option.getOrUndefined),
@@ -131,12 +155,25 @@ const resolveOptionPrecedence = <Value>(
131155
...values: ReadonlyArray<Option.Option<Value>>
132156
): Option.Option<Value> => Option.firstSomeOf(values);
133157

158+
const loadPersistedObservabilitySettings = Effect.fn(function* (settingsPath: string) {
159+
const fs = yield* FileSystem.FileSystem;
160+
const exists = yield* fs.exists(settingsPath).pipe(Effect.orElseSucceed(() => false));
161+
if (!exists) {
162+
return { otlpTracesUrl: undefined, otlpMetricsUrl: undefined };
163+
}
164+
165+
const raw = yield* fs.readFileString(settingsPath).pipe(Effect.orElseSucceed(() => ""));
166+
return parsePersistedServerObservabilitySettings(raw);
167+
});
168+
134169
export const resolveServerConfig = (
135170
flags: CliServerFlags,
136171
cliLogLevel: Option.Option<LogLevel.LogLevel>,
137172
) =>
138173
Effect.gen(function* () {
139174
const { findAvailablePort } = yield* NetService;
175+
const path = yield* Path.Path;
176+
const fs = yield* FileSystem.FileSystem;
140177
const env = yield* EnvServerConfig;
141178
const bootstrapFd = Option.getOrUndefined(flags.bootstrapFd) ?? env.bootstrapFd;
142179
const bootstrapEnvelope =
@@ -190,6 +227,11 @@ export const resolveServerConfig = (
190227
);
191228
const derivedPaths = yield* deriveServerPaths(baseDir, devUrl);
192229
yield* ensureServerDirectories(derivedPaths);
230+
const persistedObservabilitySettings = yield* loadPersistedObservabilitySettings(
231+
derivedPaths.settingsPath,
232+
);
233+
const serverTracePath = env.traceFile ?? derivedPaths.serverTracePath;
234+
yield* fs.makeDirectory(path.dirname(serverTracePath), { recursive: true });
193235
const noBrowser = resolveBooleanFlag(
194236
flags.noBrowser,
195237
Option.getOrElse(
@@ -248,11 +290,35 @@ export const resolveServerConfig = (
248290

249291
const config: ServerConfigShape = {
250292
logLevel,
293+
traceMinLevel: env.traceMinLevel,
294+
traceTimingEnabled: env.traceTimingEnabled,
295+
traceBatchWindowMs: env.traceBatchWindowMs,
296+
traceMaxBytes: env.traceMaxBytes,
297+
traceMaxFiles: env.traceMaxFiles,
298+
otlpTracesUrl:
299+
env.otlpTracesUrl ??
300+
Option.getOrUndefined(
301+
Option.flatMap(bootstrapEnvelope, (bootstrap) =>
302+
Option.fromUndefinedOr(bootstrap.otlpTracesUrl),
303+
),
304+
) ??
305+
persistedObservabilitySettings.otlpTracesUrl,
306+
otlpMetricsUrl:
307+
env.otlpMetricsUrl ??
308+
Option.getOrUndefined(
309+
Option.flatMap(bootstrapEnvelope, (bootstrap) =>
310+
Option.fromUndefinedOr(bootstrap.otlpMetricsUrl),
311+
),
312+
) ??
313+
persistedObservabilitySettings.otlpMetricsUrl,
314+
otlpExportIntervalMs: env.otlpExportIntervalMs,
315+
otlpServiceName: env.otlpServiceName,
251316
mode,
252317
port,
253318
cwd: process.cwd(),
254319
baseDir,
255320
...derivedPaths,
321+
serverTracePath,
256322
host,
257323
staticDir,
258324
devUrl,

0 commit comments

Comments
 (0)