diff --git a/.changeset/shiny-vans-sleep.md b/.changeset/shiny-vans-sleep.md
new file mode 100644
index 00000000000..438b0b976b3
--- /dev/null
+++ b/.changeset/shiny-vans-sleep.md
@@ -0,0 +1,5 @@
+---
+"effect": minor
+---
+
+add span annotation to disable propagation to the tracer
diff --git a/packages/effect/src/Tracer.ts b/packages/effect/src/Tracer.ts
index 038ddff03f9..e00d5a86837 100644
--- a/packages/effect/src/Tracer.ts
+++ b/packages/effect/src/Tracer.ts
@@ -164,3 +164,17 @@ export const externalSpan: (
*/
export const tracerWith: (f: (tracer: Tracer) => Effect.Effect) => Effect.Effect =
defaultServices.tracerWith
+
+/**
+ * @since 3.12.0
+ * @category annotations
+ */
+export interface DisablePropagation {
+ readonly _: unique symbol
+}
+
+/**
+ * @since 3.12.0
+ * @category annotations
+ */
+export const DisablePropagation: Context.Reference = internal.DisablePropagation
diff --git a/packages/effect/src/internal/core-effect.ts b/packages/effect/src/internal/core-effect.ts
index 5c2d29bf6de..207d7713f69 100644
--- a/packages/effect/src/internal/core-effect.ts
+++ b/packages/effect/src/internal/core-effect.ts
@@ -2017,61 +2017,75 @@ export const linkSpans = dual<
const bigint0 = BigInt(0)
+const filterDisablePropagation: (self: Option.Option) => Option.Option = Option.flatMap(
+ (span) =>
+ Context.get(span.context, internalTracer.DisablePropagation)
+ ? span._tag === "Span" ? filterDisablePropagation(span.parent) : Option.none()
+ : Option.some(span)
+)
+
/** @internal */
export const unsafeMakeSpan = (
fiber: FiberRuntime,
name: string,
options: Tracer.SpanOptions
) => {
- const enabled = fiber.getFiberRef(core.currentTracerEnabled)
- if (enabled === false) {
- return core.noopSpan(name)
- }
-
+ const disablePropagation = !fiber.getFiberRef(core.currentTracerEnabled) ||
+ (options.context && Context.get(options.context, internalTracer.DisablePropagation))
const context = fiber.getFiberRef(core.currentContext)
- const services = fiber.getFiberRef(defaultServices.currentServices)
-
- const tracer = Context.get(services, internalTracer.tracerTag)
- const clock = Context.get(services, Clock.Clock)
- const timingEnabled = fiber.getFiberRef(core.currentTracerTimingEnabled)
-
- const fiberRefs = fiber.getFiberRefs()
- const annotationsFromEnv = FiberRefs.get(fiberRefs, core.currentTracerSpanAnnotations)
- const linksFromEnv = FiberRefs.get(fiberRefs, core.currentTracerSpanLinks)
-
const parent = options.parent
? Option.some(options.parent)
: options.root
? Option.none()
- : Context.getOption(context, internalTracer.spanTag)
-
- const links = linksFromEnv._tag === "Some" ?
- options.links !== undefined ?
- [
- ...Chunk.toReadonlyArray(linksFromEnv.value),
- ...(options.links ?? [])
- ] :
- Chunk.toReadonlyArray(linksFromEnv.value) :
- options.links ?? Arr.empty()
-
- const span = tracer.span(
- name,
- parent,
- options.context ?? Context.empty(),
- links,
- timingEnabled ? clock.unsafeCurrentTimeNanos() : bigint0,
- options.kind ?? "internal"
- )
+ : filterDisablePropagation(Context.getOption(context, internalTracer.spanTag))
- if (typeof options.captureStackTrace === "function") {
- internalCause.spanToTrace.set(span, options.captureStackTrace)
- }
+ let span: Tracer.Span
- if (annotationsFromEnv._tag === "Some") {
- HashMap.forEach(annotationsFromEnv.value, (value, key) => span.attribute(key, value))
+ if (disablePropagation) {
+ span = core.noopSpan({
+ name,
+ parent,
+ context: Context.add(options.context ?? Context.empty(), internalTracer.DisablePropagation, true)
+ })
+ } else {
+ const services = fiber.getFiberRef(defaultServices.currentServices)
+
+ const tracer = Context.get(services, internalTracer.tracerTag)
+ const clock = Context.get(services, Clock.Clock)
+ const timingEnabled = fiber.getFiberRef(core.currentTracerTimingEnabled)
+
+ const fiberRefs = fiber.getFiberRefs()
+ const annotationsFromEnv = FiberRefs.get(fiberRefs, core.currentTracerSpanAnnotations)
+ const linksFromEnv = FiberRefs.get(fiberRefs, core.currentTracerSpanLinks)
+
+ const links = linksFromEnv._tag === "Some" ?
+ options.links !== undefined ?
+ [
+ ...Chunk.toReadonlyArray(linksFromEnv.value),
+ ...(options.links ?? [])
+ ] :
+ Chunk.toReadonlyArray(linksFromEnv.value) :
+ options.links ?? Arr.empty()
+
+ span = tracer.span(
+ name,
+ parent,
+ options.context ?? Context.empty(),
+ links,
+ timingEnabled ? clock.unsafeCurrentTimeNanos() : bigint0,
+ options.kind ?? "internal"
+ )
+
+ if (annotationsFromEnv._tag === "Some") {
+ HashMap.forEach(annotationsFromEnv.value, (value, key) => span.attribute(key, value))
+ }
+ if (options.attributes !== undefined) {
+ Object.entries(options.attributes).forEach(([k, v]) => span.attribute(k, v))
+ }
}
- if (options.attributes !== undefined) {
- Object.entries(options.attributes).forEach(([k, v]) => span.attribute(k, v))
+
+ if (typeof options.captureStackTrace === "function") {
+ internalCause.spanToTrace.set(span, options.captureStackTrace)
}
return span
diff --git a/packages/effect/src/internal/core.ts b/packages/effect/src/internal/core.ts
index e30e50c7ce4..2254c4ef588 100644
--- a/packages/effect/src/internal/core.ts
+++ b/packages/effect/src/internal/core.ts
@@ -3063,14 +3063,11 @@ export const currentSpanFromFiber = (fiber: Fiber.RuntimeFiber): Opt
return span !== undefined && span._tag === "Span" ? Option.some(span) : Option.none()
}
-const NoopSpanProto: Tracer.Span = {
+const NoopSpanProto: Omit = {
_tag: "Span",
spanId: "noop",
traceId: "noop",
- name: "noop",
sampled: false,
- parent: Option.none(),
- context: Context.empty(),
status: {
_tag: "Ended",
startTime: BigInt(0),
@@ -3086,8 +3083,8 @@ const NoopSpanProto: Tracer.Span = {
}
/** @internal */
-export const noopSpan = (name: string): Tracer.Span => {
- const span = Object.create(NoopSpanProto)
- span.name = name
- return span
-}
+export const noopSpan = (options: {
+ readonly name: string
+ readonly parent: Option.Option
+ readonly context: Context.Context
+}): Tracer.Span => Object.assign(Object.create(NoopSpanProto), options)
diff --git a/packages/effect/src/internal/tracer.ts b/packages/effect/src/internal/tracer.ts
index 6370e6ce96a..fd327826ce1 100644
--- a/packages/effect/src/internal/tracer.ts
+++ b/packages/effect/src/internal/tracer.ts
@@ -3,6 +3,7 @@
*/
import * as Context from "../Context.js"
import type * as Exit from "../Exit.js"
+import { constFalse } from "../Function.js"
import type * as Option from "../Option.js"
import type * as Tracer from "../Tracer.js"
@@ -135,3 +136,8 @@ export const addSpanStackTrace = (options: Tracer.SpanOptions | undefined): Trac
}
}
}
+
+/** @internal */
+export const DisablePropagation = Context.Reference()("effect/Tracer/DisablePropagation", {
+ defaultValue: constFalse
+})
diff --git a/packages/effect/test/Tracer.test.ts b/packages/effect/test/Tracer.test.ts
index d41574e5eb6..3d99d47eb5b 100644
--- a/packages/effect/test/Tracer.test.ts
+++ b/packages/effect/test/Tracer.test.ts
@@ -1,3 +1,4 @@
+import { Cause, Tracer } from "effect"
import * as Context from "effect/Context"
import { millis, seconds } from "effect/Duration"
import * as Effect from "effect/Effect"
@@ -274,6 +275,44 @@ it.effect("withTracerEnabled", () =>
assert.deepEqual(spanB.name, "B")
}))
+describe("Tracer.DisablePropagation", () => {
+ it.effect("creates noop span", () =>
+ Effect.gen(function*() {
+ const span = yield* Effect.currentSpan.pipe(
+ Effect.withSpan("A", { context: Tracer.DisablePropagation.context(true) })
+ )
+ const spanB = yield* Effect.currentSpan.pipe(
+ Effect.withSpan("B")
+ )
+
+ assert.deepEqual(span.name, "A")
+ assert.deepEqual(span.spanId, "noop")
+ assert.deepEqual(spanB.name, "B")
+ }))
+
+ it.effect("captures stack", () =>
+ Effect.gen(function*() {
+ const cause = yield* Effect.die(new Error("boom")).pipe(
+ Effect.withSpan("C", { context: Tracer.DisablePropagation.context(true) }),
+ Effect.sandbox,
+ Effect.flip
+ )
+ assert.include(Cause.pretty(cause), "Tracer.test.ts:295")
+ }))
+
+ it.effect("isnt used as parent span", () =>
+ Effect.gen(function*() {
+ const span = yield* Effect.currentSpan.pipe(
+ Effect.withSpan("child"),
+ Effect.withSpan("disabled", { context: Tracer.DisablePropagation.context(true) }),
+ Effect.withSpan("parent")
+ )
+ assert.strictEqual(span.name, "child")
+ assert(span.parent._tag === "Some" && span.parent.value._tag === "Span")
+ assert.strictEqual(span.parent.value.name, "parent")
+ }))
+})
+
it.effect("includes trace when errored", () =>
Effect.gen(function*() {
let maybeSpan: undefined | Span
@@ -290,7 +329,7 @@ it.effect("includes trace when errored", () =>
})
yield* Effect.flip(getSpan("fail"))
assert.isDefined(maybeSpan)
- assert.include(maybeSpan!.attributes.get("code.stacktrace"), "Tracer.test.ts:291:24")
+ assert.include(maybeSpan!.attributes.get("code.stacktrace"), "Tracer.test.ts:330:24")
}))
describe("functionWithSpan", () => {