411 lines
13 KiB
TypeScript
411 lines
13 KiB
TypeScript
import { describe, it } from "@effect/vitest"
|
|
import { assertInclude, assertNone, deepStrictEqual, strictEqual } from "@effect/vitest/utils"
|
|
import { Cause, Context, Duration, Effect, Fiber, Layer, Tracer } from "effect"
|
|
import { TestClock } from "effect/testing"
|
|
import type { Span } from "effect/Tracer"
|
|
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
|
import { OtlpSerialization, OtlpTracer } from "effect/unstable/observability"
|
|
|
|
const getParent = (span: Tracer.Span): Tracer.AnySpan => {
|
|
if (span.parent._tag === "None") {
|
|
throw new Error("Expected parent span")
|
|
}
|
|
return span.parent.value
|
|
}
|
|
|
|
describe("Tracer", () => {
|
|
describe("Effect.withSpan", () => {
|
|
it.effect("should capture the stack trace", () =>
|
|
Effect.gen(function*() {
|
|
const cause = yield* Effect.die(new Error("boom")).pipe(
|
|
Effect.withSpan("C", {
|
|
annotations: Tracer.DisablePropagation.context(true)
|
|
}),
|
|
Effect.sandbox,
|
|
Effect.flip
|
|
)
|
|
|
|
assertInclude(Cause.pretty(cause), "Tracer.test.ts:20:41")
|
|
}))
|
|
|
|
it.effect("should set the parent span", () =>
|
|
Effect.gen(function*() {
|
|
const span = yield* Effect.currentSpan.pipe(
|
|
Effect.withSpan("A"),
|
|
Effect.withSpan("B")
|
|
)
|
|
|
|
strictEqual(span.name, "A")
|
|
strictEqual((getParent(span) as Span).name, "B")
|
|
}))
|
|
|
|
it.effect("should override the parent span when root is set to true", () =>
|
|
Effect.gen(function*() {
|
|
const span = yield* Effect.currentSpan.pipe(
|
|
Effect.withSpan("A", { root: true }),
|
|
Effect.withSpan("B")
|
|
)
|
|
|
|
strictEqual(span.name, "A")
|
|
assertNone(span.parent)
|
|
}))
|
|
|
|
it.effect("should set an external parent span", () =>
|
|
Effect.gen(function*() {
|
|
const span = yield* Effect.currentSpan.pipe(
|
|
Effect.withSpan("A", {
|
|
parent: {
|
|
_tag: "ExternalSpan",
|
|
spanId: "000",
|
|
traceId: "111",
|
|
sampled: true,
|
|
annotations: Context.empty()
|
|
}
|
|
})
|
|
)
|
|
|
|
strictEqual(span.name, "A")
|
|
strictEqual(getParent(span).spanId, "000")
|
|
}))
|
|
|
|
it.effect("should still apply minimum trace level with sampled parent spans", () =>
|
|
Effect.gen(function*() {
|
|
const span = yield* Effect.currentSpan.pipe(
|
|
Effect.withSpan("A", {
|
|
parent: Tracer.externalSpan({
|
|
spanId: "000",
|
|
traceId: "111",
|
|
sampled: true
|
|
}),
|
|
level: "Info"
|
|
}),
|
|
Effect.provideService(Tracer.MinimumTraceLevel, "Error")
|
|
)
|
|
|
|
strictEqual(span.sampled, false)
|
|
}))
|
|
|
|
it.effect("should not set the parent span when none exists", () =>
|
|
Effect.gen(function*() {
|
|
const span = yield* Effect.withSpan(Effect.currentSpan, "A")
|
|
|
|
strictEqual(span.name, "A")
|
|
assertNone(span.parent)
|
|
strictEqual(span.attributes.get("code.stacktrace"), undefined)
|
|
}))
|
|
|
|
it.effect("should preserve nested span context with OtlpTracer", () =>
|
|
Effect.gen(function*() {
|
|
const innerEffect = Effect.succeed(42).pipe(
|
|
Effect.withSpan("child-span", {
|
|
attributes: {
|
|
"test": "child"
|
|
}
|
|
})
|
|
)
|
|
|
|
const result = yield* innerEffect.pipe(
|
|
Effect.withSpan("parent-span", {
|
|
attributes: {
|
|
"test": "parent"
|
|
}
|
|
})
|
|
)
|
|
|
|
strictEqual(result, 42)
|
|
}).pipe(
|
|
Effect.provide(
|
|
OtlpTracer.layer({
|
|
url: "http://localhost:4318/v1/traces",
|
|
resource: {
|
|
serviceName: "test-service"
|
|
}
|
|
}).pipe(
|
|
Layer.provide(OtlpSerialization.layerJson),
|
|
Layer.provide(Layer.succeed(
|
|
HttpClient.HttpClient,
|
|
HttpClient.make((request) => Effect.succeed(HttpClientResponse.fromWeb(request, new Response())))
|
|
))
|
|
)
|
|
)
|
|
))
|
|
|
|
it.effect("should set the correct start and end time", () =>
|
|
Effect.gen(function*() {
|
|
const spanFiber = yield* Effect.currentSpan.pipe(
|
|
Effect.delay("1 second"),
|
|
Effect.withSpan("A"),
|
|
Effect.forkChild
|
|
)
|
|
|
|
yield* TestClock.adjust("2 seconds")
|
|
|
|
const span = yield* Fiber.join(spanFiber)
|
|
|
|
strictEqual(span.name, "A")
|
|
strictEqual(span.status.startTime, 0n)
|
|
strictEqual((span.status as any)["endTime"], 1000000000n)
|
|
strictEqual(span.status._tag, "Ended")
|
|
}))
|
|
|
|
it.effect("should capture logs as span events", () =>
|
|
Effect.gen(function*() {
|
|
yield* TestClock.adjust(Duration.millis(0.01))
|
|
|
|
const [span, fiberId] = yield* Effect.log("event").pipe(
|
|
Effect.andThen(Effect.all([Effect.currentSpan, Effect.fiberId])),
|
|
Effect.withSpan("A")
|
|
)
|
|
|
|
strictEqual(span.name, "A")
|
|
assertNone(span.parent)
|
|
deepStrictEqual((span as Tracer.NativeSpan).events, [
|
|
["event", 10_000n, {
|
|
"effect.fiberId": fiberId,
|
|
"effect.logLevel": "INFO"
|
|
}]
|
|
])
|
|
}))
|
|
|
|
it.effect("should attach attributes and kind to the current span", () =>
|
|
Effect.gen(function*() {
|
|
const result = yield* Effect.succeed(42).pipe(
|
|
Effect.withSpan("test-span", {
|
|
attributes: {
|
|
"code.filepath": "test.ts",
|
|
"code.lineno": 1
|
|
}
|
|
})
|
|
)
|
|
|
|
strictEqual(result, 42)
|
|
}))
|
|
})
|
|
|
|
describe("Effect.annotateSpans", () => {
|
|
it.effect("should allow adding span attributes as key/value pairs", () =>
|
|
Effect.gen(function*() {
|
|
const span = yield* Effect.currentSpan.pipe(
|
|
Effect.withSpan("A"),
|
|
Effect.annotateSpans("key", "value")
|
|
)
|
|
|
|
strictEqual(span.name, "A")
|
|
assertNone(span.parent)
|
|
strictEqual(span.attributes.get("key"), "value")
|
|
}))
|
|
|
|
it.effect("should allow adding span attributes as a record", () =>
|
|
Effect.gen(function*() {
|
|
const span = yield* Effect.currentSpan.pipe(
|
|
Effect.withSpan("A"),
|
|
Effect.annotateSpans({
|
|
key: "value",
|
|
key2: "value2"
|
|
})
|
|
)
|
|
|
|
strictEqual(span.attributes.get("key"), "value")
|
|
strictEqual(span.attributes.get("key2"), "value2")
|
|
}))
|
|
})
|
|
|
|
describe("Effect.useSpanScoped", () => {
|
|
it.effect("should control span lifetimes with a scope", () =>
|
|
Effect.gen(function*() {
|
|
const span = yield* Effect.scoped(Effect.makeSpanScoped("A"))
|
|
strictEqual(span.status._tag, "Ended")
|
|
strictEqual(span.attributes.get("code.stacktrace"), undefined)
|
|
}))
|
|
})
|
|
|
|
describe("Effect.annotateCurrentSpan", () => {
|
|
it.effect("should allow adding attributes to the current span", () =>
|
|
Effect.gen(function*() {
|
|
yield* Effect.annotateCurrentSpan("key", "value")
|
|
const span = yield* Effect.currentSpan
|
|
strictEqual(span.attributes.get("key"), "value")
|
|
}).pipe(Effect.withSpan("A")))
|
|
})
|
|
|
|
describe("Effect.withParentSpan", () => {
|
|
it.effect("should allow setting the parent span for the current span", () =>
|
|
Effect.gen(function*() {
|
|
const span = yield* Effect.currentSpan
|
|
strictEqual(getParent(span).spanId, "456")
|
|
}).pipe(
|
|
Effect.withSpan("A"),
|
|
Effect.withParentSpan({
|
|
_tag: "ExternalSpan",
|
|
traceId: "123",
|
|
spanId: "456",
|
|
sampled: true,
|
|
annotations: Context.empty()
|
|
})
|
|
))
|
|
})
|
|
|
|
describe("Effect.withTracerEnabled", () => {
|
|
it.effect("should allow enabling/disabling the tracer", () =>
|
|
Effect.gen(function*() {
|
|
const span = yield* Effect.currentSpan.pipe(
|
|
Effect.withSpan("A"),
|
|
Effect.withTracerEnabled(false)
|
|
)
|
|
const spanB = yield* Effect.currentSpan.pipe(
|
|
Effect.withSpan("B"),
|
|
Effect.withTracerEnabled(true)
|
|
)
|
|
|
|
strictEqual(span.name, "A")
|
|
strictEqual(span.spanId, "noop")
|
|
strictEqual(spanB.name, "B")
|
|
}))
|
|
})
|
|
|
|
describe("Effect.withTracerTiming", () => {
|
|
it.effect("should include timing information in spans when true", () =>
|
|
Effect.gen(function*() {
|
|
yield* TestClock.adjust("1 millis")
|
|
|
|
const span = yield* Effect.currentSpan.pipe(
|
|
Effect.withSpan("A"),
|
|
Effect.withTracerTiming(true) // default
|
|
)
|
|
|
|
strictEqual(span.status.startTime, 1_000_000n)
|
|
}))
|
|
|
|
it.effect("should not include timing information in spans when false", () =>
|
|
Effect.gen(function*() {
|
|
yield* TestClock.adjust("1 millis")
|
|
|
|
const span = yield* Effect.currentSpan.pipe(
|
|
Effect.withSpan("A"),
|
|
Effect.withTracerTiming(false)
|
|
)
|
|
|
|
strictEqual(span.status.startTime, 0n)
|
|
}))
|
|
})
|
|
|
|
describe("Effect.linkSpans", () => {
|
|
it.effect("should link two spans together", () =>
|
|
Effect.gen(function*() {
|
|
const childA = yield* Effect.makeSpan("childA")
|
|
const childB = yield* Effect.makeSpan("childB")
|
|
const currentSpan = yield* Effect.currentSpan.pipe(
|
|
Effect.withSpan("A", {
|
|
links: [{ span: childB, attributes: {} }]
|
|
}),
|
|
Effect.linkSpans(childA)
|
|
)
|
|
deepStrictEqual(
|
|
currentSpan.links.map((_) => _.span),
|
|
[childA, childB]
|
|
)
|
|
}))
|
|
})
|
|
|
|
describe("Layer.parentSpan", () => {
|
|
it.effect("should set the parent trace span for the layer constructor", () =>
|
|
Effect.gen(function*() {
|
|
const span = yield* Effect.makeSpan("child")
|
|
const parent = getParent(span) as Tracer.Span
|
|
strictEqual(parent.name, "parent")
|
|
strictEqual(span.attributes.get("code.stacktrace"), undefined)
|
|
strictEqual(parent.attributes.get("code.stacktrace"), undefined)
|
|
}).pipe(Effect.provide(Layer.unwrap(
|
|
Effect.map(
|
|
Effect.makeSpanScoped("parent"),
|
|
(span) => Layer.parentSpan(span)
|
|
)
|
|
))))
|
|
})
|
|
|
|
describe("Layer.span", () => {
|
|
it.effect("should create a new parent trace span for the layer constructor", () =>
|
|
Effect.gen(function*() {
|
|
const span = yield* Effect.makeSpan("child")
|
|
const parent = getParent(span) as Tracer.Span
|
|
strictEqual(parent.name, "parent")
|
|
strictEqual(parent.attributes.get("code.stacktrace"), undefined)
|
|
}).pipe(Effect.provide(Layer.span("parent"))))
|
|
|
|
it.effect("should call onEnd when the span is ending", () =>
|
|
Effect.gen(function*() {
|
|
let onEndCalled = false
|
|
const span = yield* Effect.currentSpan.pipe(
|
|
Effect.provide(Layer.span("span", {
|
|
onEnd: (span, _exit) =>
|
|
Effect.sync(() => {
|
|
strictEqual(span.name, "span")
|
|
onEndCalled = true
|
|
})
|
|
}))
|
|
)
|
|
strictEqual(span.name, "span")
|
|
strictEqual(onEndCalled, true)
|
|
}))
|
|
})
|
|
|
|
describe("Layer.withSpan", () => {
|
|
it.effect("sets the trace span for the layer constructor", () =>
|
|
Effect.gen(function*() {
|
|
let onEndCalled = false
|
|
const layer = Layer.effectDiscard(Effect.gen(function*() {
|
|
const span = yield* Effect.currentSpan
|
|
strictEqual(span.name, "span")
|
|
strictEqual(span.attributes.get("code.stacktrace"), undefined)
|
|
})).pipe(Layer.withSpan("span", {
|
|
onEnd: (span, _exit) =>
|
|
Effect.sync(() => {
|
|
strictEqual(span.name, "span")
|
|
onEndCalled = true
|
|
})
|
|
}))
|
|
|
|
const span = yield* Effect.currentSpan.pipe(
|
|
Effect.provide(layer),
|
|
Effect.option
|
|
)
|
|
|
|
assertNone(span)
|
|
strictEqual(onEndCalled, true)
|
|
}))
|
|
})
|
|
|
|
describe("Tracer.DisablePropagation", () => {
|
|
it.effect("should allow disabling span propagation via noop spans", () =>
|
|
Effect.gen(function*() {
|
|
const span = yield* Effect.currentSpan.pipe(
|
|
Effect.withSpan("A", {
|
|
annotations: Tracer.DisablePropagation.context(true)
|
|
})
|
|
)
|
|
const spanB = yield* Effect.currentSpan.pipe(
|
|
Effect.withSpan("B")
|
|
)
|
|
|
|
strictEqual(span.name, "A")
|
|
strictEqual(span.spanId, "noop")
|
|
strictEqual(spanB.name, "B")
|
|
}))
|
|
|
|
it.effect("should prevent a span from being used as a parent span", () =>
|
|
Effect.gen(function*() {
|
|
const span = yield* Effect.currentSpan.pipe(
|
|
Effect.withSpan("child"),
|
|
Effect.withSpan("disabled", {
|
|
annotations: Tracer.DisablePropagation.context(true)
|
|
}),
|
|
Effect.withSpan("parent")
|
|
)
|
|
strictEqual(span.name, "child")
|
|
const parent = getParent(span)
|
|
strictEqual(parent._tag, "Span")
|
|
strictEqual((parent as Span).name, "parent")
|
|
}))
|
|
})
|
|
})
|