@@ -63,7 +63,7 @@ export default class Logger {
6363 winston . format . printf ( ( info ) => {
6464 // Apply minimal redaction for files (debugging info preserved)
6565 const redactedInfo = this . redact ( info , false ) ;
66- return JSON . stringify ( redactedInfo ) ;
66+ return this . safeStringify ( redactedInfo ) ;
6767 } ) ,
6868 ) ,
6969 } ) ,
@@ -75,7 +75,7 @@ export default class Logger {
7575 const logConfig = configHandler . get ( 'log' ) || { } ;
7676 const currentModule = logConfig . progressSupportedModule ;
7777 const hasProgressSupport = currentModule && PROGRESS_SUPPORTED_MODULES . includes ( currentModule ) ;
78-
78+
7979 if ( hasProgressSupport ) {
8080 // Plugin has progress bars - respect user's explicit setting, or default to false (show progress bars)
8181 showConsoleLogs = logConfig . showConsoleLogs ?? false ;
@@ -146,6 +146,26 @@ export default class Logger {
146146 }
147147 }
148148
149+ /**
150+ * Stringifies a log entry without throwing on circular references.
151+ * `redact()` can fall back to the original (still-circular) object when
152+ * cloning fails, so this must never assume the input is cycle-free.
153+ */
154+ private safeStringify ( value : any ) : string {
155+ const seen = new WeakSet ( ) ;
156+ try {
157+ return JSON . stringify ( value , ( _key , val ) => {
158+ if ( typeof val === 'object' && val !== null ) {
159+ if ( seen . has ( val ) ) return '[Circular]' ;
160+ seen . add ( val ) ;
161+ }
162+ return val ;
163+ } ) ;
164+ } catch {
165+ return JSON . stringify ( { level : value ?. level , message : value ?. message ?? '[Unserializable log entry]' } ) ;
166+ }
167+ }
168+
149169 private shouldLog ( level : LogType , target : 'console' | 'file' ) : boolean {
150170 // If console logging is disabled, don't log to console
151171 if ( target === 'console' && this . config . consoleLoggingEnabled === false ) {
@@ -215,9 +235,12 @@ export default class Logger {
215235 this . loggers . error . error ( logPayload ) ;
216236 }
217237
218- // For console, use debug level if hidden, otherwise error level
238+ // For console, use debug level if hidden, otherwise error level. Each level's
239+ // winston logger instance bundles both the file and console transports, so only
240+ // write again here when the target differs from the one already written above —
241+ // otherwise this would duplicate both the console line and the file entry.
219242 const consoleLevel : LogType = params . hidden ? 'debug' : 'error' ;
220- if ( this . shouldLog ( consoleLevel , 'console' ) ) {
243+ if ( consoleLevel !== 'error' && this . shouldLog ( consoleLevel , 'console' ) ) {
221244 this . loggers [ consoleLevel ] . error ( logPayload ) ;
222245 }
223246 }
0 commit comments