diff --git a/.changeset/cyan-kiwis-juggle.md b/.changeset/cyan-kiwis-juggle.md new file mode 100644 index 0000000000..9a766d4fe4 --- /dev/null +++ b/.changeset/cyan-kiwis-juggle.md @@ -0,0 +1,7 @@ +--- +"effect": minor +--- + +add Cause.prettyErrors api + +You can use this to extract `Error` instances from a `Cause`, that have clean stack traces and have had span information added to them. diff --git a/.changeset/few-bikes-cheer.md b/.changeset/few-bikes-cheer.md new file mode 100644 index 0000000000..e1361cecc7 --- /dev/null +++ b/.changeset/few-bikes-cheer.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add span stack trace to rendered causes diff --git a/.changeset/lovely-frogs-scream.md b/.changeset/lovely-frogs-scream.md new file mode 100644 index 0000000000..c82b348496 --- /dev/null +++ b/.changeset/lovely-frogs-scream.md @@ -0,0 +1,19 @@ +--- +"effect": minor +--- + +add Effect.functionWithSpan + +Allows you to define an effectful function that is wrapped with a span. + +```ts +import { Effect } from "effect" + +const getTodo = Effect.functionWithSpan({ + body: (id: number) => Effect.succeed(`Got todo ${id}!`), + options: (id) => ({ + name: `getTodo-${id}`, + attributes: { id } + }) +}) +``` diff --git a/.changeset/tiny-zebras-work.md b/.changeset/tiny-zebras-work.md new file mode 100644 index 0000000000..b67f80da2a --- /dev/null +++ b/.changeset/tiny-zebras-work.md @@ -0,0 +1,9 @@ +--- +"effect": minor +"@effect/experimental": patch +"@effect/platform": patch +"@effect/rpc": patch +"@effect/sql": patch +--- + +capture stack trace for tracing spans diff --git a/.changeset/warm-clouds-matter.md b/.changeset/warm-clouds-matter.md new file mode 100644 index 0000000000..b514686d55 --- /dev/null +++ b/.changeset/warm-clouds-matter.md @@ -0,0 +1,5 @@ +--- +"@effect/opentelemetry": patch +--- + +properly record exceptions in otel spans diff --git a/packages/effect/src/Cause.ts b/packages/effect/src/Cause.ts index 9e90fb0b3f..a57557c00c 100644 --- a/packages/effect/src/Cause.ts +++ b/packages/effect/src/Cause.ts @@ -36,6 +36,7 @@ import type { Pipeable } from "./Pipeable.js" import type { Predicate, Refinement } from "./Predicate.js" import type * as Sink from "./Sink.js" import type * as Stream from "./Stream.js" +import type { Span } from "./Tracer.js" import type { Covariant, NoInfer } from "./Types.js" /** @@ -914,6 +915,22 @@ export const isUnknownException: (u: unknown) => u is UnknownException = core.is */ export const pretty: (cause: Cause) => string = internal.pretty +/** + * @since 3.2.0 + * @category models + */ +export interface PrettyError extends Error { + readonly span: Span | undefined +} + +/** + * Returns the specified `Cause` as a pretty-printed string. + * + * @since 3.2.0 + * @category rendering + */ +export const prettyErrors: (cause: Cause) => Array = internal.prettyErrors + /** * Returns the original, unproxied, instance of a thrown error * diff --git a/packages/effect/src/Effect.ts b/packages/effect/src/Effect.ts index 9c18eb4066..0f35c9e78a 100644 --- a/packages/effect/src/Effect.ts +++ b/packages/effect/src/Effect.ts @@ -5364,6 +5364,46 @@ export const withSpan: { ): Effect> } = effect.withSpan +/** + * Wraps a function that returns an effect with a new span for tracing. + * + * @since 3.2.0 + * @category models + */ +export interface FunctionWithSpanOptions { + readonly name: string + readonly attributes?: Record | undefined + readonly links?: ReadonlyArray | undefined + readonly parent?: Tracer.AnySpan | undefined + readonly root?: boolean | undefined + readonly context?: Context.Context | undefined + readonly kind?: Tracer.SpanKind | undefined +} + +/** + * Wraps a function that returns an effect with a new span for tracing. + * + * @since 3.2.0 + * @category tracing + * @example + * import { Effect } from "effect" + * + * const getTodo = Effect.functionWithSpan({ + * body: (id: number) => Effect.succeed(`Got todo ${id}!`), + * options: (id) => ({ + * name: `getTodo-${id}`, + * attributes: { id } + * }) + * }) + */ +export const functionWithSpan: , Ret extends Effect>( + options: { + readonly body: (...args: Args) => Ret + readonly options: FunctionWithSpanOptions | ((...args: Args) => FunctionWithSpanOptions) + readonly captureStackTrace?: boolean | undefined + } +) => (...args: Args) => Unify.Unify = effect.functionWithSpan + /** * Wraps the effect with a new span for tracing. * diff --git a/packages/effect/src/Tracer.ts b/packages/effect/src/Tracer.ts index 37996a5852..69a33e4f69 100644 --- a/packages/effect/src/Tracer.ts +++ b/packages/effect/src/Tracer.ts @@ -92,6 +92,7 @@ export interface SpanOptions { readonly root?: boolean | undefined readonly context?: Context.Context | undefined readonly kind?: SpanKind | undefined + readonly captureStackTrace?: boolean | string | undefined } /** diff --git a/packages/effect/src/internal/cause.ts b/packages/effect/src/internal/cause.ts index b7bc12babf..c87a5d56a2 100644 --- a/packages/effect/src/internal/cause.ts +++ b/packages/effect/src/internal/cause.ts @@ -10,8 +10,8 @@ import * as HashSet from "../HashSet.js" import { NodeInspectSymbol, toJSON } from "../Inspectable.js" import * as Option from "../Option.js" import { pipeArguments } from "../Pipeable.js" -import { hasProperty, isFunction } from "../Predicate.js" import type { Predicate, Refinement } from "../Predicate.js" +import { hasProperty, isFunction } from "../Predicate.js" import type { AnySpan, Span } from "../Tracer.js" import type { NoInfer } from "../Types.js" import { getBugErrorMessage } from "./errors.js" @@ -968,53 +968,36 @@ export const reduceWithContext = dual< // Pretty Printing // ----------------------------------------------------------------------------- -const filterStack = (stack: string) => { - const lines = stack.split("\n") - const out: Array = [] - for (let i = 0; i < lines.length; i++) { - out.push(lines[i].replace(/at .*effect_instruction_i.*\((.*)\)/, "at $1")) - if (lines[i].includes("effect_instruction_i")) { - return out.join("\n") - } - } - return out.join("\n") -} - /** @internal */ export const pretty = (cause: Cause.Cause): string => { if (isInterruptedOnly(cause)) { return "All fibers interrupted without errors." } - const final = prettyErrors(cause).map((e) => { - let message = e.message - if (e.stack) { - message += `\r\n${filterStack(e.stack)}` - } - if (e.span) { - let current: Span | AnySpan | undefined = e.span - let i = 0 - while (current && current._tag === "Span" && i < 10) { - message += `\r\n at ${current.name}` - current = Option.getOrUndefined(current.parent) - i++ - } - } - return message - }).join("\r\n") - return final + return prettyErrors(cause).map((e) => e.stack).join("\n") } -class PrettyError { - constructor( - readonly message: string, - readonly stack: string | undefined, - readonly span: Span | undefined - ) {} +class PrettyError extends globalThis.Error implements Cause.PrettyError { + constructor(originalError: unknown) { + const prevLimit = Error.stackTraceLimit + Error.stackTraceLimit = 0 + super(prettyErrorMessage(originalError), { cause: originalError }) + Error.stackTraceLimit = prevLimit + this.name = originalError instanceof Error ? originalError.name : "Error" + this.stack = prettyErrorStack( + this.message, + originalError instanceof Error && originalError.stack + ? originalError.stack + : "", + this.span + ) + } + + get span(): Span | undefined { + return hasProperty(this.cause, spanSymbol) ? this.cause[spanSymbol] as Span : undefined + } + toJSON() { - const out: any = { message: this.message } - if (this.stack) { - out.stack = this.stack - } + const out: any = { message: this.message, stack: this.stack } if (this.span) { out.span = this.span } @@ -1058,29 +1041,55 @@ export const prettyErrorMessage = (u: unknown): string => { return `Error: ${JSON.stringify(u)}` } -const spanSymbol = Symbol.for("effect/SpanAnnotation") +const locationRegex = /\((.*)\)/ + +const prettyErrorStack = (message: string, stack: string, span?: Span | undefined): string => { + const out: Array = [message] + const lines = stack.split("\n") -const defaultRenderError = (error: unknown): PrettyError => { - const span: any = hasProperty(error, spanSymbol) && error[spanSymbol] - if (error instanceof Error) { - return new PrettyError( - prettyErrorMessage(error), - error.stack?.split("\n").filter((_) => _.match(/at (.*)/)).join("\n"), - span + for (let i = 1; i < lines.length; i++) { + if (lines[i].includes("effect_cutpoint")) { + break + } + out.push( + lines[i].replace(/at .*effect_instruction_i.*\((.*)\)/, "at $1").replace(/EffectPrimitive\.\w+/, "") ) + if (lines[i].includes("effect_instruction_i")) { + break + } } - return new PrettyError(prettyErrorMessage(error), void 0, span) + + if (span) { + let current: Span | AnySpan | undefined = span + let i = 0 + while (current && current._tag === "Span" && i < 10) { + const stack = current.attributes.get("code.stacktrace") + if (typeof stack === "string") { + const locationMatch = stack.match(locationRegex) + const location = locationMatch ? locationMatch[1] : stack.replace(/^at /, "") + out.push(` at ${current.name} (${location})`) + } else { + out.push(` at ${current.name}`) + } + current = Option.getOrUndefined(current.parent) + i++ + } + } + + return out.join("\n") } +const spanSymbol = Symbol.for("effect/SpanAnnotation") + /** @internal */ -export const prettyErrors = (cause: Cause.Cause): ReadonlyArray => +export const prettyErrors = (cause: Cause.Cause): Array => reduceWithContext(cause, void 0, { - emptyCase: (): ReadonlyArray => [], + emptyCase: (): Array => [], dieCase: (_, unknownError) => { - return [defaultRenderError(unknownError)] + return [new PrettyError(unknownError)] }, failCase: (_, error) => { - return [defaultRenderError(error)] + return [new PrettyError(error)] }, interruptCase: () => [], parallelCase: (_, l, r) => [...l, ...r], diff --git a/packages/effect/src/internal/channel.ts b/packages/effect/src/internal/channel.ts index d5c33048b1..fe36316738 100644 --- a/packages/effect/src/internal/channel.ts +++ b/packages/effect/src/internal/channel.ts @@ -2310,29 +2310,47 @@ export const updateService = dual< ))) /** @internal */ -export const withSpan = dual< +export const withSpan: { ( name: string, options?: Tracer.SpanOptions - ) => ( + ): ( self: Channel.Channel - ) => Channel.Channel>, + ) => Channel.Channel> ( self: Channel.Channel, name: string, options?: Tracer.SpanOptions - ) => Channel.Channel> ->(3, (self, name, options) => - unwrapScoped( - Effect.flatMap( - Effect.context(), - (context) => - Effect.map( - Effect.makeSpanScoped(name, options), - (span) => core.provideContext(self, Context.add(context, tracer.spanTag, span)) - ) + ): Channel.Channel> +} = function() { + const dataFirst = typeof arguments[0] !== "string" + const name = dataFirst ? arguments[1] : arguments[0] + const options = tracer.addSpanStackTrace(dataFirst ? arguments[2] : arguments[1]) + if (dataFirst) { + const self = arguments[0] + return unwrapScoped( + Effect.flatMap( + Effect.context(), + (context) => + Effect.map( + Effect.makeSpanScoped(name, options), + (span) => core.provideContext(self, Context.add(context, tracer.spanTag, span)) + ) + ) + ) + } + return (self: Effect.Effect) => + unwrapScoped( + Effect.flatMap( + Effect.context(), + (context) => + Effect.map( + Effect.makeSpanScoped(name, options), + (span) => core.provideContext(self, Context.add(context, tracer.spanTag, span)) + ) + ) ) - ) as any) +} as any /** @internal */ export const writeAll = ( diff --git a/packages/effect/src/internal/core-effect.ts b/packages/effect/src/internal/core-effect.ts index a20595a956..d8fed1c20a 100644 --- a/packages/effect/src/internal/core-effect.ts +++ b/packages/effect/src/internal/core-effect.ts @@ -26,6 +26,7 @@ import * as Ref from "../Ref.js" import type * as runtimeFlagsPatch from "../RuntimeFlagsPatch.js" import * as Tracer from "../Tracer.js" import type { NoInfer } from "../Types.js" +import type { Unify } from "../Unify.js" import { yieldWrapGet } from "../Utils.js" import * as internalCause from "./cause.js" import { clockTag } from "./clock.js" @@ -2011,7 +2012,7 @@ const bigint0 = BigInt(0) export const unsafeMakeSpan = ( fiber: FiberRuntime, name: string, - options?: Tracer.SpanOptions + options: Tracer.SpanOptions ) => { const enabled = fiber.getFiberRef(core.currentTracerEnabled) if (enabled === false) { @@ -2029,34 +2030,38 @@ export const unsafeMakeSpan = ( const annotationsFromEnv = FiberRefs.get(fiberRefs, core.currentTracerSpanAnnotations) const linksFromEnv = FiberRefs.get(fiberRefs, core.currentTracerSpanLinks) - const parent = options?.parent + const parent = options.parent ? Option.some(options.parent) - : options?.root + : options.root ? Option.none() : Context.getOption(context, internalTracer.spanTag) const links = linksFromEnv._tag === "Some" ? - options?.links !== undefined ? + options.links !== undefined ? [ ...Chunk.toReadonlyArray(linksFromEnv.value), - ...(options?.links ?? []) + ...(options.links ?? []) ] : Chunk.toReadonlyArray(linksFromEnv.value) : - options?.links ?? Arr.empty() + options.links ?? Arr.empty() const span = tracer.span( name, parent, - options?.context ?? Context.empty(), + options.context ?? Context.empty(), links, timingEnabled ? clock.unsafeCurrentTimeNanos() : bigint0, - options?.kind ?? "internal" + options.kind ?? "internal" ) + if (typeof options.captureStackTrace === "string") { + span.attribute("code.stacktrace", options.captureStackTrace) + } + if (annotationsFromEnv._tag === "Some") { HashMap.forEach(annotationsFromEnv.value, (value, key) => span.attribute(key, value)) } - if (options?.attributes !== undefined) { + if (options.attributes !== undefined) { Object.entries(options.attributes).forEach(([k, v]) => span.attribute(k, v)) } @@ -2067,7 +2072,10 @@ export const unsafeMakeSpan = ( export const makeSpan = ( name: string, options?: Tracer.SpanOptions -): Effect.Effect => core.withFiberRuntime((fiber) => core.succeed(unsafeMakeSpan(fiber, name, options))) +): Effect.Effect => { + options = internalTracer.addSpanStackTrace(options) + return core.withFiberRuntime((fiber) => core.succeed(unsafeMakeSpan(fiber, name, options))) +} /* @internal */ export const spanAnnotations: Effect.Effect> = core @@ -2092,7 +2100,7 @@ export const useSpan: { evaluate: (span: Tracer.Span) => Effect.Effect ] ) => { - const options: Tracer.SpanOptions | undefined = args.length === 1 ? undefined : args[0] + const options = internalTracer.addSpanStackTrace(args.length === 1 ? undefined : args[0]) const evaluate: (span: Tracer.Span) => Effect.Effect = args[args.length - 1] return core.withFiberRuntime((fiber) => { @@ -2118,25 +2126,66 @@ export const withParentSpan = dual< >(2, (self, span) => provideService(self, internalTracer.spanTag, span)) /** @internal */ -export const withSpan = dual< +export const withSpan: { ( name: string, - options?: Tracer.SpanOptions - ) => (self: Effect.Effect) => Effect.Effect>, + options?: Tracer.SpanOptions | undefined + ): (self: Effect.Effect) => Effect.Effect> ( self: Effect.Effect, name: string, - options?: Tracer.SpanOptions - ) => Effect.Effect> ->( - (args) => typeof args[0] !== "string", - (self, name, options) => - useSpan( - name, - options ?? {}, - (span) => withParentSpan(self, span) - ) -) + options?: Tracer.SpanOptions | undefined + ): Effect.Effect> +} = function() { + const dataFirst = typeof arguments[0] !== "string" + const name = dataFirst ? arguments[1] : arguments[0] + const options = internalTracer.addSpanStackTrace(dataFirst ? arguments[2] : arguments[1]) + if (dataFirst) { + const self = arguments[0] + return useSpan(name, options, (span) => withParentSpan(self, span)) + } + return (self: Effect.Effect) => useSpan(name, options, (span) => withParentSpan(self, span)) +} as any + +export const functionWithSpan = , Ret extends Effect.Effect>( + options: { + readonly body: (...args: Args) => Ret + readonly options: Effect.FunctionWithSpanOptions | ((...args: Args) => Effect.FunctionWithSpanOptions) + readonly captureStackTrace?: boolean | undefined + } +): (...args: Args) => Unify => + (function(this: any) { + let captureStackTrace: string | boolean = options.captureStackTrace ?? false + if (options.captureStackTrace !== false) { + const limit = Error.stackTraceLimit + Error.stackTraceLimit = 2 + const error = new Error() + Error.stackTraceLimit = limit + if (error.stack !== undefined) { + const stack = error.stack.trim().split("\n") + captureStackTrace = stack.slice(2).join("\n").trim() + } + } + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this + const args = arguments + return core.suspend(() => { + const opts = typeof options.options === "function" + ? options.options.apply(null, arguments as any) + : options.options + + return withSpan( + core.custom(options.body, function() { + return this.effect_instruction_i0.apply(self, args as any) + }), + opts.name, + { + ...opts, + captureStackTrace + } + ) + }) + }) as any // ------------------------------------------------------------------------------------- // optionality diff --git a/packages/effect/src/internal/core.ts b/packages/effect/src/internal/core.ts index 2147a2b57b..0924417510 100644 --- a/packages/effect/src/internal/core.ts +++ b/packages/effect/src/internal/core.ts @@ -459,6 +459,10 @@ export const as: { /* @internal */ export const asVoid = (self: Effect.Effect): Effect.Effect => as(self, void 0) +function commitCallCutpoint(this: any) { + return this.effect_cutpoint() +} + /* @internal */ export const custom: { (i0: X, body: (this: { effect_instruction_i0: X }) => Effect.Effect): Effect.Effect @@ -477,23 +481,24 @@ export const custom: { ): Effect.Effect } = function() { const wrapper = new EffectPrimitive(OpCodes.OP_COMMIT) as any + wrapper.commit = commitCallCutpoint switch (arguments.length) { case 2: { wrapper.effect_instruction_i0 = arguments[0] - wrapper.commit = arguments[1] + wrapper.effect_cutpoint = arguments[1] break } case 3: { wrapper.effect_instruction_i0 = arguments[0] wrapper.effect_instruction_i1 = arguments[1] - wrapper.commit = arguments[2] + wrapper.effect_cutpoint = arguments[2] break } case 4: { wrapper.effect_instruction_i0 = arguments[0] wrapper.effect_instruction_i1 = arguments[1] wrapper.effect_instruction_i2 = arguments[2] - wrapper.commit = arguments[3] + wrapper.effect_cutpoint = arguments[3] break } default: { diff --git a/packages/effect/src/internal/fiberRuntime.ts b/packages/effect/src/internal/fiberRuntime.ts index 15fd069f90..da3a020d5d 100644 --- a/packages/effect/src/internal/fiberRuntime.ts +++ b/packages/effect/src/internal/fiberRuntime.ts @@ -3622,8 +3622,9 @@ export const interruptWhenPossible = dual< export const makeSpanScoped = ( name: string, options?: Tracer.SpanOptions | undefined -): Effect.Effect => - core.uninterruptible( +): Effect.Effect => { + options = tracer.addSpanStackTrace(options) + return core.uninterruptible( core.withFiberRuntime((fiber) => { const scope = Context.unsafeGet(fiber.getFiberRef(core.currentContext), scopeTag) const span = internalEffect.unsafeMakeSpan(fiber, name, options) @@ -3641,27 +3642,37 @@ export const makeSpanScoped = ( ) }) ) +} /* @internal */ export const withTracerScoped = (value: Tracer.Tracer): Effect.Effect => fiberRefLocallyScopedWith(defaultServices.currentServices, Context.add(tracer.tracerTag, value)) /** @internal */ -export const withSpanScoped = dual< +export const withSpanScoped: { ( name: string, options?: Tracer.SpanOptions - ) => (self: Effect.Effect) => Effect.Effect | Scope.Scope>, + ): (self: Effect.Effect) => Effect.Effect> ( self: Effect.Effect, name: string, options?: Tracer.SpanOptions - ) => Effect.Effect | Scope.Scope> ->( - (args) => typeof args[0] !== "string", - (self, name, options) => + ): Effect.Effect> +} = function() { + const dataFirst = typeof arguments[0] !== "string" + const name = dataFirst ? arguments[1] : arguments[0] + const options = tracer.addSpanStackTrace(dataFirst ? arguments[2] : arguments[1]) + if (dataFirst) { + const self = arguments[0] + return core.flatMap( + makeSpanScoped(name, tracer.addSpanStackTrace(options)), + (span) => internalEffect.provideService(self, tracer.spanTag, span) + ) + } + return (self: Effect.Effect) => core.flatMap( - makeSpanScoped(name, options), + makeSpanScoped(name, tracer.addSpanStackTrace(options)), (span) => internalEffect.provideService(self, tracer.spanTag, span) ) -) +} as any diff --git a/packages/effect/src/internal/layer.ts b/packages/effect/src/internal/layer.ts index bb88a424c7..749ab4ccff 100644 --- a/packages/effect/src/internal/layer.ts +++ b/packages/effect/src/internal/layer.ts @@ -1113,7 +1113,7 @@ export const unwrapScoped = ( // ----------------------------------------------------------------------------- /** @internal */ -export const withSpan = dual< +export const withSpan: { ( name: string, options?: Tracer.SpanOptions & { @@ -1121,7 +1121,7 @@ export const withSpan = dual< | ((span: Tracer.Span, exit: Exit.Exit) => Effect.Effect) | undefined } - ) => (self: Layer.Layer) => Layer.Layer>, + ): (self: Layer.Layer) => Layer.Layer> ( self: Layer.Layer, name: string, @@ -1130,19 +1130,42 @@ export const withSpan = dual< | ((span: Tracer.Span, exit: Exit.Exit) => Effect.Effect) | undefined } - ) => Layer.Layer> ->((args) => isLayer(args[0]), (self, name, options) => - unwrapScoped( - core.map( - options?.onEnd - ? core.tap( - fiberRuntime.makeSpanScoped(name, options), - (span) => fiberRuntime.addFinalizer((exit) => options.onEnd!(span, exit)) - ) - : fiberRuntime.makeSpanScoped(name, options), - (span) => withParentSpan(self, span) + ): Layer.Layer> +} = function() { + const dataFirst = typeof arguments[0] !== "string" + const name = dataFirst ? arguments[1] : arguments[0] + const options = tracer.addSpanStackTrace(dataFirst ? arguments[2] : arguments[1]) as Tracer.SpanOptions & { + readonly onEnd?: + | ((span: Tracer.Span, exit: Exit.Exit) => Effect.Effect) + | undefined + } + if (dataFirst) { + const self = arguments[0] + return unwrapScoped( + core.map( + options?.onEnd + ? core.tap( + fiberRuntime.makeSpanScoped(name, options), + (span) => fiberRuntime.addFinalizer((exit) => options.onEnd!(span, exit)) + ) + : fiberRuntime.makeSpanScoped(name, options), + (span) => withParentSpan(self, span) + ) ) - )) + } + return (self: Layer.Layer) => + unwrapScoped( + core.map( + options?.onEnd + ? core.tap( + fiberRuntime.makeSpanScoped(name, options), + (span) => fiberRuntime.addFinalizer((exit) => options.onEnd!(span, exit)) + ) + : fiberRuntime.makeSpanScoped(name, options), + (span) => withParentSpan(self, span) + ) + ) +} as any /** @internal */ export const withParentSpan = dual< diff --git a/packages/effect/src/internal/layer/circular.ts b/packages/effect/src/internal/layer/circular.ts index be46420d4d..791b143fcd 100644 --- a/packages/effect/src/internal/layer/circular.ts +++ b/packages/effect/src/internal/layer/circular.ts @@ -196,8 +196,9 @@ export const span = ( | ((span: Tracer.Span, exit: Exit.Exit) => Effect.Effect) | undefined } -): Layer.Layer => - layer.scoped( +): Layer.Layer => { + options = tracer.addSpanStackTrace(options) as any + return layer.scoped( tracer.spanTag, options?.onEnd ? core.tap( @@ -206,6 +207,7 @@ export const span = ( ) : fiberRuntime.makeSpanScoped(name, options) ) +} /** @internal */ export const setTracer = (tracer: Tracer.Tracer): Layer.Layer => diff --git a/packages/effect/src/internal/runtime.ts b/packages/effect/src/internal/runtime.ts index 36f0cdbfc7..5b0c8f289f 100644 --- a/packages/effect/src/internal/runtime.ts +++ b/packages/effect/src/internal/runtime.ts @@ -173,9 +173,9 @@ class FiberFailureImpl extends Error implements Runtime.FiberFailure { const prettyErrors = InternalCause.prettyErrors(cause) if (prettyErrors.length > 0) { const head = prettyErrors[0] - this.name = head.message.split(":")[0] - this.message = head.message.substring(this.name.length + 2) - this.stack = InternalCause.pretty(cause) + this.name = head.name + this.message = head.message + this.stack = head.stack! } this.name = `(FiberFailure) ${this.name}` @@ -188,7 +188,7 @@ class FiberFailureImpl extends Error implements Runtime.FiberFailure { } } toString(): string { - return "(FiberFailure) " + InternalCause.pretty(this[FiberFailureCauseId]) + return "(FiberFailure) " + (this.stack ?? this.message) } [Inspectable.NodeInspectSymbol](): unknown { return this.toString() diff --git a/packages/effect/src/internal/stream.ts b/packages/effect/src/internal/stream.ts index 15ec125e44..8d84d49427 100644 --- a/packages/effect/src/internal/stream.ts +++ b/packages/effect/src/internal/stream.ts @@ -50,6 +50,7 @@ import * as SinkEndReason from "./stream/sinkEndReason.js" import * as ZipAllState from "./stream/zipAllState.js" import * as ZipChunksState from "./stream/zipChunksState.js" import * as InternalTake from "./take.js" +import * as InternalTracer from "./tracer.js" /** @internal */ const StreamSymbolKey = "effect/Stream" @@ -6815,17 +6816,26 @@ export const whenEffect = dual< ) /** @internal */ -export const withSpan = dual< +export const withSpan: { ( name: string, options?: Tracer.SpanOptions - ) => (self: Stream.Stream) => Stream.Stream>, + ): (self: Stream.Stream) => Stream.Stream> ( self: Stream.Stream, name: string, options?: Tracer.SpanOptions - ) => Stream.Stream> ->(3, (self, name, options) => new StreamImpl(channel.withSpan(toChannel(self), name, options))) + ): Stream.Stream> +} = function() { + const dataFirst = typeof arguments[0] !== "string" + const name = dataFirst ? arguments[1] : arguments[0] + const options = InternalTracer.addSpanStackTrace(dataFirst ? arguments[2] : arguments[1]) + if (dataFirst) { + const self = arguments[0] + return new StreamImpl(channel.withSpan(toChannel(self), name, options)) + } + return (self: Stream.Stream) => new StreamImpl(channel.withSpan(toChannel(self), name, options)) +} as any /** @internal */ export const zip = dual< diff --git a/packages/effect/src/internal/tracer.ts b/packages/effect/src/internal/tracer.ts index 7fa29fb9b0..a4c8ac4887 100644 --- a/packages/effect/src/internal/tracer.ts +++ b/packages/effect/src/internal/tracer.ts @@ -106,3 +106,24 @@ export const externalSpan = (options: { sampled: options.sampled ?? true, context: options.context ?? Context.empty() }) + +/** @internal */ +export const addSpanStackTrace = (options: Tracer.SpanOptions | undefined): Tracer.SpanOptions => { + if (options?.captureStackTrace === false) { + return options + } else if (options?.captureStackTrace !== undefined && typeof options.captureStackTrace !== "boolean") { + return options + } + const limit = Error.stackTraceLimit + Error.stackTraceLimit = 3 + const traceError = new Error() + Error.stackTraceLimit = limit + if (traceError.stack === undefined) { + return { ...options, captureStackTrace: false } + } + const stack = traceError.stack.split("\n") + if (!stack[3]) { + return { ...options, captureStackTrace: false } + } + return { ...options, captureStackTrace: stack[3].trim() } +} diff --git a/packages/effect/test/Cause.test.ts b/packages/effect/test/Cause.test.ts index 207ee5dc97..6308715398 100644 --- a/packages/effect/test/Cause.test.ts +++ b/packages/effect/test/Cause.test.ts @@ -223,13 +223,13 @@ describe("Cause", () => { it("Sequential", () => { expect(String(Cause.sequential(Cause.fail("failure 1"), Cause.fail("failure 2")))).toEqual( - `Error: failure 1\r\nError: failure 2` + `Error: failure 1\nError: failure 2` ) }) it("Parallel", () => { expect(String(Cause.parallel(Cause.fail("failure 1"), Cause.fail("failure 2")))).toEqual( - `Error: failure 1\r\nError: failure 2` + `Error: failure 1\nError: failure 2` ) }) }) diff --git a/packages/effect/test/Effect/cause-rendering.test.ts b/packages/effect/test/Effect/cause-rendering.test.ts index 93d805d4e4..8ab99d5fcf 100644 --- a/packages/effect/test/Effect/cause-rendering.test.ts +++ b/packages/effect/test/Effect/cause-rendering.test.ts @@ -16,7 +16,9 @@ describe("Effect", () => { ))) const rendered = Cause.pretty(cause) assert.include(rendered, "spanA") + assert.include(rendered, "cause-rendering.test.ts:12") assert.include(rendered, "spanB") + assert.include(rendered, "cause-rendering.test.ts:11") })) it.effect("catchTag should not invalidate traces", () => Effect.gen(function*($) { @@ -98,4 +100,21 @@ describe("Effect", () => { const pretty = Cause.pretty(cause) assert.include(pretty, "cause-rendering.test.ts") })) + + it.effect("functionWithSpan PrettyError stack", () => + Effect.gen(function*() { + const fail = Effect.functionWithSpan({ + body: (_id: number) => Effect.fail(new Error("boom")), + options: (id) => ({ name: `span-${id}` }) + }) + const cause = yield* fail(123).pipe(Effect.sandbox, Effect.flip) + const prettyErrors = Cause.prettyErrors(cause) + assert.strictEqual(prettyErrors.length, 1) + const error = prettyErrors[0] + assert.strictEqual(error.name, "Error") + assert.notInclude(error.stack, "/internal/") + assert.include(error.stack, "cause-rendering.test.ts:107") + assert.include(error.stack, "span-123") + assert.include(error.stack, "cause-rendering.test.ts:110") + })) }) diff --git a/packages/effect/test/Tracer.test.ts b/packages/effect/test/Tracer.test.ts index 80334fdc9f..d373906a4f 100644 --- a/packages/effect/test/Tracer.test.ts +++ b/packages/effect/test/Tracer.test.ts @@ -14,13 +14,11 @@ import { assert, describe } from "vitest" describe("Tracer", () => { describe("withSpan", () => { it.effect("no parent", () => - Effect.gen(function*($) { - const span = yield* $( - Effect.withSpan("A")(Effect.currentSpan) - ) - + Effect.gen(function*() { + const span = yield* Effect.withSpan("A")(Effect.currentSpan) assert.deepEqual(span.name, "A") assert.deepEqual(span.parent, Option.none()) + assert.include(span.attributes.get("code.stacktrace"), "Tracer.test.ts:18") })) it.effect("parent", () => @@ -77,203 +75,226 @@ describe("Tracer", () => { assert.deepEqual((span.status as any)["endTime"], 1000000000n) assert.deepEqual(span.status._tag, "Ended") })) + }) - it.effect("annotateSpans", () => - Effect.gen(function*($) { - const span = yield* $( - Effect.annotateSpans( - Effect.withSpan("A")(Effect.currentSpan), - "key", - "value" - ) + it.effect("annotateSpans", () => + Effect.gen(function*($) { + const span = yield* $( + Effect.annotateSpans( + Effect.withSpan("A")(Effect.currentSpan), + "key", + "value" ) + ) - assert.deepEqual(span.name, "A") - assert.deepEqual(span.parent, Option.none()) - assert.deepEqual(span.attributes.get("key"), "value") - })) + assert.deepEqual(span.name, "A") + assert.deepEqual(span.parent, Option.none()) + assert.deepEqual(span.attributes.get("key"), "value") + })) - it.effect("annotateSpans record", () => - Effect.gen(function*($) { - const span = yield* $( - Effect.annotateSpans( - Effect.withSpan("A")(Effect.currentSpan), - { key: "value", key2: "value2" } - ) + it.effect("annotateSpans record", () => + Effect.gen(function*($) { + const span = yield* $( + Effect.annotateSpans( + Effect.withSpan("A")(Effect.currentSpan), + { key: "value", key2: "value2" } ) + ) - assert.deepEqual(span.attributes.get("key"), "value") - assert.deepEqual(span.attributes.get("key2"), "value2") - })) - - it.effect("logger", () => - Effect.gen(function*($) { - yield* $(TestClock.adjust(millis(0.01))) - - const [span, fiberId] = yield* $( - Effect.log("event"), - Effect.zipRight(Effect.all([Effect.currentSpan, Effect.fiberId])), - Effect.withSpan("A") - ) + assert.deepEqual(span.attributes.get("key"), "value") + assert.deepEqual(span.attributes.get("key2"), "value2") + })) - assert.deepEqual(span.name, "A") - assert.deepEqual(span.parent, Option.none()) - assert.deepEqual((span as NativeSpan).events, [["event", 10000n, { - "effect.fiberId": FiberId.threadName(fiberId), - "effect.logLevel": "INFO" - }]]) - })) + it.effect("logger", () => + Effect.gen(function*($) { + yield* $(TestClock.adjust(millis(0.01))) - it.effect("withTracerTiming false", () => - Effect.gen(function*($) { - yield* $(TestClock.adjust(millis(1))) + const [span, fiberId] = yield* $( + Effect.log("event"), + Effect.zipRight(Effect.all([Effect.currentSpan, Effect.fiberId])), + Effect.withSpan("A") + ) - const span = yield* $( - Effect.withSpan("A")(Effect.currentSpan), - Effect.withTracerTiming(false) - ) + assert.deepEqual(span.name, "A") + assert.deepEqual(span.parent, Option.none()) + assert.deepEqual((span as NativeSpan).events, [["event", 10000n, { + "effect.fiberId": FiberId.threadName(fiberId), + "effect.logLevel": "INFO" + }]]) + })) - assert.deepEqual(span.status.startTime, 0n) - })) + it.effect("withTracerTiming false", () => + Effect.gen(function*($) { + yield* $(TestClock.adjust(millis(1))) - it.effect("useSpanScoped", () => - Effect.gen(function*(_) { - const span = yield* _(Effect.scoped(Effect.makeSpanScoped("A"))) - assert.deepEqual(span.status._tag, "Ended") - })) + const span = yield* $( + Effect.withSpan("A")(Effect.currentSpan), + Effect.withTracerTiming(false) + ) - it.effect("annotateCurrentSpan", () => - Effect.gen(function*(_) { - yield* _(Effect.annotateCurrentSpan("key", "value")) - const span = yield* _(Effect.currentSpan) - assert.deepEqual(span.attributes.get("key"), "value") - }).pipe( - Effect.withSpan("A") - )) + assert.deepEqual(span.status.startTime, 0n) + })) - it.effect("withParentSpan", () => - Effect.gen(function*(_) { - const span = yield* _(Effect.currentSpan) - assert.deepEqual( - span.parent.pipe( - Option.map((_) => _.spanId) - ), - Option.some("456") - ) - }).pipe( - Effect.withSpan("A"), - Effect.withParentSpan({ - _tag: "ExternalSpan", - traceId: "123", - spanId: "456", - sampled: true, - context: Context.empty() - }) - )) + it.effect("useSpanScoped", () => + Effect.gen(function*() { + const span = yield* Effect.scoped(Effect.makeSpanScoped("A")) + assert.deepEqual(span.status._tag, "Ended") + assert.include(span.attributes.get("code.stacktrace"), "Tracer.test.ts:140") + })) - it.effect("Layer.parentSpan", () => - Effect.gen(function*(_) { - const span = yield* _(Effect.makeSpan("child")) - assert.deepEqual( - span.parent.pipe( - Option.filter((span): span is Span => span._tag === "Span"), - Option.map((span) => span.name) - ), - Option.some("parent") + it.effect("annotateCurrentSpan", () => + Effect.gen(function*(_) { + yield* _(Effect.annotateCurrentSpan("key", "value")) + const span = yield* _(Effect.currentSpan) + assert.deepEqual(span.attributes.get("key"), "value") + }).pipe( + Effect.withSpan("A") + )) + + it.effect("withParentSpan", () => + Effect.gen(function*(_) { + const span = yield* _(Effect.currentSpan) + assert.deepEqual( + span.parent.pipe( + Option.map((_) => _.spanId) + ), + Option.some("456") + ) + }).pipe( + Effect.withSpan("A"), + Effect.withParentSpan({ + _tag: "ExternalSpan", + traceId: "123", + spanId: "456", + sampled: true, + context: Context.empty() + }) + )) + + it.effect("Layer.parentSpan", () => + Effect.gen(function*() { + const span = yield* Effect.makeSpan("child") + const parent = yield* Option.filter(span.parent, (span): span is Span => span._tag === "Span") + assert.deepEqual(parent.name, "parent") + assert.include(span.attributes.get("code.stacktrace"), "Tracer.test.ts:176") + assert.include(parent.attributes.get("code.stacktrace"), "Tracer.test.ts:184") + }).pipe( + Effect.provide(Layer.unwrapScoped( + Effect.map( + Effect.makeSpanScoped("parent"), + (span) => Layer.parentSpan(span) ) - }).pipe( - Effect.provide(Layer.unwrapScoped( - Effect.map( - Effect.makeSpanScoped("parent"), - (span) => Layer.parentSpan(span) - ) - )) )) + )) + + it.effect("Layer.span", () => + Effect.gen(function*() { + const span = yield* Effect.makeSpan("child") + const parent = span.parent.pipe( + Option.filter((span): span is Span => span._tag === "Span"), + Option.getOrThrow + ) + assert.strictEqual(parent.name, "parent") + assert.include(parent.attributes.get("code.stacktrace"), "Tracer.test.ts:200") + }).pipe( + Effect.provide(Layer.span("parent")) + )) + + it.effect("Layer.span onEnd", () => + Effect.gen(function*(_) { + let onEndCalled = false + const span = yield* _( + Effect.currentSpan, + Effect.provide(Layer.span("span", { + onEnd: (span, _exit) => + Effect.sync(() => { + assert.strictEqual(span.name, "span") + onEndCalled = true + }) + })) + ) + assert.strictEqual(span.name, "span") + assert.strictEqual(onEndCalled, true) + })) - it.effect("Layer.span", () => - Effect.gen(function*(_) { - const span = yield* _(Effect.makeSpan("child")) - assert.deepEqual( - span.parent.pipe( - Option.filter((span): span is Span => span._tag === "Span"), - Option.map((span) => span.name) - ), - Option.some("parent") - ) - }).pipe( - Effect.provide(Layer.span("parent")) - )) + it.effect("linkSpans", () => + Effect.gen(function*(_) { + const childA = yield* _(Effect.makeSpan("childA")) + const childB = yield* _(Effect.makeSpan("childB")) + const currentSpan = yield* _( + Effect.currentSpan, + Effect.withSpan("A", { links: [{ _tag: "SpanLink", span: childB, attributes: {} }] }), + Effect.linkSpans(childA) + ) + assert.includeMembers( + currentSpan.links.map((_) => _.span), + [childB, childA] + ) + })) - it.effect("Layer.span onEnd", () => - Effect.gen(function*(_) { - let onEndCalled = false - const span = yield* _( - Effect.currentSpan, - Effect.provide(Layer.span("span", { - onEnd: (span, _exit) => - Effect.sync(() => { - assert.strictEqual(span.name, "span") - onEndCalled = true - }) - })) - ) + it.effect("Layer.withSpan", () => + Effect.gen(function*(_) { + let onEndCalled = false + const layer = Layer.effectDiscard(Effect.gen(function*() { + const span = yield* Effect.currentSpan assert.strictEqual(span.name, "span") - assert.strictEqual(onEndCalled, true) - })) - - it.effect("linkSpans", () => - Effect.gen(function*(_) { - const childA = yield* _(Effect.makeSpan("childA")) - const childB = yield* _(Effect.makeSpan("childB")) - const currentSpan = yield* _( - Effect.currentSpan, - Effect.withSpan("A", { links: [{ _tag: "SpanLink", span: childB, attributes: {} }] }), - Effect.linkSpans(childA) - ) - assert.includeMembers( - currentSpan.links.map((_) => _.span), - [childB, childA] - ) - })) + assert.include(span.attributes.get("code.stacktrace"), "Tracer.test.ts:243") + })).pipe( + Layer.withSpan("span", { + onEnd: (span, _exit) => + Effect.sync(() => { + assert.strictEqual(span.name, "span") + onEndCalled = true + }) + }) + ) - it.effect("Layer.withSpan", () => - Effect.gen(function*(_) { - let onEndCalled = false - const layer = Layer.effectDiscard(Effect.gen(function*(_) { - const span = yield* _(Effect.currentSpan) - assert.strictEqual(span.name, "span") - })).pipe( - Layer.withSpan("span", { - onEnd: (span, _exit) => - Effect.sync(() => { - assert.strictEqual(span.name, "span") - onEndCalled = true - }) - }) - ) + const span = yield* _(Effect.currentSpan, Effect.provide(layer), Effect.option) - const span = yield* _(Effect.currentSpan, Effect.provide(layer), Effect.option) + assert.deepEqual(span, Option.none()) + assert.strictEqual(onEndCalled, true) + })) +}) - assert.deepEqual(span, Option.none()) - assert.strictEqual(onEndCalled, true) - })) +it.effect("withTracerEnabled", () => + Effect.gen(function*($) { + const span = yield* $( + Effect.currentSpan, + Effect.withSpan("A"), + Effect.withTracerEnabled(false) + ) + const spanB = yield* $( + Effect.currentSpan, + Effect.withSpan("B"), + Effect.withTracerEnabled(true) + ) + + assert.deepEqual(span.name, "A") + assert.deepEqual(span.spanId, "noop") + assert.deepEqual(spanB.name, "B") + })) + +describe("functionWithSpan", () => { + const getSpan = Effect.functionWithSpan({ + body: (_id: string) => Effect.currentSpan, + options: (id) => ({ + name: `span-${id}`, + attributes: { id } + }) }) - it.effect("withTracerEnabled", () => - Effect.gen(function*($) { - const span = yield* $( - Effect.currentSpan, - Effect.withSpan("A"), - Effect.withTracerEnabled(false) - ) - const spanB = yield* $( - Effect.currentSpan, - Effect.withSpan("B"), - Effect.withTracerEnabled(true) - ) + it.effect("no parent", () => + Effect.gen(function*() { + const span = yield* getSpan("A") + assert.deepEqual(span.name, "span-A") + assert.deepEqual(span.parent, Option.none()) + assert.include(span.attributes.get("code.stacktrace"), "Tracer.test.ts:288") + })) - assert.deepEqual(span.name, "A") - assert.deepEqual(span.spanId, "noop") - assert.deepEqual(spanB.name, "B") + it.effect("parent", () => + Effect.gen(function*() { + const span = yield* Effect.withSpan("B")(getSpan("A")) + assert.deepEqual(span.name, "span-A") + assert.deepEqual(Option.map(span.parent, (span) => (span as Span).name), Option.some("B")) })) }) diff --git a/packages/experimental/src/Machine.ts b/packages/experimental/src/Machine.ts index 53f1f3642f..5ac3517b96 100644 --- a/packages/experimental/src/Machine.ts +++ b/packages/experimental/src/Machine.ts @@ -542,7 +542,8 @@ export const boot = < "effect.machine": runState.identifier, ...request }, - kind: "client" + kind: "client", + captureStackTrace: false }, (span) => Queue.offer(requests, [request, deferred, span, true]).pipe( Effect.zipRight(Deferred.await(deferred)), @@ -565,7 +566,8 @@ export const boot = < "effect.machine": runState.identifier, ...request }, - kind: "client" + kind: "client", + captureStackTrace: false }, (span) => Queue.offer(requests, [request, deferred, span, true])) } ) @@ -762,7 +764,8 @@ export const boot = < parent: span, attributes: { "effect.machine": runState.identifier - } + }, + captureStackTrace: false }) } else if (span !== undefined) { handler = Effect.provideService(handler, Tracer.ParentSpan, span) diff --git a/packages/opentelemetry/src/internal/tracer.ts b/packages/opentelemetry/src/internal/tracer.ts index ecccf7d953..2e0d13d340 100644 --- a/packages/opentelemetry/src/internal/tracer.ts +++ b/packages/opentelemetry/src/internal/tracer.ts @@ -75,6 +75,7 @@ export class OtelSpan implements EffectTracer.Span { } end(endTime: bigint, exit: Exit) { + const hrTime = nanosToHrTime(endTime) this.status = { _tag: "Ended", endTime, @@ -83,9 +84,7 @@ export class OtelSpan implements EffectTracer.Span { } if (exit._tag === "Success") { - this.span.setStatus({ - code: OtelApi.SpanStatusCode.OK - }) + this.span.setStatus({ code: OtelApi.SpanStatusCode.OK }) } else { if (Cause.isInterruptedOnly(exit.cause)) { this.span.setStatus({ @@ -95,13 +94,22 @@ export class OtelSpan implements EffectTracer.Span { this.span.setAttribute("span.label", "⚠︎ Interrupted") this.span.setAttribute("status.interrupted", true) } else { - this.span.setStatus({ - code: OtelApi.SpanStatusCode.ERROR, - message: Cause.pretty(exit.cause) - }) + const errors = Cause.prettyErrors(exit.cause) + if (errors.length > 0) { + for (const error of errors) { + this.span.recordException(error, hrTime) + } + this.span.setStatus({ + code: OtelApi.SpanStatusCode.ERROR, + message: errors[0].message + }) + } else { + // empty cause means no error + this.span.setStatus({ code: OtelApi.SpanStatusCode.OK }) + } } } - this.span.end(nanosToHrTime(endTime)) + this.span.end(hrTime) } event(name: string, startTime: bigint, attributes?: Record) { diff --git a/packages/platform/src/internal/http/client.ts b/packages/platform/src/internal/http/client.ts index ca4b08f806..b92311c448 100644 --- a/packages/platform/src/internal/http/client.ts +++ b/packages/platform/src/internal/http/client.ts @@ -143,7 +143,7 @@ export const makeDefault = ( addAbort, Effect.useSpan( `http.client ${request.method}`, - { kind: "client" }, + { kind: "client", captureStackTrace: false }, (span) => { span.attribute("http.request.method", request.method) span.attribute("server.address", url.origin) diff --git a/packages/platform/src/internal/http/middleware.ts b/packages/platform/src/internal/http/middleware.ts index 36df4374c6..c4cff7920e 100644 --- a/packages/platform/src/internal/http/middleware.ts +++ b/packages/platform/src/internal/http/middleware.ts @@ -125,7 +125,11 @@ export const tracer = make((httpApp) => const redactedHeaders = Headers.redact(request.headers, redactedHeaderNames) return Effect.useSpan( `http.server ${request.method}`, - { parent: Option.getOrUndefined(TraceContext.fromHeaders(request.headers)), kind: "server" }, + { + parent: Option.getOrUndefined(TraceContext.fromHeaders(request.headers)), + kind: "server", + captureStackTrace: false + }, (span) => { span.attribute("http.request.method", request.method) if (url !== undefined) { diff --git a/packages/rpc/src/Router.ts b/packages/rpc/src/Router.ts index 1b671c3b48..0db28d8809 100644 --- a/packages/rpc/src/Router.ts +++ b/packages/rpc/src/Router.ts @@ -244,7 +244,8 @@ export const toHandler = >(router: R, options?: { spanId: req.spanId, sampled: req.sampled, context: Context.empty() - } + }, + captureStackTrace: false }) ) } @@ -276,7 +277,8 @@ export const toHandler = >(router: R, options?: { spanId: req.spanId, sampled: req.sampled, context: Context.empty() - } + }, + captureStackTrace: false }) ) }, { concurrency: "unbounded", discard: true }), @@ -332,7 +334,8 @@ export const toHandlerEffect = >(router: R, options?: spanId: req.spanId, sampled: req.sampled, context: Context.empty() - } + }, + captureStackTrace: false }) ) } @@ -352,7 +355,8 @@ export const toHandlerEffect = >(router: R, options?: spanId: req.spanId, sampled: req.sampled, context: Context.empty() - } + }, + captureStackTrace: false }) ) }, { concurrency: "unbounded" }) diff --git a/packages/rpc/src/Rpc.ts b/packages/rpc/src/Rpc.ts index 61d71e626c..943885afd6 100644 --- a/packages/rpc/src/Rpc.ts +++ b/packages/rpc/src/Rpc.ts @@ -329,7 +329,8 @@ export const request = ( ): Effect.Effect, never, Scope> => pipe( Effect.makeSpanScoped(`${options?.spanPrefix ?? "Rpc.request "}${request._tag}`, { - kind: "client" + kind: "client", + captureStackTrace: false }), Effect.zip(FiberRef.get(currentHeaders)), Effect.map(([span, headers]) => diff --git a/packages/sql/src/Resolver.ts b/packages/sql/src/Resolver.ts index c3faef4b9f..e96dedce91 100644 --- a/packages/sql/src/Resolver.ts +++ b/packages/sql/src/Resolver.ts @@ -118,7 +118,7 @@ const makeResolver = ( return function(input: I) { return Effect.useSpan( `sql.Resolver.execute ${tag}`, - { kind: "client" }, + { kind: "client", captureStackTrace: false }, (span) => Effect.withFiberRuntime((fiber) => { span.attribute("request.input", input) @@ -247,7 +247,8 @@ export const ordered = } @@ -326,7 +327,8 @@ export const grouped = } @@ -404,7 +406,8 @@ export const findById = } @@ -450,7 +453,8 @@ const void_ = ( Effect.withSpan(`sql.Resolver.batch ${tag}`, { kind: "client", links: spanLinks, - attributes: { "request.count": inputs.length } + attributes: { "request.count": inputs.length }, + captureStackTrace: false }) ) as Effect.Effect } diff --git a/packages/sql/src/internal/client.ts b/packages/sql/src/internal/client.ts index fdc38138af..5ca7c04bc3 100644 --- a/packages/sql/src/internal/client.ts +++ b/packages/sql/src/internal/client.ts @@ -56,54 +56,58 @@ export function make({ effect: Effect.Effect ): Effect.Effect => Effect.uninterruptibleMask((restore) => - Effect.useSpan("sql.transaction", (span) => - Effect.withFiberRuntime((fiber) => { - for (const [key, value] of spanAttributes) { - span.attribute(key, value) - } - const context = fiber.getFiberRef(FiberRef.currentContext) - const clock = Context.get(fiber.getFiberRef(DefaultServices.currentServices), Clock.Clock) - const connOption = Context.getOption(context, TransactionConnection) - const conn = connOption._tag === "Some" - ? Effect.succeed([undefined, connOption.value[0]] as const) - : getTxConn - const id = connOption._tag === "Some" ? connOption.value[1] + 1 : 0 - return Effect.flatMap( - conn, - ( - [scope, conn] - ) => - conn.executeRaw(id === 0 ? beginTransaction : savepoint(`effect_sql_${id}`)).pipe( - Effect.zipRight(Effect.locally( - restore(effect), - FiberRef.currentContext, - Context.add(context, TransactionConnection, [conn, id]).pipe( - Context.add(Tracer.ParentSpan, span) - ) - )), - Effect.exit, - Effect.flatMap((exit) => { - let effect: Effect.Effect - if (Exit.isSuccess(exit)) { - if (id === 0) { - span.event("db.transaction.commit", clock.unsafeCurrentTimeNanos()) - effect = Effect.orDie(conn.executeRaw(commit)) + Effect.useSpan( + "sql.transaction", + { kind: "client", captureStackTrace: false }, + (span) => + Effect.withFiberRuntime((fiber) => { + for (const [key, value] of spanAttributes) { + span.attribute(key, value) + } + const context = fiber.getFiberRef(FiberRef.currentContext) + const clock = Context.get(fiber.getFiberRef(DefaultServices.currentServices), Clock.Clock) + const connOption = Context.getOption(context, TransactionConnection) + const conn = connOption._tag === "Some" + ? Effect.succeed([undefined, connOption.value[0]] as const) + : getTxConn + const id = connOption._tag === "Some" ? connOption.value[1] + 1 : 0 + return Effect.flatMap( + conn, + ( + [scope, conn] + ) => + conn.executeRaw(id === 0 ? beginTransaction : savepoint(`effect_sql_${id}`)).pipe( + Effect.zipRight(Effect.locally( + restore(effect), + FiberRef.currentContext, + Context.add(context, TransactionConnection, [conn, id]).pipe( + Context.add(Tracer.ParentSpan, span) + ) + )), + Effect.exit, + Effect.flatMap((exit) => { + let effect: Effect.Effect + if (Exit.isSuccess(exit)) { + if (id === 0) { + span.event("db.transaction.commit", clock.unsafeCurrentTimeNanos()) + effect = Effect.orDie(conn.executeRaw(commit)) + } else { + span.event("db.transaction.savepoint", clock.unsafeCurrentTimeNanos()) + effect = Effect.void + } } else { - span.event("db.transaction.savepoint", clock.unsafeCurrentTimeNanos()) - effect = Effect.void + span.event("db.transaction.rollback", clock.unsafeCurrentTimeNanos()) + effect = Effect.orDie( + id > 0 ? conn.executeRaw(rollbackSavepoint(`effect_sql_${id}`)) : conn.executeRaw(rollback) + ) } - } else { - span.event("db.transaction.rollback", clock.unsafeCurrentTimeNanos()) - effect = Effect.orDie( - id > 0 ? conn.executeRaw(rollbackSavepoint(`effect_sql_${id}`)) : conn.executeRaw(rollback) - ) - } - const withScope = scope !== undefined ? Effect.ensuring(effect, Scope.close(scope, exit)) : effect - return Effect.zipRight(withScope, exit) - }) - ) - ) - })) + const withScope = scope !== undefined ? Effect.ensuring(effect, Scope.close(scope, exit)) : effect + return Effect.zipRight(withScope, exit) + }) + ) + ) + }) + ) ) const client: Client.Client = Object.assign( diff --git a/packages/sql/src/internal/statement.ts b/packages/sql/src/internal/statement.ts index 6666581f3e..c5e02751e7 100644 --- a/packages/sql/src/internal/statement.ts +++ b/packages/sql/src/internal/statement.ts @@ -90,7 +90,7 @@ export class StatementPrimitive extends Effectable.Class, Er ): Effect.Effect { return Effect.useSpan( "sql.execute", - { kind: "client" }, + { kind: "client", captureStackTrace: false }, (span) => Effect.withFiberRuntime((fiber) => { const transform = fiber.getFiberRef(currentTransformer) @@ -118,7 +118,7 @@ export class StatementPrimitive extends Effectable.Class, Er get stream(): Stream.Stream { return Stream.unwrapScoped(Effect.flatMap( - Effect.makeSpanScoped("sql.execute", { kind: "client" }), + Effect.makeSpanScoped("sql.execute", { kind: "client", captureStackTrace: false }), (span) => Effect.withFiberRuntime, Error.SqlError, Scope>((fiber) => { const transform = fiber.getFiberRef(currentTransformer)