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/.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 2b90e1d0068..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") { @@ -2041,6 +2041,16 @@ export const currentSpan: Effect.Effect = core.flatMap( + core.context(), + (context) => { + const span = filterDisablePropagation(Context.getOption(context, internalTracer.spanTag)) + return span._tag === "Some" && span.value._tag === "Span" + ? core.succeed(span.value) + : core.fail(new core.NoSuchElementException()) + } +) + /* @internal */ export const linkSpans = dual< ( @@ -2070,12 +2080,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 8362eb2b34a..52ffccf95d2 100644 --- a/packages/effect/src/internal/fiberRuntime.ts +++ b/packages/effect/src/internal/fiberRuntime.ts @@ -1517,13 +1517,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))) + }) })