From cfc7f212788d22d99014d38f6039ff0a2a6b84e7 Mon Sep 17 00:00:00 2001 From: Michael Arnaldi Date: Fri, 7 Nov 2025 11:18:58 +0100 Subject: [PATCH 1/3] Add logs to first propagated span --- .changeset/fast-shoes-appear.md | 17 +++++ packages/effect/src/internal/core-effect.ts | 13 ++-- packages/effect/src/internal/fiberRuntime.ts | 6 +- packages/opentelemetry/test/Tracer.test.ts | 78 ++++++++++++++++++-- 4 files changed, 100 insertions(+), 14 deletions(-) create mode 100644 .changeset/fast-shoes-appear.md diff --git a/.changeset/fast-shoes-appear.md b/.changeset/fast-shoes-appear.md new file mode 100644 index 00000000000..80a4adda4f8 --- /dev/null +++ b/.changeset/fast-shoes-appear.md @@ -0,0 +1,17 @@ +--- +"@effect/opentelemetry": patch +"effect": patch +--- + +Add logs to first propagated span, in the following case before this fix the log would not be added to the `p` span because `Effect.fn` adds a fake span for the purpose of adding a stack frame. + +```ts +import { Effect } from "effect" + +const f = Effect.fn(function* () { + yield* Effect.logWarning("FooBar") + return yield* Effect.fail("Oops") +}) + +const p = f().pipe(Effect.withSpan("p")) +``` diff --git a/packages/effect/src/internal/core-effect.ts b/packages/effect/src/internal/core-effect.ts index 2b90e1d0068..78cd5d51bcd 100644 --- a/packages/effect/src/internal/core-effect.ts +++ b/packages/effect/src/internal/core-effect.ts @@ -2070,12 +2070,13 @@ 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) -) +export 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 = ( diff --git a/packages/effect/src/internal/fiberRuntime.ts b/packages/effect/src/internal/fiberRuntime.ts index 44816b8243b..514840f2a96 100644 --- a/packages/effect/src/internal/fiberRuntime.ts +++ b/packages/effect/src/internal/fiberRuntime.ts @@ -1507,13 +1507,15 @@ export const tracerLogger = globalValue( logLevel, message }) => { - const span = Context.getOption( + const span = internalEffect.filterDisablePropagation(Context.getOption( fiberRefs.getOrDefault(context, core.currentContext), tracer.spanTag - ) + )) + if (span._tag === "None" || span.value._tag === "ExternalSpan") { return } + const clockService = Context.unsafeGet( fiberRefs.getOrDefault(context, defaultServices.currentServices), clock.clockTag diff --git a/packages/opentelemetry/test/Tracer.test.ts b/packages/opentelemetry/test/Tracer.test.ts index 0a2f6fa422e..7eefc8f4ded 100644 --- a/packages/opentelemetry/test/Tracer.test.ts +++ b/packages/opentelemetry/test/Tracer.test.ts @@ -4,16 +4,27 @@ import { assert, describe, expect, 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 Console from "effect/Console" import * as Effect from "effect/Effect" +import * as FiberRef from "effect/FiberRef" +import * as Layer from "effect/Layer" import * as Runtime from "effect/Runtime" import { OtelSpan } from "../src/internal/tracer.js" -const TracingLive = NodeSdk.layer(Effect.sync(() => ({ - resource: { - serviceName: "test" - }, - spanProcessor: [new SimpleSpanProcessor(new InMemorySpanExporter())] -}))) +class Exporter extends Effect.Service()("Exporter", { + effect: Effect.sync(() => ({ exporter: new InMemorySpanExporter() })) +}) {} + +const TracingLive = Layer.unwrapEffect(Effect.gen(function*() { + const { exporter } = yield* Exporter + + return NodeSdk.layer(Effect.sync(() => ({ + resource: { + serviceName: "test" + }, + spanProcessor: [new SimpleSpanProcessor(exporter)] + }))) +})).pipe(Layer.provideMerge(Exporter.Default)) // needed to test context propagation const contextManager = new AsyncHooksContextManager() @@ -123,4 +134,59 @@ describe("Tracer", () => { }) )) }) + + describe("Log Attributes", () => { + it.effect("propagates attributes with Effect.fnUntraced", () => + Effect.gen(function*() { + const f = Effect.fnUntraced(function*() { + yield* Effect.logWarning("FooBar") + return yield* Effect.fail("Oops") + }) + + const p = f().pipe(Effect.withSpan("p")) + + yield* Effect.ignore(p) + + const { exporter } = yield* Exporter + + assert.isNotEmpty(exporter.getFinishedSpans()[0].events.filter((_) => _.name === "FooBar")) + assert.isNotEmpty(exporter.getFinishedSpans()[0].events.filter((_) => _.name === "exception")) + }).pipe(Effect.provide(TracingLive))) + + it.effect("propagates attributes with Effect.fn(name)", () => + Effect.gen(function*() { + const f = Effect.fn("f")(function*() { + yield* Effect.logWarning("FooBar") + return yield* Effect.fail("Oops") + }) + + const p = f().pipe(Effect.withSpan("p")) + + yield* Effect.ignore(p) + + const { exporter } = yield* Exporter + + assert.isNotEmpty(exporter.getFinishedSpans()[0].events.filter((_) => _.name === "FooBar")) + assert.isNotEmpty(exporter.getFinishedSpans()[0].events.filter((_) => _.name === "exception")) + }).pipe(Effect.provide(TracingLive))) + + it.effect("propagates attributes with Effect.fn", () => + Effect.gen(function*() { + const f = Effect.fn(function*() { + yield* Effect.logWarning("FooBar") + return yield* Effect.fail("Oops") + }) + + const p = f().pipe(Effect.withSpan("p")) + + yield* Effect.ignore(p) + + const { exporter } = yield* Exporter + + yield* Console.log(Array.from(yield* FiberRef.get(FiberRef.currentLoggers))) + + assert.isNotEmpty(exporter.getFinishedSpans()[0].events.filter((_) => _.name === "FooBar")) + assert.isNotEmpty(exporter.getFinishedSpans()[0].events.filter((_) => _.name === "exception")) + }).pipe(Effect.provide(TracingLive))) + }) }) From 6ee024d8eedb439cf358d449786389876cd3a617 Mon Sep 17 00:00:00 2001 From: Michael Arnaldi Date: Fri, 7 Nov 2025 11:38:50 +0100 Subject: [PATCH 2/3] Fix Effect.currentSpan to skip fake spans --- .changeset/tasty-boxes-pump.md | 5 +++++ packages/effect/src/internal/core-effect.ts | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 .changeset/tasty-boxes-pump.md diff --git a/.changeset/tasty-boxes-pump.md b/.changeset/tasty-boxes-pump.md new file mode 100644 index 00000000000..bae981d674d --- /dev/null +++ b/.changeset/tasty-boxes-pump.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix Effect.currentSpan to skip fake spans diff --git a/packages/effect/src/internal/core-effect.ts b/packages/effect/src/internal/core-effect.ts index 78cd5d51bcd..4a0253dbdf4 100644 --- a/packages/effect/src/internal/core-effect.ts +++ b/packages/effect/src/internal/core-effect.ts @@ -2034,9 +2034,9 @@ export const currentParentSpan: Effect.Effect = core.flatMap( core.context(), (context) => { - const span = context.unsafeMap.get(internalTracer.spanTag.key) as Tracer.AnySpan | undefined - return span !== undefined && span._tag === "Span" - ? core.succeed(span) + const span = filterDisablePropagation(Context.getOption(context, internalTracer.spanTag)) + return span !== undefined && span._tag === "Some" && span.value._tag === "Span" + ? core.succeed(span.value) : core.fail(new core.NoSuchElementException()) } ) From 398209325bd3538faa80906a5d3645946ca52273 Mon Sep 17 00:00:00 2001 From: Michael Arnaldi Date: Fri, 7 Nov 2025 12:37:29 +0100 Subject: [PATCH 3/3] Fix annotateCurrentSpan, add Effect.currentPropagatedSpan --- .changeset/tasty-boxes-pump.md | 5 ----- .changeset/violet-years-stare.md | 5 +++++ packages/effect/src/Effect.ts | 6 ++++++ packages/effect/src/internal/core-effect.ts | 14 ++++++++++++-- 4 files changed, 23 insertions(+), 7 deletions(-) delete mode 100644 .changeset/tasty-boxes-pump.md create mode 100644 .changeset/violet-years-stare.md diff --git a/.changeset/tasty-boxes-pump.md b/.changeset/tasty-boxes-pump.md deleted file mode 100644 index bae981d674d..00000000000 --- a/.changeset/tasty-boxes-pump.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"effect": patch ---- - -Fix Effect.currentSpan to skip fake spans diff --git a/.changeset/violet-years-stare.md b/.changeset/violet-years-stare.md new file mode 100644 index 00000000000..1e320c0ca62 --- /dev/null +++ b/.changeset/violet-years-stare.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +Fix annotateCurrentSpan, add Effect.currentPropagatedSpan diff --git a/packages/effect/src/Effect.ts b/packages/effect/src/Effect.ts index b1e897d82b1..d9143c239b6 100644 --- a/packages/effect/src/Effect.ts +++ b/packages/effect/src/Effect.ts @@ -12998,6 +12998,12 @@ export const annotateCurrentSpan: { */ export const currentSpan: Effect = effect.currentSpan +/** + * @since 3.20.0 + * @category Tracing + */ +export const currentPropagatedSpan: Effect = effect.currentPropagatedSpan + /** * @since 2.0.0 * @category Tracing diff --git a/packages/effect/src/internal/core-effect.ts b/packages/effect/src/internal/core-effect.ts index 4a0253dbdf4..0ef68fb67b6 100644 --- a/packages/effect/src/internal/core-effect.ts +++ b/packages/effect/src/internal/core-effect.ts @@ -1966,7 +1966,7 @@ export const annotateCurrentSpan: { } = function(): Effect.Effect { const args = arguments return ignore(core.flatMap( - currentSpan, + currentPropagatedSpan, (span) => core.sync(() => { if (typeof args[0] === "string") { @@ -2032,10 +2032,20 @@ export const currentParentSpan: Effect.Effect = core.flatMap( + core.context(), + (context) => { + const span = context.unsafeMap.get(internalTracer.spanTag.key) as Tracer.AnySpan | undefined + return span !== undefined && span._tag === "Span" + ? core.succeed(span) + : core.fail(new core.NoSuchElementException()) + } +) + +export const currentPropagatedSpan: Effect.Effect = core.flatMap( core.context(), (context) => { const span = filterDisablePropagation(Context.getOption(context, internalTracer.spanTag)) - return span !== undefined && span._tag === "Some" && span.value._tag === "Span" + return span._tag === "Some" && span.value._tag === "Span" ? core.succeed(span.value) : core.fail(new core.NoSuchElementException()) }