import { assert, describe, it } from "@effect/vitest"; import * as NodeServices from "@effect/platform-node/NodeServices"; import * as Arr from "effect/Array"; import * as Cause from "effect/Cause"; 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 Order from "effect/Order"; import * as Path from "effect/Path"; import * as References from "effect/References"; import * as Schema from "effect/Schema"; import * as Tracer from "effect/Tracer"; import { causeErrorTag, compactTraceAttributes, errorTag, makeLocalFileTracer, makeTraceSink, type TraceRecord, } from "./observability.ts"; describe("errorTag", () => { it("reports structural tags without retaining arbitrary values", () => { assert.equal(errorTag({ _tag: "AcpRequestError" }), "AcpRequestError"); assert.equal(errorTag(new TypeError("secret-token-value")), "TypeError"); assert.equal(errorTag({ _tag: "secret token value" }), "TaggedError"); }); }); describe("causeErrorTag", () => { it("reports the tagged failure value instead of the Cause reason wrapper", () => { assert.equal( causeErrorTag(Cause.fail({ _tag: "ServerAuthInvalidCredentialError" })), "ServerAuthInvalidCredentialError", ); }); it("reports structural cause kinds when no typed failure exists", () => { assert.equal(causeErrorTag(Cause.die(new Error("unexpected"))), "Die"); assert.equal(causeErrorTag(Cause.interrupt()), "Interrupt"); }); }); const TraceRecordLine = Schema.Struct({ name: Schema.String, spanId: Schema.String, parentSpanId: Schema.optional(Schema.String), attributes: Schema.Record(Schema.String, Schema.Unknown), events: Schema.Array( Schema.Struct({ name: Schema.String, attributes: Schema.Record(Schema.String, Schema.Unknown), }), ), exit: Schema.optional( Schema.Struct({ _tag: Schema.String, }), ), }); const decodeTraceRecordLine = Schema.decodeUnknownSync(Schema.fromJsonString(TraceRecordLine)); const makeRecord = (name: string, suffix = ""): TraceRecord => ({ type: "effect-span", name, traceId: `trace-${name}-${suffix}`, spanId: `span-${name}-${suffix}`, sampled: true, kind: "internal", startTimeUnixNano: "1", endTimeUnixNano: "2", durationMs: 1, attributes: { payload: suffix, }, events: [], links: [], exit: { _tag: "Success", }, }); const readTraceRecords = Effect.fn("readTraceRecords")(function* (tracePath: string) { const fileSystem = yield* FileSystem.FileSystem; return (yield* fileSystem.readFileString(tracePath)) .trim() .split("\n") .filter((line) => line.length > 0) .map((line) => decodeTraceRecordLine(line)); }); const makeTestLayer = (tracePath: string) => Layer.mergeAll( Layer.effect( Tracer.Tracer, makeLocalFileTracer({ filePath: tracePath, maxBytes: 1024 * 1024, maxFiles: 2, batchWindowMs: 10_000, }), ), Logger.layer([Logger.tracerLogger], { mergeWithExisting: false }), Layer.succeed(References.MinimumLogLevel, "Info"), ); const nodeServicesIt = it.layer(NodeServices.layer); describe("observability", () => { it("normalizes circular arrays, maps, and sets without recursing forever", () => { const array: Array = ["alpha"]; array.push(array); const map = new Map(); map.set("self", map); const set = new Set(); set.add(set); assert.deepStrictEqual( compactTraceAttributes({ array, map, set, }), { array: ["alpha", "[Circular]"], map: { self: "[Circular]" }, set: ["[Circular]"], }, ); }); it("normalizes invalid dates without throwing", () => { // @effect-diagnostics-next-line globalDate:off const invalidDate = new Date("not-a-real-date"); assert.deepStrictEqual( compactTraceAttributes({ invalidDate, }), { invalidDate: "Invalid Date", }, ); }); nodeServicesIt("node services", (it) => { it.effect("flushes buffered trace records on close", () => Effect.scoped( Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-trace-sink-" }); const tracePath = path.join(tempDir, "shared.trace.ndjson"); const sink = yield* makeTraceSink({ filePath: tracePath, maxBytes: 1024, maxFiles: 2, batchWindowMs: 10_000, }); sink.push(makeRecord("alpha")); sink.push(makeRecord("beta")); yield* sink.close(); const lines = yield* readTraceRecords(tracePath); assert.equal(lines.length, 2); assert.equal(lines[0]?.name, "alpha"); assert.equal(lines[1]?.name, "beta"); }), ), ); it.effect("rotates the trace file when the configured max size is exceeded", () => Effect.scoped( Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-trace-sink-" }); const tracePath = path.join(tempDir, "shared.trace.ndjson"); const sink = yield* makeTraceSink({ filePath: tracePath, maxBytes: 180, maxFiles: 2, batchWindowMs: 10_000, }); for (let index = 0; index < 8; index += 1) { sink.push(makeRecord("rotate", `${index}-${"x".repeat(48)}`)); yield* sink.flush; } yield* sink.close(); const matchingFiles = Arr.sort( (yield* fileSystem.readDirectory(tempDir)).filter( (entry) => entry === "shared.trace.ndjson" || entry.startsWith("shared.trace.ndjson."), ), Order.String, ); assert.equal( matchingFiles.some((entry) => entry === "shared.trace.ndjson.1"), true, ); assert.equal( matchingFiles.some((entry) => entry === "shared.trace.ndjson.3"), false, ); }), ), ); it.effect("drops only the invalid trace record when serialization fails", () => Effect.scoped( Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-trace-sink-" }); const tracePath = path.join(tempDir, "shared.trace.ndjson"); const sink = yield* makeTraceSink({ filePath: tracePath, maxBytes: 1024, maxFiles: 2, batchWindowMs: 10_000, }); const circular: Array = []; circular.push(circular); sink.push(makeRecord("alpha")); sink.push({ ...makeRecord("invalid"), attributes: { circular, }, } as TraceRecord); sink.push(makeRecord("beta")); yield* sink.close(); const lines = yield* readTraceRecords(tracePath); assert.deepStrictEqual( lines.map((line) => line.name), ["alpha", "beta"], ); }), ), ); it.effect("writes nested spans to disk and captures log messages as span events", () => Effect.scoped( Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-local-tracer-" }); const tracePath = path.join(tempDir, "shared.trace.ndjson"); yield* Effect.scoped( Effect.gen(function* () { const program = Effect.gen(function* () { yield* Effect.annotateCurrentSpan({ "demo.parent": true, }); yield* Effect.logInfo("parent event"); yield* Effect.gen(function* () { yield* Effect.annotateCurrentSpan({ "demo.child": true, }); yield* Effect.logInfo("child event"); }).pipe(Effect.withSpan("child-span")); }).pipe(Effect.withSpan("parent-span")); yield* program.pipe(Effect.provide(makeTestLayer(tracePath))); }), ); const records = yield* readTraceRecords(tracePath); assert.equal(records.length, 2); const parent = records.find((record) => record.name === "parent-span"); const child = records.find((record) => record.name === "child-span"); assert.notEqual(parent, undefined); assert.notEqual(child, undefined); if (!parent || !child) { return; } assert.equal(child.parentSpanId, parent.spanId); assert.equal(parent.attributes["demo.parent"], true); assert.equal(child.attributes["demo.child"], true); assert.equal( parent.events.some((event) => event.name === "parent event"), true, ); assert.equal( child.events.some((event) => event.name === "child event"), true, ); assert.equal( child.events.some((event) => event.attributes["effect.logLevel"] === "INFO"), true, ); }), ), ); it.effect("serializes interrupted spans with an interrupted exit status", () => Effect.scoped( Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-local-tracer-" }); const tracePath = path.join(tempDir, "shared.trace.ndjson"); yield* Effect.scoped( Effect.exit( Effect.interrupt.pipe( Effect.withSpan("interrupt-span"), Effect.provide(makeTestLayer(tracePath)), ), ), ); const records = yield* readTraceRecords(tracePath); assert.equal(records.length, 1); assert.equal(records[0]?.name, "interrupt-span"); assert.equal(records[0]?.exit?._tag, "Interrupted"); }), ), ); }); });