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";...@@ -3,8 +3,10 @@ import * as DateTime from "effect/DateTime";
3import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";4import * as FileSystem from "effect/FileSystem";
5import * as Layer from "effect/Layer";5import * as Layer from "effect/Layer";
6import * as Logger from "effect/Logger";
6import * as Option from "effect/Option";7import * as Option from "effect/Option";
7import * as PlatformError from "effect/PlatformError";8import * as PlatformError from "effect/PlatformError";
9import * as References from "effect/References";
810
9import * as TraceDiagnostics from "./TraceDiagnostics.ts";11import * as TraceDiagnostics from "./TraceDiagnostics.ts";
1012
...@@ -187,18 +189,17 @@ describe("TraceDiagnostics", () => {...@@ -187,18 +189,17 @@ describe("TraceDiagnostics", () => {
187 it.effect("keeps loaded trace data when one rotated trace file fails to read", () =>189 it.effect("keeps loaded trace data when one rotated trace file fails to read", () =>
188 Effect.gen(function* () {190 Effect.gen(function* () {
189 const traceFilePath = "/tmp/server.trace.ndjson";191 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 });
190 const fileSystemLayer = FileSystem.layerNoop({199 const fileSystemLayer = FileSystem.layerNoop({
191 readFileString: (path) =>200 readFileString: (path) =>
192 path === `${traceFilePath}.1`201 path === `${traceFilePath}.1`
193 ? Effect.fail(202 ? Effect.fail(readFailure)
194 PlatformError.systemError({
195 _tag: "PermissionDenied",
196 module: "FileSystem",
197 method: "readFileString",
198 description: "permission denied",
199 pathOrDescriptor: path,
200 }),
201 )
202 : Effect.succeed(203 : Effect.succeed(
203 record({204 record({
204 name: "server.getConfig",205 name: "server.getConfig",
...@@ -209,20 +210,44 @@ describe("TraceDiagnostics", () => {...@@ -209,20 +210,44 @@ describe("TraceDiagnostics", () => {
209 }),210 }),
210 ),211 ),
211 });212 });
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
213 const diagnostics = yield* TraceDiagnostics.readTraceDiagnostics({218 const diagnostics = yield* TraceDiagnostics.readTraceDiagnostics({
214 traceFilePath,219 traceFilePath,
215 maxFiles: 1,220 maxFiles: 1,
216 readAt: DateTime.makeUnsafe("2026-05-05T10:00:00.000Z"),221 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
219 assert.equal(diagnostics.recordCount, 1);231 assert.equal(diagnostics.recordCount, 1);
220 assert.equal(232 assert.equal(
221 Option.getOrElse(diagnostics.partialFailure, () => false),233 Option.getOrElse(diagnostics.partialFailure, () => false),
222 true,234 true,
223 );235 );
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 });
225 assert.deepStrictEqual(diagnostics.scannedFilePaths, [`${traceFilePath}.1`, traceFilePath]);240 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 });
226 }),251 }),
227 );252 );
228253
apps/server/src/diagnostics/TraceDiagnostics.ts+49-19
...@@ -14,6 +14,8 @@ import * as FileSystem from "effect/FileSystem";...@@ -14,6 +14,8 @@ import * as FileSystem from "effect/FileSystem";
14import * as Layer from "effect/Layer";14import * as Layer from "effect/Layer";
15import * as Option from "effect/Option";15import * as Option from "effect/Option";
16import * as PlatformError from "effect/PlatformError";16import * as PlatformError from "effect/PlatformError";
17import * as Result from "effect/Result";
18import * as Schema from "effect/Schema";
1719
18interface TraceRecordLike {20interface TraceRecordLike {
19 readonly name?: unknown;21 readonly name?: unknown;
...@@ -39,6 +41,19 @@ export interface TraceDiagnosticsOptions {...@@ -39,6 +41,19 @@ export interface TraceDiagnosticsOptions {
39 readonly readAt?: DateTime.Utc;41 readonly readAt?: DateTime.Utc;
40}42}
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
42export class TraceDiagnostics extends Context.Service<57export class TraceDiagnostics extends Context.Service<
43 TraceDiagnostics,58 TraceDiagnostics,
44 {59 {
...@@ -153,10 +168,6 @@ function isNotFoundError(error: PlatformError.PlatformError): boolean {...@@ -153,10 +168,6 @@ function isNotFoundError(error: PlatformError.PlatformError): boolean {
153 return error.reason._tag === "NotFound";168 return error.reason._tag === "NotFound";
154}169}
155170
156function platformErrorMessage(error: PlatformError.PlatformError): string {
157 return error.message || String(error);
158}
159
160function insertBoundedSlowestSpan(171function insertBoundedSlowestSpan(
161 slowestSpans: ServerTraceDiagnosticsSpanOccurrence[],172 slowestSpans: ServerTraceDiagnosticsSpanOccurrence[],
162 span: ServerTraceDiagnosticsSpanOccurrence,173 span: ServerTraceDiagnosticsSpanOccurrence,
...@@ -377,22 +388,26 @@ export function aggregateTraceDiagnostics(...@@ -377,22 +388,26 @@ export function aggregateTraceDiagnostics(
377388
378type TraceFileReadResult =389type TraceFileReadResult =
379 | { readonly _tag: "Loaded"; readonly path: string; readonly text: string }390 | { readonly _tag: "Loaded"; readonly path: string; readonly text: string }
380 | { readonly _tag: "Missing"; readonly path: string }391 | { readonly _tag: "Missing"; readonly path: string };
381 | { readonly _tag: "Failed"; readonly path: string; readonly message: string };
382392
383function readTraceFile(393function readTraceFile(
384 fileSystem: FileSystem.FileSystem,394 fileSystem: FileSystem.FileSystem,
385 path: string,395 path: string,
386): Effect.Effect<TraceFileReadResult> {396): Effect.Effect<TraceFileReadResult, TraceFileReadError> {
387 return fileSystem.readFileString(path).pipe(397 return fileSystem.readFileString(path).pipe(
388 Effect.map((text) => ({ _tag: "Loaded" as const, path, text })),398 Effect.map((text): TraceFileReadResult => ({ _tag: "Loaded", path, text })),
389 Effect.catch((error: PlatformError.PlatformError) =>399 Effect.catchTags({
390 Effect.succeed(400 PlatformError: (cause) =>
391 isNotFoundError(error)401 isNotFoundError(cause)
392 ? { _tag: "Missing" as const, path }402 ? Effect.succeed<TraceFileReadResult>({ _tag: "Missing", path })
393 : { _tag: "Failed" as const, path, message: platformErrorMessage(error) },403 : Effect.fail(
394 ),404 new TraceFileReadError({
395 ),405 traceFilePath: path,
406 causeTag: cause.reason._tag,
407 cause,
408 }),
409 ),
410 }),
396 );411 );
397}412}
398413
...@@ -405,19 +420,34 @@ export const make = Effect.gen(function* () {...@@ -405,19 +420,34 @@ export const make = Effect.gen(function* () {
405 const slowSpanThresholdMs = options.slowSpanThresholdMs ?? DEFAULT_SLOW_SPAN_THRESHOLD_MS;420 const slowSpanThresholdMs = options.slowSpanThresholdMs ?? DEFAULT_SLOW_SPAN_THRESHOLD_MS;
406 const paths = toRotatedTracePaths(options.traceFilePath, options.maxFiles);421 const paths = toRotatedTracePaths(options.traceFilePath, options.maxFiles);
407 const results = yield* Effect.all(422 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 ),
409 {437 {
410 concurrency: 1,438 concurrency: 1,
411 },439 },
412 );440 );
413 const files = results.flatMap((result) =>441 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 : [],
415 );445 );
416 const readFailure = results.find((result) => result._tag === "Failed");446 const readFailure = results.find(Result.isFailure);
417 const readFailureError = readFailure447 const readFailureError = readFailure
418 ? ({448 ? ({
419 kind: "trace-file-read-failed",449 kind: "trace-file-read-failed",
420 message: readFailure.message.trim() || `Failed to read ${readFailure.path}.`,450 message: readFailure.failure.message,
421 } satisfies TraceDiagnosticsErrorSummary)451 } satisfies TraceDiagnosticsErrorSummary)
422 : undefined;452 : undefined;
423453