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", () => {