import * as NodeSdk from "@effect/opentelemetry/NodeSdk" import { assert, describe, it } from "@effect/vitest" import { SeverityNumber } from "@opentelemetry/api-logs" import { InMemoryLogRecordExporter, SimpleLogRecordProcessor } from "@opentelemetry/sdk-logs" import { InMemorySpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base" import * as Clock from "effect/Clock" import * as Effect from "effect/Effect" import * as Layer from "effect/Layer" import * as References from "effect/References" describe("Logger", () => { describe("provided", () => { const exporter = new InMemoryLogRecordExporter() const TracingLive = NodeSdk.layer(Effect.sync(() => ({ resource: { serviceName: "test" }, logRecordProcessor: [new SimpleLogRecordProcessor({ exporter })] }))) it.effect("emits log records", () => Effect.gen(function*() { yield* Effect.log("test").pipe( Effect.repeat({ times: 9 }) ) assert.lengthOf(exporter.getFinishedLogRecords(), 10) }).pipe(Effect.provide(TracingLive))) it.effect("maps Effect LogLevel to OTel SeverityNumber spec values", () => { const severityExporter = new InMemoryLogRecordExporter() const SeverityLayer = NodeSdk.layer(Effect.sync(() => ({ resource: { serviceName: "test" }, logRecordProcessor: [new SimpleLogRecordProcessor({ exporter: severityExporter })] }))) return Effect.gen(function*() { yield* Effect.logTrace("trace") yield* Effect.logDebug("debug") yield* Effect.logInfo("info") yield* Effect.logWarning("warn") yield* Effect.logError("error") yield* Effect.logFatal("fatal") const records = severityExporter.getFinishedLogRecords() const byText = Object.fromEntries(records.map((r) => [r.severityText, r.severityNumber])) assert.strictEqual(byText.Trace, SeverityNumber.TRACE) assert.strictEqual(byText.Debug, SeverityNumber.DEBUG) assert.strictEqual(byText.Info, SeverityNumber.INFO) assert.strictEqual(byText.Warn, SeverityNumber.WARN) assert.strictEqual(byText.Error, SeverityNumber.ERROR) assert.strictEqual(byText.Fatal, SeverityNumber.FATAL) }).pipe( Effect.provide(SeverityLayer.pipe(Layer.provideMerge(Layer.succeed(References.MinimumLogLevel, "Trace")))) ) }) it.effect("uses monotonic clock timestamps and keeps them aligned with spans", () => { const logExporter = new InMemoryLogRecordExporter() const spanExporter = new InMemorySpanExporter() const timeNanos = 1_735_689_600_123_456_789n const expectedTime: readonly [number, number] = [ Number(timeNanos / BigInt(1_000_000_000)), Number(timeNanos % BigInt(1_000_000_000)) ] const skewedClock: Clock.Clock = { currentTimeMillisUnsafe: () => 1, currentTimeMillis: Effect.succeed(1), currentTimeNanosUnsafe: () => timeNanos, currentTimeNanos: Effect.succeed(timeNanos), sleep: () => Effect.void } const TracingLive = NodeSdk.layer(Effect.sync(() => ({ resource: { serviceName: "test" }, spanProcessor: [new SimpleSpanProcessor(spanExporter)], logRecordProcessor: [new SimpleLogRecordProcessor({ exporter: logExporter })] }))) return Effect.gen(function*() { yield* Effect.log("test").pipe( Effect.withSpan("parent") ) const logs = logExporter.getFinishedLogRecords() const spans = spanExporter.getFinishedSpans() assert.lengthOf(logs, 1) assert.lengthOf(spans, 1) const log = logs[0]! const span = spans[0]! assert.deepStrictEqual(log.hrTime, expectedTime) assert.deepStrictEqual(log.hrTimeObserved, expectedTime) assert.deepStrictEqual(log.hrTime, span.startTime) assert.strictEqual(log.attributes.spanId, span.spanContext().spanId) assert.strictEqual(log.attributes.traceId, span.spanContext().traceId) }).pipe( Effect.provide(TracingLive), Effect.provideService(Clock.Clock, skewedClock) ) }) }) describe("not provided", () => { const exporter = new InMemoryLogRecordExporter() const TracingLive = NodeSdk.layer(Effect.sync(() => ({ resource: { serviceName: "test" } }))) it.effect("withSpan", () => Effect.gen(function*() { yield* Effect.log("test") assert.lengthOf(exporter.getFinishedLogRecords(), 0) }).pipe(Effect.provide(TracingLive))) }) })