From ca24efcb7f2c8b32187cc579c35b68635c547d7e Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 8 Jul 2024 10:44:08 +1200 Subject: [PATCH] include Error.cause stack in log output --- .changeset/chilly-berries-talk.md | 5 +++ .changeset/clever-parents-leave.md | 5 +++ .changeset/fast-dodos-impress.md | 5 +++ packages/effect/src/Cause.ts | 4 ++- packages/effect/src/internal/cause.ts | 31 ++++++++++++++++--- packages/effect/src/internal/logger.ts | 19 +++++------- .../test/Effect/cause-rendering.test.ts | 10 ++++++ 7 files changed, 62 insertions(+), 17 deletions(-) create mode 100644 .changeset/chilly-berries-talk.md create mode 100644 .changeset/clever-parents-leave.md create mode 100644 .changeset/fast-dodos-impress.md diff --git a/.changeset/chilly-berries-talk.md b/.changeset/chilly-berries-talk.md new file mode 100644 index 0000000000..3cd38cd2ce --- /dev/null +++ b/.changeset/chilly-berries-talk.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +include Error.cause stack in log output diff --git a/.changeset/clever-parents-leave.md b/.changeset/clever-parents-leave.md new file mode 100644 index 0000000000..5dd8872705 --- /dev/null +++ b/.changeset/clever-parents-leave.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +add renderErrorCause option to Cause.pretty diff --git a/.changeset/fast-dodos-impress.md b/.changeset/fast-dodos-impress.md new file mode 100644 index 0000000000..1085f69794 --- /dev/null +++ b/.changeset/fast-dodos-impress.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +set stackTraceLimit to 1 in PrettyError to address performance issues diff --git a/packages/effect/src/Cause.ts b/packages/effect/src/Cause.ts index 3368592163..9f3bb1a517 100644 --- a/packages/effect/src/Cause.ts +++ b/packages/effect/src/Cause.ts @@ -913,7 +913,9 @@ export const isUnknownException: (u: unknown) => u is UnknownException = core.is * @since 2.0.0 * @category rendering */ -export const pretty: (cause: Cause) => string = internal.pretty +export const pretty: (cause: Cause, options?: { + readonly renderErrorCause?: boolean | undefined +}) => string = internal.pretty /** * @since 3.2.0 diff --git a/packages/effect/src/internal/cause.ts b/packages/effect/src/internal/cause.ts index 27b328667f..680a19e8c4 100644 --- a/packages/effect/src/internal/cause.ts +++ b/packages/effect/src/internal/cause.ts @@ -970,25 +970,46 @@ export const reduceWithContext = dual< // ----------------------------------------------------------------------------- /** @internal */ -export const pretty = (cause: Cause.Cause): string => { +export const pretty = (cause: Cause.Cause, options?: { + readonly renderErrorCause?: boolean | undefined +}): string => { if (isInterruptedOnly(cause)) { return "All fibers interrupted without errors." } - return prettyErrors(cause).map((e) => e.stack).join("\n") + return prettyErrors(cause).map(function(e) { + if (options?.renderErrorCause !== true || e.cause === undefined) { + return e.stack + } + return `${e.stack} {\n${renderErrorCause(e.cause as PrettyError, " ")}\n}` + }).join("\n") +} + +const renderErrorCause = (cause: PrettyError, prefix: string) => { + const lines = cause.stack!.split("\n") + let stack = `${prefix}[cause]: ${lines[0]}` + for (let i = 1, len = lines.length; i < len; i++) { + stack += `\n${prefix}${lines[i]}` + } + return stack } class PrettyError extends globalThis.Error implements Cause.PrettyError { span: undefined | Span = undefined constructor(originalError: unknown) { + const originalErrorIsObject = typeof originalError === "object" && originalError !== null const prevLimit = Error.stackTraceLimit - Error.stackTraceLimit = 0 - super(prettyErrorMessage(originalError)) + Error.stackTraceLimit = 1 + super(prettyErrorMessage(originalError), { + cause: originalErrorIsObject && "cause" in originalError && typeof originalError.cause !== "undefined" + ? new PrettyError(originalError.cause) + : undefined + }) if (this.message === "") { this.message = "An error has occurred" } Error.stackTraceLimit = prevLimit this.name = originalError instanceof Error ? originalError.name : "Error" - if (typeof originalError === "object" && originalError !== null) { + if (originalErrorIsObject) { if (spanSymbol in originalError) { this.span = originalError[spanSymbol] as Span } diff --git a/packages/effect/src/internal/logger.ts b/packages/effect/src/internal/logger.ts index 13b506ff85..a512c60365 100644 --- a/packages/effect/src/internal/logger.ts +++ b/packages/effect/src/internal/logger.ts @@ -185,7 +185,7 @@ export const stringLogger: Logger.Logger = makeLogger( if (cause != null && cause._tag !== "Empty") { output = output + " cause=" - output = appendQuoted(Cause.pretty(cause), output) + output = appendQuoted(Cause.pretty(cause, { renderErrorCause: true }), output) } if (List.isCons(spans)) { @@ -255,7 +255,7 @@ export const logfmtLogger = makeLogger( if (cause != null && cause._tag !== "Empty") { output = output + " cause=" - output = appendQuotedLogfmt(Cause.pretty(cause), output) + output = appendQuotedLogfmt(Cause.pretty(cause, { renderErrorCause: true }), output) } if (List.isCons(spans)) { @@ -324,7 +324,7 @@ export const structuredLogger = makeLogger = [] if (!Cause.isEmpty(cause)) { - const errors = Cause.prettyErrors(cause) - for (let i = 0; i < errors.length; i++) { - if (isBrowser) { - console.error(errors[i].stack) - } else { - currentMessage += "\n%s" - params.push(errors[i].stack) - } + if (isBrowser) { + console.error(Cause.pretty(cause, { renderErrorCause: true })) + } else { + currentMessage += "\n%s" + params.push(Cause.pretty(cause, { renderErrorCause: true })) } } diff --git a/packages/effect/test/Effect/cause-rendering.test.ts b/packages/effect/test/Effect/cause-rendering.test.ts index 058fa163c3..21567cfa57 100644 --- a/packages/effect/test/Effect/cause-rendering.test.ts +++ b/packages/effect/test/Effect/cause-rendering.test.ts @@ -154,4 +154,14 @@ error message at`)) })) + + it.effect("pretty includes error.cause with renderErrorCause: true", () => + Effect.gen(function*() { + const cause = yield* Effect.fail(new Error("parent", { cause: new Error("child") })).pipe( + Effect.sandbox, + Effect.flip + ) + const pretty = Cause.pretty(cause, { renderErrorCause: true }) + assert.include(pretty, "[cause]: Error: child") + })) })