diff --git a/apps/server/src/diagnostics/TraceDiagnostics.test.ts b/apps/server/src/diagnostics/TraceDiagnostics.test.ts index d4ffa4a5fc2..70bb4dc815c 100644 --- a/apps/server/src/diagnostics/TraceDiagnostics.test.ts +++ b/apps/server/src/diagnostics/TraceDiagnostics.test.ts @@ -3,8 +3,10 @@ import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; +import * as Logger from "effect/Logger"; import * as Option from "effect/Option"; import * as PlatformError from "effect/PlatformError"; +import * as References from "effect/References"; import * as TraceDiagnostics from "./TraceDiagnostics.ts"; @@ -187,18 +189,17 @@ describe("TraceDiagnostics", () => { it.effect("keeps loaded trace data when one rotated trace file fails to read", () => Effect.gen(function* () { const traceFilePath = "/tmp/server.trace.ndjson"; + const readFailure = PlatformError.systemError({ + _tag: "PermissionDenied", + module: "FileSystem", + method: "readFileString", + description: "permission denied", + pathOrDescriptor: `${traceFilePath}.1`, + }); const fileSystemLayer = FileSystem.layerNoop({ readFileString: (path) => path === `${traceFilePath}.1` - ? Effect.fail( - PlatformError.systemError({ - _tag: "PermissionDenied", - module: "FileSystem", - method: "readFileString", - description: "permission denied", - pathOrDescriptor: path, - }), - ) + ? Effect.fail(readFailure) : Effect.succeed( record({ name: "server.getConfig", @@ -209,20 +210,44 @@ describe("TraceDiagnostics", () => { }), ), }); + const logAnnotations: Array> = []; + const logger = Logger.make((options) => { + logAnnotations.push({ ...options.fiber.getRef(References.CurrentLogAnnotations) }); + }); const diagnostics = yield* TraceDiagnostics.readTraceDiagnostics({ traceFilePath, maxFiles: 1, readAt: DateTime.makeUnsafe("2026-05-05T10:00:00.000Z"), - }).pipe(Effect.provide(TraceDiagnostics.layer.pipe(Layer.provide(fileSystemLayer)))); + }).pipe( + Effect.provide( + Layer.mergeAll( + TraceDiagnostics.layer.pipe(Layer.provide(fileSystemLayer)), + Logger.layer([logger], { mergeWithExisting: false }), + ), + ), + ); assert.equal(diagnostics.recordCount, 1); assert.equal( Option.getOrElse(diagnostics.partialFailure, () => false), true, ); - assert.equal(Option.getOrUndefined(diagnostics.error)?.kind, "trace-file-read-failed"); + assert.deepStrictEqual(Option.getOrUndefined(diagnostics.error), { + kind: "trace-file-read-failed", + message: `Failed to read local trace file '${traceFilePath}.1'.`, + }); assert.deepStrictEqual(diagnostics.scannedFilePaths, [`${traceFilePath}.1`, traceFilePath]); + + const failureLog = logAnnotations.find( + (annotations) => annotations.traceFilePath === `${traceFilePath}.1`, + ); + assert.exists(failureLog); + assert.deepStrictEqual(failureLog, { + traceFilePath: `${traceFilePath}.1`, + errorTag: "TraceFileReadError", + causeTag: "PermissionDenied", + }); }), ); diff --git a/apps/server/src/diagnostics/TraceDiagnostics.ts b/apps/server/src/diagnostics/TraceDiagnostics.ts index d396f4e4ee9..d54e033380c 100644 --- a/apps/server/src/diagnostics/TraceDiagnostics.ts +++ b/apps/server/src/diagnostics/TraceDiagnostics.ts @@ -14,6 +14,8 @@ import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as PlatformError from "effect/PlatformError"; +import * as Result from "effect/Result"; +import * as Schema from "effect/Schema"; interface TraceRecordLike { readonly name?: unknown; @@ -39,6 +41,19 @@ export interface TraceDiagnosticsOptions { readonly readAt?: DateTime.Utc; } +export class TraceFileReadError extends Schema.TaggedErrorClass()( + "TraceFileReadError", + { + traceFilePath: Schema.String, + causeTag: Schema.String, + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to read local trace file '${this.traceFilePath}'.`; + } +} + export class TraceDiagnostics extends Context.Service< TraceDiagnostics, { @@ -153,10 +168,6 @@ function isNotFoundError(error: PlatformError.PlatformError): boolean { return error.reason._tag === "NotFound"; } -function platformErrorMessage(error: PlatformError.PlatformError): string { - return error.message || String(error); -} - function insertBoundedSlowestSpan( slowestSpans: ServerTraceDiagnosticsSpanOccurrence[], span: ServerTraceDiagnosticsSpanOccurrence, @@ -377,22 +388,26 @@ export function aggregateTraceDiagnostics( type TraceFileReadResult = | { readonly _tag: "Loaded"; readonly path: string; readonly text: string } - | { readonly _tag: "Missing"; readonly path: string } - | { readonly _tag: "Failed"; readonly path: string; readonly message: string }; + | { readonly _tag: "Missing"; readonly path: string }; function readTraceFile( fileSystem: FileSystem.FileSystem, path: string, -): Effect.Effect { +): Effect.Effect { return fileSystem.readFileString(path).pipe( - Effect.map((text) => ({ _tag: "Loaded" as const, path, text })), - Effect.catch((error: PlatformError.PlatformError) => - Effect.succeed( - isNotFoundError(error) - ? { _tag: "Missing" as const, path } - : { _tag: "Failed" as const, path, message: platformErrorMessage(error) }, - ), - ), + Effect.map((text): TraceFileReadResult => ({ _tag: "Loaded", path, text })), + Effect.catchTags({ + PlatformError: (cause) => + isNotFoundError(cause) + ? Effect.succeed({ _tag: "Missing", path }) + : Effect.fail( + new TraceFileReadError({ + traceFilePath: path, + causeTag: cause.reason._tag, + cause, + }), + ), + }), ); } @@ -405,19 +420,34 @@ export const make = Effect.gen(function* () { const slowSpanThresholdMs = options.slowSpanThresholdMs ?? DEFAULT_SLOW_SPAN_THRESHOLD_MS; const paths = toRotatedTracePaths(options.traceFilePath, options.maxFiles); const results = yield* Effect.all( - paths.map((path) => readTraceFile(fileSystem, path)), + paths.map((path) => + readTraceFile(fileSystem, path).pipe( + Effect.tapError((cause) => + Effect.logWarning("Failed to read local trace file.").pipe( + Effect.annotateLogs({ + traceFilePath: cause.traceFilePath, + errorTag: cause._tag, + causeTag: cause.causeTag, + }), + ), + ), + Effect.result, + ), + ), { concurrency: 1, }, ); const files = results.flatMap((result) => - result._tag === "Loaded" ? [{ path: result.path, text: result.text }] : [], + Result.isSuccess(result) && result.success._tag === "Loaded" + ? [{ path: result.success.path, text: result.success.text }] + : [], ); - const readFailure = results.find((result) => result._tag === "Failed"); + const readFailure = results.find(Result.isFailure); const readFailureError = readFailure ? ({ kind: "trace-file-read-failed", - message: readFailure.message.trim() || `Failed to read ${readFailure.path}.`, + message: readFailure.failure.message, } satisfies TraceDiagnosticsErrorSummary) : undefined;