55 * Provides standardized console logging with environment-aware configuration.
66 */
77import { logs , SeverityNumber } from '@opentelemetry/api-logs'
8+ import { describeError , redactBoundParameters } from '@sim/utils/errors'
89import { filterUndefined , isRecordLike } from '@sim/utils/object'
910import chalk from 'chalk'
1011import { getRequestContext , type RequestContext } from './request-context'
@@ -180,6 +181,46 @@ const formatObject = (obj: unknown, isDev: boolean): string => {
180181 }
181182}
182183
184+ interface LoggedError {
185+ message : string
186+ stack ?: string
187+ /** `"Name: message"` of the deepest `.cause` link, present only when the error wraps another. */
188+ cause ?: string
189+ code ?: string
190+ }
191+
192+ /**
193+ * The fields a log line keeps for an error.
194+ *
195+ * Drizzle's `DrizzleQueryError` appends `\nparams: <values>` — user data — to
196+ * its message, and the stack repeats the message, so both are redacted. The
197+ * wrapper's message is only the failing SQL; the reason (a statement timeout, a
198+ * constraint violation) lives on the driver error in `.cause`, which no field
199+ * would otherwise carry.
200+ */
201+ const toLoggedError = ( error : Error ) : LoggedError => {
202+ const message = redactBoundParameters ( error . message )
203+ const stack =
204+ error . stack === undefined || message === error . message
205+ ? error . stack
206+ : error . stack . includes ( error . message )
207+ ? error . stack . replace ( error . message , ( ) => message )
208+ : redactBoundParameters ( error . stack )
209+ const described = describeError ( error )
210+ return {
211+ message,
212+ stack,
213+ ...( described . causeChain ? { cause : `${ described . name } : ${ described . message } ` } : { } ) ,
214+ ...( described . code ? { code : described . code } : { } ) ,
215+ }
216+ }
217+
218+ /** Adds an error's cause fields to the entry without displacing caller-supplied values. */
219+ const assignErrorCause = ( entry : Record < string , unknown > , logged : LoggedError ) => {
220+ if ( logged . cause !== undefined && entry . errorCause === undefined ) entry . errorCause = logged . cause
221+ if ( logged . code !== undefined && entry . errorCode === undefined ) entry . errorCode = logged . code
222+ }
223+
183224/**
184225 * Merges caller-supplied log arguments into the structured entry.
185226 *
@@ -188,23 +229,27 @@ const formatObject = (obj: unknown, isDev: boolean): string => {
188229 * is by far the most common call shape, which would otherwise reduce the one
189230 * field worth reading to an empty object. Errors nested in an object argument
190231 * are therefore unwrapped like a bare `Error` argument. `error` stays a plain
191- * message string so log queries can group on it; richer diagnostics are opt-in
192- * via `describeError` from `@sim/utils/errors `.
232+ * message string so log queries can group on it; the primary error's deepest
233+ * cause and its code go in `errorCause` and `errorCode `.
193234 */
194235const mergeArgs = ( entry : Record < string , unknown > , args : unknown [ ] ) : Record < string , unknown > => {
195236 for ( const arg of args ) {
196237 if ( arg === null || arg === undefined ) continue
197238 if ( arg instanceof Error ) {
198- entry . error = arg . message
199- entry . stack = arg . stack
239+ const logged = toLoggedError ( arg )
240+ entry . error = logged . message
241+ entry . stack = logged . stack
242+ assignErrorCause ( entry , logged )
200243 } else if ( typeof arg === 'object' ) {
201244 const source = arg as Record < string , unknown >
202245 for ( const key of Object . keys ( source ) ) {
203246 const value = source [ key ]
204247 if ( value instanceof Error ) {
205- entry [ key ] = value . message
248+ const logged = toLoggedError ( value )
249+ entry [ key ] = logged . message
206250 if ( key === 'error' && entry . stack === undefined ) {
207- entry . stack = value . stack
251+ entry . stack = logged . stack
252+ assignErrorCause ( entry , logged )
208253 }
209254 } else {
210255 entry [ key ] = value
@@ -585,8 +630,10 @@ function emitOtelLogRecord(
585630 }
586631 const firstError = args . find ( ( arg ) => arg instanceof Error ) as Error | undefined
587632 if ( firstError ) {
588- attributes [ 'error.message' ] = firstError . message
589- if ( firstError . stack ) attributes [ 'error.stack' ] = firstError . stack
633+ const logged = toLoggedError ( firstError )
634+ attributes [ 'error.message' ] = logged . message
635+ if ( logged . stack ) attributes [ 'error.stack' ] = logged . stack
636+ if ( logged . cause ) attributes [ 'error.cause' ] = logged . cause
590637 }
591638 const plainArgs = args . filter ( ( arg ) => ! ( arg instanceof Error ) )
592639 if ( plainArgs . length > 0 ) {
0 commit comments