From 534daa0a18c817dd93947bbdb7d51142ff730eeb Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 10 Jul 2024 14:05:03 +1200 Subject: [PATCH] include Error.cause stack in log output (#3194) --- .changeset/chilly-berries-talk.md | 5 +++ .changeset/clever-parents-leave.md | 5 +++ .changeset/fast-dodos-impress.md | 5 +++ .changeset/ninety-starfishes-flow.md | 5 +++ packages/effect/src/Cause.ts | 4 ++- packages/effect/src/Console.ts | 1 + packages/effect/src/internal/cause.ts | 31 ++++++++++++++--- packages/effect/src/internal/logger.ts | 33 ++++++------------- .../test/Effect/cause-rendering.test.ts | 10 ++++++ 9 files changed, 70 insertions(+), 29 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 create mode 100644 .changeset/ninety-starfishes-flow.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/.changeset/ninety-starfishes-flow.md b/.changeset/ninety-starfishes-flow.md new file mode 100644 index 0000000000..7443ff37db --- /dev/null +++ b/.changeset/ninety-starfishes-flow.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +add .groupCollapsed to UnsafeConsole diff --git a/packages/effect/src/Cause.ts b/packages/effect/src/Cause.ts index eda06b12ab..7c7ce6c9a2 100644 --- a/packages/effect/src/Cause.ts +++ b/packages/effect/src/Cause.ts @@ -957,7 +957,9 @@ export const isExceededCapacityException: (u: unknown) => u is ExceededCapacityE * @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/Console.ts b/packages/effect/src/Console.ts index b6df353fec..40b6d516a6 100644 --- a/packages/effect/src/Console.ts +++ b/packages/effect/src/Console.ts @@ -64,6 +64,7 @@ export interface UnsafeConsole { dirxml(...args: ReadonlyArray): void error(...args: ReadonlyArray): void group(label?: string | undefined): void + groupCollapsed(label?: string | undefined): void groupEnd(): void info(...args: ReadonlyArray): void log(...args: ReadonlyArray): void 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..3404359712 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 { + log(Cause.pretty(cause, { renderErrorCause: true })) } } if (messageIndex < message.length) { for (; messageIndex < message.length; messageIndex++) { - currentMessage += "\n%O" - params.push(message[messageIndex]) + log(message[messageIndex]) } } if (HashMap.size(annotations) > 0) { for (const [key, value] of annotations) { - currentMessage += "\n" + color(`${key}:`, colors.bold, colors.white) + " %O" - params.push(value) + log(color(`${key}:`, colors.bold, colors.white), value) } } - - if (currentMessage.length > 0) { - log(currentMessage.slice(1), ...params) - } console.groupEnd() } ) 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") + })) })