213 lines
7.3 KiB
TypeScript
213 lines
7.3 KiB
TypeScript
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<never>()
|
|
|
|
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")
|
|
))
|
|
})
|
|
})
|