261 lines · 9.0 KB · TypeScript
RawDownload
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<unknown>;}) {  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],      );    }),  );});