|
5 | 5 |
|
6 | 6 | import { createLogger } from '@sim/logger' |
7 | 7 | import { getErrorMessage } from '@sim/utils/errors' |
| 8 | +import { filterUndefined } from '@sim/utils/object' |
8 | 9 | import { mergeSubblockStateWithValues } from '@sim/workflow-persistence/subblocks' |
9 | 10 | import type { Edge } from 'reactflow' |
10 | 11 | import { z } from 'zod' |
@@ -39,6 +40,49 @@ const logger = createLogger('ExecutionCore') |
39 | 40 |
|
40 | 41 | const EnvVarsSchema = z.record(z.string(), z.string()) |
41 | 42 |
|
| 43 | +/** |
| 44 | + * Surfaces the underlying driver error from a wrapped error chain. |
| 45 | + * |
| 46 | + * Drizzle wraps the original `postgres`/Node driver error as `error.cause`, |
| 47 | + * which the logger's Error serializer drops (it only emits own-enumerable |
| 48 | + * keys). Walking the chain from `error` itself and preferring the first error |
| 49 | + * carrying a `code` exposes the diagnostic fields — notably the Postgres |
| 50 | + * `code` — that distinguish a connection drop (`08006`), a rejected connection |
| 51 | + * (`53300`), and a statement timeout (`57014`) behind an opaque "Failed query" |
| 52 | + * message. Starting at `error` also captures a bare driver error that reaches |
| 53 | + * this path unwrapped; when no error in the chain carries a `code`, it falls |
| 54 | + * back to the first wrapped cause (the top-level error is already logged on its |
| 55 | + * own, so it is not echoed here). |
| 56 | + */ |
| 57 | +function describeErrorCause(error: unknown): Record<string, unknown> | undefined { |
| 58 | + try { |
| 59 | + let driver: (Error & Record<string, unknown>) | undefined |
| 60 | + let current: unknown = error |
| 61 | + for (let depth = 0; depth < 10 && current instanceof Error; depth++) { |
| 62 | + const candidate = current as Error & Record<string, unknown> |
| 63 | + if (candidate.code !== undefined) { |
| 64 | + driver = candidate |
| 65 | + break |
| 66 | + } |
| 67 | + if (depth === 1) driver = candidate |
| 68 | + current = candidate.cause |
| 69 | + } |
| 70 | + if (!driver) return undefined |
| 71 | + return filterUndefined({ |
| 72 | + name: driver.name, |
| 73 | + message: driver.message, |
| 74 | + code: driver.code, |
| 75 | + severity: driver.severity, |
| 76 | + detail: driver.detail, |
| 77 | + routine: driver.routine, |
| 78 | + errno: driver.errno, |
| 79 | + syscall: driver.syscall, |
| 80 | + }) |
| 81 | + } catch { |
| 82 | + return undefined |
| 83 | + } |
| 84 | +} |
| 85 | + |
42 | 86 | export interface ExecuteWorkflowCoreOptions { |
43 | 87 | snapshot: ExecutionSnapshot |
44 | 88 | callbacks: ExecutionCallbacks |
@@ -682,7 +726,12 @@ export async function executeWorkflowCore( |
682 | 726 |
|
683 | 727 | return result |
684 | 728 | } catch (error: unknown) { |
685 | | - logger.error(`[${requestId}] Execution failed:`, error) |
| 729 | + const errorCause = describeErrorCause(error) |
| 730 | + logger.error( |
| 731 | + `[${requestId}] Execution failed:`, |
| 732 | + error, |
| 733 | + ...(errorCause ? [{ cause: errorCause }] : []) |
| 734 | + ) |
686 | 735 |
|
687 | 736 | await waitForLifecycleCallbacks() |
688 | 737 |
|
|
0 commit comments