authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 13:11:52-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 13:11:52-07:00
log6e9b43cb84c1b93c348d76880a0bf0083f395e47
tree2fe2c9ec1f37c9a38b48bdae219939abaeeb484f
parentbe56fa43b0aaad6ab537526e2129501038bbb6a8
signature Signed by PGP key B5690EEEBB952194

Preserve trace file read causes (#3300)

Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 85 insertions(+), 30 deletions(-)

apps/server/src/diagnostics/TraceDiagnostics.test.ts+36-11
......@@ -3,8 +3,10 @@ import * as DateTime from "effect/DateTime";
33import * as Effect from "effect/Effect";
44import * as FileSystem from "effect/FileSystem";
55import * as Layer from "effect/Layer";
6import * as Logger from "effect/Logger";
67import * as Option from "effect/Option";
78import * as PlatformError from "effect/PlatformError";
9import * as References from "effect/References";
810
911import * as TraceDiagnostics from "./TraceDiagnostics.ts";
1012
......@@ -187,18 +189,17 @@ describe("TraceDiagnostics", () => {
187189 it.effect("keeps loaded trace data when one rotated trace file fails to read", () =>
188190 Effect.gen(function* () {
189191 const traceFilePath = "/tmp/server.trace.ndjson";
192 const readFailure = PlatformError.systemError({
193 _tag: "PermissionDenied",
194 module: "FileSystem",
195 method: "readFileString",
196 description: "permission denied",
197 pathOrDescriptor: `${traceFilePath}.1`,
198 });
190199 const fileSystemLayer = FileSystem.layerNoop({
191200 readFileString: (path) =>
192201 path === `${traceFilePath}.1`
193 ? Effect.fail(
194 PlatformError.systemError({
195 _tag: "PermissionDenied",
196 module: "FileSystem",
197 method: "readFileString",
198 description: "permission denied",
199 pathOrDescriptor: path,
200 }),
201 )
202 ? Effect.fail(readFailure)
202203 : Effect.succeed(
203204 record({
204205 name: "server.getConfig",
......@@ -209,20 +210,44 @@ describe("TraceDiagnostics", () => {
209210 }),
210211 ),
211212 });
213 const logAnnotations: Array<Record<string, unknown>> = [];
214 const logger = Logger.make<unknown, void>((options) => {
215 logAnnotations.push({ ...options.fiber.getRef(References.CurrentLogAnnotations) });
216 });
212217
213218 const diagnostics = yield* TraceDiagnostics.readTraceDiagnostics({
214219 traceFilePath,
215220 maxFiles: 1,
216221 readAt: DateTime.makeUnsafe("2026-05-05T10:00:00.000Z"),
217 }).pipe(Effect.provide(TraceDiagnostics.layer.pipe(Layer.provide(fileSystemLayer))));
222 }).pipe(
223 Effect.provide(
224 Layer.mergeAll(
225 TraceDiagnostics.layer.pipe(Layer.provide(fileSystemLayer)),
226 Logger.layer([logger], { mergeWithExisting: false }),
227 ),
228 ),
229 );
218230
219231 assert.equal(diagnostics.recordCount, 1);
220232 assert.equal(
221233 Option.getOrElse(diagnostics.partialFailure, () => false),
222234 true,
223235 );
224 assert.equal(Option.getOrUndefined(diagnostics.error)?.kind, "trace-file-read-failed");
236 assert.deepStrictEqual(Option.getOrUndefined(diagnostics.error), {
237 kind: "trace-file-read-failed",
238 message: `Failed to read local trace file '${traceFilePath}.1'.`,
239 });
225240 assert.deepStrictEqual(diagnostics.scannedFilePaths, [`${traceFilePath}.1`, traceFilePath]);
241
242 const failureLog = logAnnotations.find(
243 (annotations) => annotations.traceFilePath === `${traceFilePath}.1`,
244 );
245 assert.exists(failureLog);
246 assert.deepStrictEqual(failureLog, {
247 traceFilePath: `${traceFilePath}.1`,
248 errorTag: "TraceFileReadError",
249 causeTag: "PermissionDenied",
250 });
226251 }),
227252 );
228253
apps/server/src/diagnostics/TraceDiagnostics.ts+49-19
......@@ -14,6 +14,8 @@ import * as FileSystem from "effect/FileSystem";
1414import * as Layer from "effect/Layer";
1515import * as Option from "effect/Option";
1616import * as PlatformError from "effect/PlatformError";
17import * as Result from "effect/Result";
18import * as Schema from "effect/Schema";
1719
1820interface TraceRecordLike {
1921 readonly name?: unknown;
......@@ -39,6 +41,19 @@ export interface TraceDiagnosticsOptions {
3941 readonly readAt?: DateTime.Utc;
4042}
4143
44export class TraceFileReadError extends Schema.TaggedErrorClass<TraceFileReadError>()(
45 "TraceFileReadError",
46 {
47 traceFilePath: Schema.String,
48 causeTag: Schema.String,
49 cause: Schema.Defect(),
50 },
51) {
52 override get message(): string {
53 return `Failed to read local trace file '${this.traceFilePath}'.`;
54 }
55}
56
4257export class TraceDiagnostics extends Context.Service<
4358 TraceDiagnostics,
4459 {
......@@ -153,10 +168,6 @@ function isNotFoundError(error: PlatformError.PlatformError): boolean {
153168 return error.reason._tag === "NotFound";
154169}
155170
156function platformErrorMessage(error: PlatformError.PlatformError): string {
157 return error.message || String(error);
158}
159
160171function insertBoundedSlowestSpan(
161172 slowestSpans: ServerTraceDiagnosticsSpanOccurrence[],
162173 span: ServerTraceDiagnosticsSpanOccurrence,
......@@ -377,22 +388,26 @@ export function aggregateTraceDiagnostics(
377388
378389type TraceFileReadResult =
379390 | { readonly _tag: "Loaded"; readonly path: string; readonly text: string }
380 | { readonly _tag: "Missing"; readonly path: string }
381 | { readonly _tag: "Failed"; readonly path: string; readonly message: string };
391 | { readonly _tag: "Missing"; readonly path: string };
382392
383393function readTraceFile(
384394 fileSystem: FileSystem.FileSystem,
385395 path: string,
386): Effect.Effect<TraceFileReadResult> {
396): Effect.Effect<TraceFileReadResult, TraceFileReadError> {
387397 return fileSystem.readFileString(path).pipe(
388 Effect.map((text) => ({ _tag: "Loaded" as const, path, text })),
389 Effect.catch((error: PlatformError.PlatformError) =>
390 Effect.succeed(
391 isNotFoundError(error)
392 ? { _tag: "Missing" as const, path }
393 : { _tag: "Failed" as const, path, message: platformErrorMessage(error) },
394 ),
395 ),
398 Effect.map((text): TraceFileReadResult => ({ _tag: "Loaded", path, text })),
399 Effect.catchTags({
400 PlatformError: (cause) =>
401 isNotFoundError(cause)
402 ? Effect.succeed<TraceFileReadResult>({ _tag: "Missing", path })
403 : Effect.fail(
404 new TraceFileReadError({
405 traceFilePath: path,
406 causeTag: cause.reason._tag,
407 cause,
408 }),
409 ),
410 }),
396411 );
397412}
398413
......@@ -405,19 +420,34 @@ export const make = Effect.gen(function* () {
405420 const slowSpanThresholdMs = options.slowSpanThresholdMs ?? DEFAULT_SLOW_SPAN_THRESHOLD_MS;
406421 const paths = toRotatedTracePaths(options.traceFilePath, options.maxFiles);
407422 const results = yield* Effect.all(
408 paths.map((path) => readTraceFile(fileSystem, path)),
423 paths.map((path) =>
424 readTraceFile(fileSystem, path).pipe(
425 Effect.tapError((cause) =>
426 Effect.logWarning("Failed to read local trace file.").pipe(
427 Effect.annotateLogs({
428 traceFilePath: cause.traceFilePath,
429 errorTag: cause._tag,
430 causeTag: cause.causeTag,
431 }),
432 ),
433 ),
434 Effect.result,
435 ),
436 ),
409437 {
410438 concurrency: 1,
411439 },
412440 );
413441 const files = results.flatMap((result) =>
414 result._tag === "Loaded" ? [{ path: result.path, text: result.text }] : [],
442 Result.isSuccess(result) && result.success._tag === "Loaded"
443 ? [{ path: result.success.path, text: result.success.text }]
444 : [],
415445 );
416 const readFailure = results.find((result) => result._tag === "Failed");
446 const readFailure = results.find(Result.isFailure);
417447 const readFailureError = readFailure
418448 ? ({
419449 kind: "trace-file-read-failed",
420 message: readFailure.message.trim() || `Failed to read ${readFailure.path}.`,
450 message: readFailure.failure.message,
421451 } satisfies TraceDiagnosticsErrorSummary)
422452 : undefined;
423453