import * as NodeSdk from "@effect/opentelemetry/NodeSdk" import * as OtelTracer from "@effect/opentelemetry/OtelTracer" import { assert, describe, it } from "@effect/vitest" import * as OtelApi from "@opentelemetry/api" import { AsyncHooksContextManager } from "@opentelemetry/context-async-hooks" import { InMemorySpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base" import * as Cause from "effect/Cause" import * as Effect from "effect/Effect" import * as EffectTracer from "effect/Tracer" const TracingLive = NodeSdk.layer(Effect.sync(() => ({ resource: { serviceName: "test" }, spanProcessor: [new SimpleSpanProcessor(new InMemorySpanExporter())] }))) // needed to test context propagation const contextManager = new AsyncHooksContextManager() OtelApi.context.setGlobalContextManager(contextManager) describe("Tracer", () => { describe("provided", () => { it.effect("withSpan", () => Effect.gen(function*() { const span = yield* Effect.currentSpan assert.instanceOf(span, OtelTracer.OtelSpan) }).pipe( Effect.withSpan("ok"), Effect.provide(TracingLive) )) it.effect("withSpan links", () => Effect.gen(function*() { const linkedSpan = yield* Effect.makeSpanScoped("B") const span = yield* Effect.currentSpan.pipe( Effect.withSpan("A"), Effect.linkSpans(linkedSpan) ) assert.instanceOf(span, OtelTracer.OtelSpan) assert.lengthOf(span.links, 1) }).pipe( Effect.scoped, Effect.provide(TracingLive) )) it.effect("nested withSpan sets correct parent chain", () => Effect.gen(function*() { const child = yield* Effect.currentSpan.pipe( Effect.withSpan("child"), Effect.withSpan("parent") ) assert.instanceOf(child, OtelTracer.OtelSpan) assert.strictEqual(child.name, "child") assert.isDefined(child.parent) assert.strictEqual((child.parent.valueOrUndefined! as OtelTracer.OtelSpan).name, "parent") }).pipe( Effect.provide(TracingLive) )) it.effect("supervisor sets context", () => Effect.sync(() => { const context = OtelApi.context.active() assert.isDefined(OtelApi.trace.getSpan(context)) }).pipe( Effect.withSpan("ok"), Effect.provide(TracingLive) )) it.effect("supervisor sets context generator", () => Effect.gen(function*() { yield* Effect.yieldNow const context = OtelApi.context.active() assert.isDefined(OtelApi.trace.getSpan(context)) }).pipe( Effect.withSpan("ok"), Effect.provide(TracingLive) )) it.effect("currentOtelSpan", () => Effect.gen(function*() { const span = yield* Effect.currentSpan const otelSpan = yield* OtelTracer.currentOtelSpan assert.strictEqual((span as OtelTracer.OtelSpan).span, otelSpan) }).pipe( Effect.withSpan("ok"), Effect.provide(TracingLive) )) it.effect("preserves the sampling decision of generic external spans", () => Effect.gen(function*() { const span = yield* Effect.currentSpan assert.instanceOf(span, OtelTracer.OtelSpan) assert.strictEqual(span.span.spanContext().traceFlags, OtelApi.TraceFlags.NONE) }).pipe( Effect.withSpan("child"), Effect.withParentSpan(EffectTracer.externalSpan({ traceId: "1".repeat(32), spanId: "2".repeat(16), sampled: false })), Effect.provide(TracingLive) )) it.effect("records every pretty error", () => Effect.gen(function*() { const exporter = new InMemorySpanExporter() const spanProcessor = new SimpleSpanProcessor(exporter) const firstFailure = Cause.fail(new Error("first")) const secondFailure = Cause.fail(new Error("second")) const cause = Cause.combine(firstFailure, secondFailure) yield* Effect.failCause(cause).pipe( Effect.withSpan("error-span"), Effect.andThen(Effect.never), // keep the exporter alive Effect.provide(NodeSdk.layer(() => ({ resource: { serviceName: "test" }, spanProcessor: [spanProcessor] }))), Effect.forkChild({ startImmediately: true }) ) const spanData = exporter.getFinishedSpans()[0] if (spanData === undefined) { return yield* Effect.die("Missing span data") } const exceptionEvents = spanData.events.filter((event) => event.name === "exception") assert.lengthOf(exceptionEvents, 2) assert.strictEqual(spanData.status.message, "first") })) it.effect("renders nested error causes in the stacktrace", () => Effect.gen(function*() { const exporter = new InMemorySpanExporter() const spanProcessor = new SimpleSpanProcessor(exporter) const error = new Error("outer failure", { cause: new Error("inner cause") }) yield* Effect.die(error).pipe( Effect.withSpan("error-span"), Effect.andThen(Effect.never), // keep the exporter alive Effect.provide(NodeSdk.layer(() => ({ resource: { serviceName: "test" }, spanProcessor: [spanProcessor] }))), Effect.forkChild({ startImmediately: true }) ) const spanData = exporter.getFinishedSpans()[0] if (spanData === undefined) { return yield* Effect.die("Missing span data") } const exceptionEvent = spanData.events.find((event) => event.name === "exception") assert(exceptionEvent !== undefined) const stacktrace = exceptionEvent.attributes?.["exception.stacktrace"] assert.isString(stacktrace) assert.include(stacktrace as string, "[cause]: Error: inner cause") })) it.effect("withSpanContext", () => Effect.gen(function*() { const effect = Effect.gen(function*() { const span = yield* Effect.currentParentSpan assert(span._tag === "Span") if (span.parent._tag === "None") { return yield* Effect.die("No parent span") } return span.parent.value }).pipe(Effect.withSpan("child")) const services = yield* Effect.context() yield* Effect.promise(async () => { await OtelApi.trace.getTracer("test").startActiveSpan("otel-span", { root: true, attributes: { "root": "yes" } }, async (span) => { try { const parent = await effect.pipe( OtelTracer.withSpanContext(span.spanContext()), Effect.provideContext(services), Effect.runPromise ) const { spanId, traceId } = span.spanContext() assert.containsSubset(parent, { spanId, traceId }) } finally { span.end() } }) }) }).pipe( Effect.scoped, Effect.provide(TracingLive) )) }) describe("not provided", () => { it.effect("withSpan", () => Effect.gen(function*() { const span = yield* Effect.currentSpan assert.notInstanceOf(span, OtelTracer.OtelSpan) }).pipe( Effect.withSpan("ok") )) }) })