import { assert, describe, it } from "@effect/vitest"; 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 Option from "effect/Option"; import * as PlatformError from "effect/PlatformError"; import * as Schema from "effect/Schema"; import * as TraceDiagnostics from "./TraceDiagnostics.ts"; const encodeTraceRecordJson = Schema.encodeUnknownSync(Schema.UnknownFromJsonString); function ns(ms: number): string { return String(BigInt(ms) * 1_000_000n); } function record(input: { readonly name: string; readonly traceId: string; readonly spanId: string; readonly startMs: number; readonly durationMs: number; readonly exit?: { readonly _tag: "Success" | "Failure" | "Interrupted"; readonly cause?: string }; readonly events?: ReadonlyArray; }) { return encodeTraceRecordJson({ type: "effect-span", name: input.name, traceId: input.traceId, spanId: input.spanId, sampled: true, kind: "internal", startTimeUnixNano: ns(input.startMs), endTimeUnixNano: ns(input.startMs + input.durationMs), durationMs: input.durationMs, attributes: {}, events: input.events ?? [], links: [], exit: input.exit ?? { _tag: "Success" }, }); } describe("TraceDiagnostics", () => { it.effect("aggregates failures, slow spans, log levels, and parse errors", () => Effect.sync(() => { const diagnostics = TraceDiagnostics.aggregateTraceDiagnostics({ traceFilePath: "/tmp/server.trace.ndjson", readAt: DateTime.makeUnsafe("2026-05-05T10:00:00.000Z"), slowSpanThresholdMs: 1_000, files: [ { path: "/tmp/server.trace.ndjson.1", text: [ record({ name: "server.getConfig", traceId: "trace-a", spanId: "span-a", startMs: 1_000, durationMs: 50, }), "not-json", ].join("\n"), }, { path: "/tmp/server.trace.ndjson", text: [ record({ name: "orchestration.dispatch", traceId: "trace-b", spanId: "span-b", startMs: 2_000, durationMs: 1_500, exit: { _tag: "Failure", cause: "Provider crashed" }, events: [ { name: "provider failed", timeUnixNano: ns(3_400), attributes: { "effect.logLevel": "Error" }, }, ], }), record({ name: "orchestration.dispatch", traceId: "trace-c", spanId: "span-c", startMs: 4_000, durationMs: 250, exit: { _tag: "Failure", cause: "Provider crashed" }, }), record({ name: "git.status", traceId: "trace-d", spanId: "span-d", startMs: 5_000, durationMs: 25, exit: { _tag: "Interrupted", cause: "Interrupted" }, events: [ { name: "status delayed", timeUnixNano: ns(5_010), attributes: { "effect.logLevel": "Warning" }, }, ], }), ].join("\n"), }, ], }); assert.equal(diagnostics.recordCount, 4); assert.equal(DateTime.formatIso(diagnostics.readAt), "2026-05-05T10:00:00.000Z"); assert.equal( Option.match(diagnostics.firstSpanAt, { onNone: () => null, onSome: DateTime.formatIso, }), "1970-01-01T00:00:01.000Z", ); assert.equal( Option.match(diagnostics.lastSpanAt, { onNone: () => null, onSome: DateTime.formatIso, }), "1970-01-01T00:00:05.025Z", ); assert.equal(diagnostics.parseErrorCount, 1); assert.equal(diagnostics.failureCount, 2); assert.equal(diagnostics.interruptionCount, 1); assert.equal(diagnostics.slowSpanCount, 1); assert.equal(diagnostics.logLevelCounts.Error, 1); assert.equal(diagnostics.logLevelCounts.Warning, 1); assert.equal(diagnostics.commonFailures[0]?.name, "orchestration.dispatch"); assert.equal(diagnostics.commonFailures[0]?.count, 2); assert.equal(diagnostics.latestFailures[0]?.traceId, "trace-c"); assert.equal(diagnostics.slowestSpans[0]?.traceId, "trace-b"); assert.equal(diagnostics.latestWarningAndErrorLogs[0]?.message, "status delayed"); assert.equal(diagnostics.topSpansByCount[0]?.name, "orchestration.dispatch"); }), ); it.effect("returns a not-found diagnostic when no files are available", () => Effect.sync(() => { const diagnostics = TraceDiagnostics.aggregateTraceDiagnostics({ traceFilePath: "/tmp/missing.trace.ndjson", readAt: DateTime.makeUnsafe("2026-05-05T10:00:00.000Z"), files: [], }); assert.equal(diagnostics.recordCount, 0); assert.equal(Option.getOrUndefined(diagnostics.error)?.kind, "trace-file-not-found"); }), ); it.effect("preserves full failure causes and log messages", () => Effect.sync(() => { const longCause = `VcsProcessSpawnError: ${"missing executable ".repeat(80)}`.trim(); const longMessage = `provider warning: ${"retrying command ".repeat(80)}`.trim(); const diagnostics = TraceDiagnostics.aggregateTraceDiagnostics({ traceFilePath: "/tmp/server.trace.ndjson", readAt: DateTime.makeUnsafe("2026-05-05T10:00:00.000Z"), files: [ { path: "/tmp/server.trace.ndjson", text: record({ name: "VcsProcess.run", traceId: "trace-long", spanId: "span-long", startMs: 1_000, durationMs: 25, exit: { _tag: "Failure", cause: longCause }, events: [ { name: longMessage, timeUnixNano: ns(1_010), attributes: { "effect.logLevel": "Warning" }, }, ], }), }, ], }); assert.equal(diagnostics.latestFailures[0]?.cause, longCause); assert.equal(diagnostics.commonFailures[0]?.cause, longCause); assert.equal(diagnostics.latestWarningAndErrorLogs[0]?.message, longMessage); }), ); it.effect("keeps loaded trace data when one rotated trace file fails to read", () => Effect.gen(function* () { const traceFilePath = "/tmp/server.trace.ndjson"; 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.succeed( record({ name: "server.getConfig", traceId: "trace-a", spanId: "span-a", startMs: 1_000, durationMs: 50, }), ), }); 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)))); 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(diagnostics.scannedFilePaths, [`${traceFilePath}.1`, traceFilePath]); }), ); it.effect("keeps only the slowest span occurrences while aggregating large inputs", () => Effect.sync(() => { const diagnostics = TraceDiagnostics.aggregateTraceDiagnostics({ traceFilePath: "/tmp/server.trace.ndjson", readAt: DateTime.makeUnsafe("2026-05-05T10:00:00.000Z"), files: [ { path: "/tmp/server.trace.ndjson", text: Array.from({ length: 25 }, (_, index) => record({ name: `span-${index}`, traceId: `trace-${index}`, spanId: `span-${index}`, startMs: index * 1_000, durationMs: index, }), ).join("\n"), }, ], }); assert.equal(diagnostics.recordCount, 25); assert.equal(diagnostics.slowestSpans.length, 10); assert.deepStrictEqual( diagnostics.slowestSpans.map((span) => span.durationMs), [24, 23, 22, 21, 20, 19, 18, 17, 16, 15], ); }), ); });