From 19393581eff54fcb1651ff77c4a8d7d9d80b2781 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 22:46:38 +0000 Subject: [PATCH 1/2] Initial plan From da0b55e90d2c62af4c7d2aa6c5d1c17ed0014a57 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 22:51:20 +0000 Subject: [PATCH 2/2] fix: fall back to stderr (not stdout) when log-dir is unwritable When --log-dir points at a directory awmg cannot open log files in, the FileLogger previously fell back to os.Stdout. Because stdout is the JSON channel used by start_mcp_gateway.cjs to receive the gateway configuration, the structured log output was mixed into the JSON payload, causing: SyntaxError: Expected ',' or ']' after array element in JSON at position 5 Change handleFileLoggerError to use os.Stderr and update GetWriter() to return os.Stderr in fallback mode. Update related comments and tests. --- internal/logger/common.go | 10 ++++++---- internal/logger/file_logger.go | 14 ++++++++------ internal/logger/file_logger_test.go | 6 +++--- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/internal/logger/common.go b/internal/logger/common.go index 8cd118422..f6db8ac43 100644 --- a/internal/logger/common.go +++ b/internal/logger/common.go @@ -98,21 +98,23 @@ import ( // // Different logger types implement different fallback strategies based on their purpose: // -// 1. FileLogger - Stdout Fallback: +// 1. FileLogger - Stderr Fallback: // - Purpose: Operational logs must always be visible -// - Fallback: Redirects to stdout if log directory/file creation fails +// - Fallback: Redirects to stderr if log directory/file creation fails +// (stderr is used, not stdout, to avoid corrupting the stdout +// JSON channel that callers use to receive gateway config output) // - Error: Returns nil (never fails, always provides output) // - Use case: Critical operational messages that must be seen // // Example error handler: // func(err error, logDir, fileName string) (*FileLogger, error) { // log.Printf("WARNING: Failed to initialize log file: %v", err) -// log.Printf("WARNING: Falling back to stdout for logging") +// log.Printf("WARNING: Falling back to stderr for logging") // return &FileLogger{ // logDir: logDir, // fileName: fileName, // useFallback: true, -// logger: log.New(os.Stdout, "", 0), +// logger: log.New(os.Stderr, "", 0), // }, nil // } // diff --git a/internal/logger/file_logger.go b/internal/logger/file_logger.go index f270263e9..2ddc6d01e 100644 --- a/internal/logger/file_logger.go +++ b/internal/logger/file_logger.go @@ -8,7 +8,7 @@ import ( "sync" ) -// FileLogger manages logging to a file with fallback to stdout +// FileLogger manages logging to a file with fallback to stderr type FileLogger struct { lockable logFile *os.File @@ -35,14 +35,16 @@ func setupFileLogger(file *os.File, logDir, fileName string) (*FileLogger, error return fl, nil } -// handleFileLoggerError falls back to stdout when the log file cannot be opened. +// handleFileLoggerError falls back to stderr when the log file cannot be opened. +// Stderr is used (not stdout) to avoid corrupting the stdout JSON channel that +// callers use to receive the gateway configuration output. func handleFileLoggerError(err error, logDir, fileName string) (*FileLogger, error) { - logFallbackWarnings(err, "Failed to initialize log file", "Falling back to stdout for logging") + logFallbackWarnings(err, "Failed to initialize log file", "Falling back to stderr for logging") fl := &FileLogger{ logDir: logDir, fileName: fileName, useFallback: true, - logger: log.New(os.Stdout, "", 0), + logger: log.New(os.Stderr, "", 0), } return fl, nil } @@ -54,7 +56,7 @@ var fileLoggerFactory = loggerFactory[*FileLogger]{ } // InitFileLogger initializes the global file logger -// If the log directory doesn't exist and can't be created, falls back to stdout +// If the log directory doesn't exist and can't be created, falls back to stderr func InitFileLogger(logDir, fileName string) error { logger, err := initLogger(logDir, fileName, os.O_APPEND, fileLoggerFactory) initGlobalLogger(&globalLoggerMu, &globalFileLogger, logger) @@ -103,7 +105,7 @@ func (fl *FileLogger) GetWriter() io.Writer { if fl.logFile != nil { return fl.logFile } - return os.Stdout + return os.Stderr } // Global logging functions that use the global file logger diff --git a/internal/logger/file_logger_test.go b/internal/logger/file_logger_test.go index d3b71af52..07349a3a9 100644 --- a/internal/logger/file_logger_test.go +++ b/internal/logger/file_logger_test.go @@ -200,7 +200,7 @@ func TestFileLoggerFlushes(t *testing.T) { } // TestFileLogger_GetWriter verifies GetWriter returns the underlying file for a real -// logger and os.Stdout for the fallback logger. +// logger and os.Stderr for the fallback logger. func TestFileLogger_GetWriter(t *testing.T) { t.Run("real logger returns file writer", func(t *testing.T) { tmpDir := t.TempDir() @@ -221,7 +221,7 @@ func TestFileLogger_GetWriter(t *testing.T) { assert.True(t, isFile, "GetWriter should return *os.File for real logger") }) - t.Run("fallback logger returns stdout", func(t *testing.T) { + t.Run("fallback logger returns stderr", func(t *testing.T) { err := InitFileLogger("/root/nonexistent/directory", "test.log") require.NoError(t, err) defer CloseGlobalLogger() @@ -233,7 +233,7 @@ func TestFileLogger_GetWriter(t *testing.T) { require.NotNil(t, logger) if logger.useFallback { w := logger.GetWriter() - assert.Equal(t, os.Stdout, w, "Fallback logger GetWriter should return os.Stdout") + assert.Equal(t, os.Stderr, w, "Fallback logger GetWriter should return os.Stderr") } else { t.Skip("System has permissions to write to /root; cannot test fallback path") }