@@ -132,61 +132,14 @@ const getLogConfig = () => {
132132 }
133133}
134134
135- /**
136- * Renders an error as the plain object `JSON.stringify` cannot produce for it.
137- *
138- * `message`, `stack` and `name` are non-enumerable on `Error.prototype`, so a
139- * plain stringify emits `{}`. Own enumerable properties are copied too — driver
140- * and HTTP errors carry the useful part (`code`, `status`) there.
141- */
142- const errorToPlainObject = ( error : Error , isDev : boolean ) : Record < string , unknown > => {
143- const errorObj : Record < string , unknown > = {
144- message : error . message ,
145- stack : isDev ? error . stack : undefined ,
146- name : error . name ,
147- }
148- for ( const key of Object . keys ( error ) ) {
149- if ( ! ( key in errorObj ) ) {
150- errorObj [ key ] = ( error as unknown as Record < string , unknown > ) [ key ]
151- }
152- }
153- return errorObj
154- }
155-
156- /**
157- * Format objects for logging
158- *
159- * Errors held under a key are unwrapped as well as bare ones — `{ error }` is
160- * the common call shape, and it would otherwise print as `{"error":{}}`.
161- */
162- const formatObject = ( obj : unknown , isDev : boolean ) : string => {
163- try {
164- if ( obj instanceof Error ) {
165- return JSON . stringify ( errorToPlainObject ( obj , isDev ) , null , isDev ? 2 : 0 )
166- }
167- if ( isRecordLike ( obj ) ) {
168- let unwrapped : Record < string , unknown > | undefined
169- for ( const [ key , value ] of Object . entries ( obj as Record < string , unknown > ) ) {
170- if ( ! ( value instanceof Error ) ) continue
171- unwrapped ??= { ...( obj as Record < string , unknown > ) }
172- unwrapped [ key ] = errorToPlainObject ( value , isDev )
173- }
174- if ( unwrapped ) {
175- return JSON . stringify ( unwrapped , null , isDev ? 2 : 0 )
176- }
177- }
178- return JSON . stringify ( obj , null , isDev ? 2 : 0 )
179- } catch {
180- return '[Circular or Non-Serializable Object]'
181- }
182- }
183-
184135interface LoggedError {
185136 message : string
186137 stack ?: string
187138 /** `"Name: message"` of the deepest `.cause` link, present only when the error wraps another. */
188139 cause ?: string
189140 code ?: string
141+ /** Whether the message carried a bound-parameter tail, marking the error as a query wrapper. */
142+ redacted : boolean
190143}
191144
192145/**
@@ -212,9 +165,61 @@ const toLoggedError = (error: Error): LoggedError => {
212165 stack,
213166 ...( described . causeChain ? { cause : `${ described . name } : ${ described . message } ` } : { } ) ,
214167 ...( described . code ? { code : described . code } : { } ) ,
168+ redacted : message !== error . message ,
215169 }
216170}
217171
172+ /**
173+ * Renders an error as the plain object `JSON.stringify` cannot produce for it.
174+ *
175+ * `message`, `stack` and `name` are non-enumerable on `Error.prototype`, so a
176+ * plain stringify emits `{}` — or, for an error with own enumerable properties,
177+ * only those. Own properties are copied because driver and HTTP errors carry the
178+ * useful part (`code`, `status`) there, except what `toLoggedError` replaces: a
179+ * query wrapper's `params` holds the redacted values, and a summarized `cause`
180+ * would carry the driver's `detail`.
181+ */
182+ const toPlainError = ( error : Error , includeStack : boolean ) : Record < string , unknown > => {
183+ const logged = toLoggedError ( error )
184+ const plain : Record < string , unknown > = {
185+ message : logged . message ,
186+ stack : includeStack ? logged . stack : undefined ,
187+ name : error . name ,
188+ ...( logged . cause ? { cause : logged . cause } : { } ) ,
189+ ...( logged . code ? { code : logged . code } : { } ) ,
190+ }
191+ for ( const key of Object . keys ( error ) ) {
192+ if ( key in plain || ( logged . redacted && key === 'params' ) ) continue
193+ plain [ key ] = ( error as unknown as Record < string , unknown > ) [ key ]
194+ }
195+ return plain
196+ }
197+
198+ /** JSON replacer that serializes every `Error`, however deeply nested, as its plain form. */
199+ const errorReplacer =
200+ ( includeStack : boolean ) =>
201+ ( _key : string , value : unknown ) : unknown =>
202+ value instanceof Error ? toPlainError ( value , includeStack ) : value
203+
204+ /** Format objects for logging. */
205+ const formatObject = ( obj : unknown , isDev : boolean ) : string => {
206+ try {
207+ return JSON . stringify ( obj , errorReplacer ( isDev ) , isDev ? 2 : 0 )
208+ } catch {
209+ return '[Circular or Non-Serializable Object]'
210+ }
211+ }
212+
213+ /** The error a line is about: the first bare `Error` argument, else the first `{ error }` field. */
214+ const primaryError = ( args : unknown [ ] ) : Error | undefined => {
215+ const bare = args . find ( ( arg ) => arg instanceof Error )
216+ if ( bare ) return bare as Error
217+ for ( const arg of args ) {
218+ if ( isRecordLike ( arg ) && arg . error instanceof Error ) return arg . error
219+ }
220+ return undefined
221+ }
222+
218223/** Adds an error's cause fields to the entry without displacing caller-supplied values. */
219224const assignErrorCause = ( entry : Record < string , unknown > , logged : LoggedError ) => {
220225 if ( logged . cause !== undefined && entry . errorCause === undefined ) entry . errorCause = logged . cause
@@ -262,9 +267,14 @@ const mergeArgs = (entry: Record<string, unknown>, args: unknown[]): Record<stri
262267 return entry
263268}
264269
265- /** JSON replacer that tolerates cyclic references and BigInt values. */
270+ /**
271+ * JSON replacer that tolerates cyclic references and BigInt values, and
272+ * serializes errors as their plain form like `errorReplacer`.
273+ */
266274const tolerantReplacer = ( ) => {
267275 const ancestors : object [ ] = [ ]
276+ /** What `JSON.stringify` descends into for each ancestor — an error's plain form, not the error. */
277+ const holders : object [ ] = [ ]
268278 return function ( this : unknown , _key : string , value : unknown ) : unknown {
269279 if ( typeof value === 'bigint' ) return value . toString ( )
270280 if ( value === null || typeof value !== 'object' ) return value
@@ -275,10 +285,15 @@ const tolerantReplacer = () => {
275285 * appearance of a merely repeated reference `[Circular]` and discard real
276286 * data, since a payload that references one object twice has no cycle.
277287 */
278- while ( ancestors . length > 0 && ancestors [ ancestors . length - 1 ] !== this ) ancestors . pop ( )
288+ while ( holders . length > 0 && holders [ holders . length - 1 ] !== this ) {
289+ holders . pop ( )
290+ ancestors . pop ( )
291+ }
279292 if ( ancestors . includes ( value ) ) return '[Circular]'
293+ const serialized = value instanceof Error ? toPlainError ( value , false ) : value
280294 ancestors . push ( value )
281- return value
295+ holders . push ( serialized )
296+ return serialized
282297 }
283298}
284299
@@ -292,7 +307,7 @@ const tolerantReplacer = () => {
292307 */
293308const serializeEntry = ( base : Record < string , unknown > , args : unknown [ ] ) : string => {
294309 try {
295- return JSON . stringify ( mergeArgs ( { ...base } , args ) )
310+ return JSON . stringify ( mergeArgs ( { ...base } , args ) , errorReplacer ( false ) )
296311 } catch { }
297312
298313 try {
@@ -628,17 +643,20 @@ function emitOtelLogRecord(
628643 for ( const [ key , value ] of Object . entries ( filterUndefined ( metadata ) ) ) {
629644 attributes [ key ] = String ( value )
630645 }
631- const firstError = args . find ( ( arg ) => arg instanceof Error ) as Error | undefined
632- if ( firstError ) {
633- const logged = toLoggedError ( firstError )
646+ const error = primaryError ( args )
647+ if ( error ) {
648+ const logged = toLoggedError ( error )
634649 attributes [ 'error.message' ] = logged . message
635650 if ( logged . stack ) attributes [ 'error.stack' ] = logged . stack
636651 if ( logged . cause ) attributes [ 'error.cause' ] = logged . cause
637652 }
638653 const plainArgs = args . filter ( ( arg ) => ! ( arg instanceof Error ) )
639654 if ( plainArgs . length > 0 ) {
640655 try {
641- attributes [ 'log.args' ] = JSON . stringify ( plainArgs ) . slice ( 0 , OTEL_LOG_ARG_MAX_CHARS )
656+ attributes [ 'log.args' ] = JSON . stringify ( plainArgs , errorReplacer ( false ) ) . slice (
657+ 0 ,
658+ OTEL_LOG_ARG_MAX_CHARS
659+ )
642660 } catch {
643661 attributes [ 'log.args' ] = String ( plainArgs ) . slice ( 0 , OTEL_LOG_ARG_MAX_CHARS )
644662 }
0 commit comments