Skip to content

Commit ac33d64

Browse files
authored
Merge pull request #146 from githubnext/copilot/update-gateway-log-accessibility
2 parents 33aa0b4 + 2e81caa commit ac33d64

3 files changed

Lines changed: 196 additions & 69 deletions

File tree

internal/logger/file_logger.go

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"os"
88
"path/filepath"
99
"sync"
10-
"syscall"
1110
"time"
1211
)
1312

@@ -19,7 +18,6 @@ type FileLogger struct {
1918
logDir string
2019
fileName string
2120
useFallback bool
22-
locked bool // tracks if file is locked
2321
}
2422

2523
var (
@@ -70,36 +68,21 @@ func InitFileLogger(logDir, fileName string) error {
7068
fl.logFile = file
7169
fl.logger = log.New(file, "", 0)
7270

73-
// Apply a shared lock (LOCK_SH) to allow other processes to read the file
74-
// This is non-blocking (LOCK_NB) so we don't hang if another process has an exclusive lock
75-
if err := syscall.Flock(int(file.Fd()), syscall.LOCK_SH|syscall.LOCK_NB); err != nil {
76-
// If we can't get a shared lock, log a warning but continue
77-
// The file is still usable, just not with advisory locking guarantees
78-
log.Printf("WARNING: Failed to acquire shared lock on log file %s: %v", logPath, err)
79-
log.Printf("WARNING: Continuing without file lock - other processes may have limited access")
80-
} else {
81-
fl.locked = true
82-
}
83-
8471
log.Printf("Logging to file: %s", logPath)
8572

8673
globalFileLogger = fl
8774
return nil
8875
}
8976

90-
// Close closes the log file and releases any locks
77+
// Close closes the log file
9178
func (fl *FileLogger) Close() error {
9279
fl.mu.Lock()
9380
defer fl.mu.Unlock()
9481

9582
if fl.logFile != nil {
96-
// Release the file lock before closing
97-
if fl.locked {
98-
if err := syscall.Flock(int(fl.logFile.Fd()), syscall.LOCK_UN); err != nil {
99-
log.Printf("WARNING: Failed to release lock on log file: %v", err)
100-
// Continue to close the file even if unlock fails
101-
}
102-
fl.locked = false
83+
// Sync any remaining buffered data before closing
84+
if err := fl.logFile.Sync(); err != nil {
85+
log.Printf("WARNING: Failed to sync log file before close: %v", err)
10386
}
10487
return fl.logFile.Close()
10588
}
@@ -126,6 +109,14 @@ func (fl *FileLogger) Log(level LogLevel, category, format string, args ...inter
126109

127110
logLine := fmt.Sprintf("[%s] [%s] [%s] %s", timestamp, level, category, message)
128111
fl.logger.Println(logLine)
112+
113+
// Flush the log to disk immediately to ensure it's readable by other processes
114+
if fl.logFile != nil {
115+
if err := fl.logFile.Sync(); err != nil {
116+
// Log sync errors to stderr to avoid infinite recursion
117+
log.Printf("WARNING: Failed to sync log file: %v", err)
118+
}
119+
}
129120
}
130121

131122
// GetWriter returns the underlying io.Writer for the file logger

internal/logger/file_logger_test.go

Lines changed: 24 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"os"
55
"path/filepath"
66
"strings"
7-
"syscall"
87
"testing"
98
)
109

@@ -210,100 +209,77 @@ func TestFileLoggerConcurrency(t *testing.T) {
210209
}
211210
}
212211

213-
func TestFileLoggerLocking(t *testing.T) {
212+
func TestFileLoggerReadableByOtherProcesses(t *testing.T) {
214213
tmpDir := t.TempDir()
215214
logDir := filepath.Join(tmpDir, "logs")
216-
fileName := "lock-test.log"
215+
fileName := "readable-test.log"
217216
logPath := filepath.Join(logDir, fileName)
218217

219-
// Initialize the logger - this will acquire a shared lock
218+
// Initialize the logger
220219
err := InitFileLogger(logDir, fileName)
221220
if err != nil {
222221
t.Fatalf("InitFileLogger failed: %v", err)
223222
}
224223

225224
// Write some data
226-
LogInfo("test", "Testing file locking")
225+
LogInfo("test", "Testing file readability")
227226

228-
// Verify another process can open and read the file with a shared lock
227+
// Verify another process can open and read the file without any locks
229228
// This simulates another process trying to read the log file
230229
readFile, err := os.Open(logPath)
231230
if err != nil {
232231
t.Fatalf("Failed to open log file for reading: %v", err)
233232
}
234233
defer readFile.Close()
235234

236-
// Try to acquire a shared lock (should succeed with shared lock)
237-
err = syscall.Flock(int(readFile.Fd()), syscall.LOCK_SH|syscall.LOCK_NB)
238-
if err != nil {
239-
t.Errorf("Failed to acquire shared lock for reading: %v", err)
240-
} else {
241-
// Release the shared lock
242-
syscall.Flock(int(readFile.Fd()), syscall.LOCK_UN)
243-
}
244-
245235
// Read the content to verify it's readable
246236
content, err := os.ReadFile(logPath)
247237
if err != nil {
248238
t.Fatalf("Failed to read log file content: %v", err)
249239
}
250240

251-
if !strings.Contains(string(content), "Testing file locking") {
241+
if !strings.Contains(string(content), "Testing file readability") {
252242
t.Errorf("Log file does not contain expected content")
253243
}
254244

255-
// Close the logger - this should release the lock
245+
// Close the logger
256246
CloseGlobalLogger()
257247

258-
// Try to acquire an exclusive lock (should succeed now that we've released)
259-
exclusiveFile, err := os.OpenFile(logPath, os.O_RDWR, 0644)
248+
// Verify file is still readable after close
249+
content, err = os.ReadFile(logPath)
260250
if err != nil {
261-
t.Fatalf("Failed to open log file for exclusive lock: %v", err)
251+
t.Fatalf("Failed to read log file after close: %v", err)
262252
}
263-
defer exclusiveFile.Close()
264253

265-
err = syscall.Flock(int(exclusiveFile.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
266-
if err != nil {
267-
t.Errorf("Failed to acquire exclusive lock after close: %v", err)
268-
} else {
269-
// Successfully got exclusive lock - release it
270-
syscall.Flock(int(exclusiveFile.Fd()), syscall.LOCK_UN)
254+
if !strings.Contains(string(content), "Testing file readability") {
255+
t.Errorf("Log file does not contain expected content after close")
271256
}
272257
}
273258

274-
func TestFileLoggerLockReleasedOnClose(t *testing.T) {
259+
func TestFileLoggerFlushes(t *testing.T) {
275260
tmpDir := t.TempDir()
276261
logDir := filepath.Join(tmpDir, "logs")
277-
fileName := "lock-release-test.log"
262+
fileName := "flush-test.log"
278263
logPath := filepath.Join(logDir, fileName)
279264

280-
// Initialize and immediately close
265+
// Initialize the logger
281266
err := InitFileLogger(logDir, fileName)
282267
if err != nil {
283268
t.Fatalf("InitFileLogger failed: %v", err)
284269
}
270+
defer CloseGlobalLogger()
285271

286-
LogInfo("test", "Test message")
287-
288-
// Close the logger
289-
err = CloseGlobalLogger()
290-
if err != nil {
291-
t.Fatalf("CloseGlobalLogger failed: %v", err)
292-
}
272+
// Write a message
273+
LogInfo("test", "Immediate flush test")
293274

294-
// Verify we can acquire an exclusive lock after closing
295-
file, err := os.OpenFile(logPath, os.O_RDWR, 0644)
275+
// Immediately read the file without closing the logger
276+
// This tests that Sync() is being called after each write
277+
content, err := os.ReadFile(logPath)
296278
if err != nil {
297-
t.Fatalf("Failed to open log file: %v", err)
279+
t.Fatalf("Failed to read log file: %v", err)
298280
}
299-
defer file.Close()
300281

301-
// Should be able to get exclusive lock since logger is closed
302-
err = syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
303-
if err != nil {
304-
t.Fatalf("Failed to acquire exclusive lock after logger close: %v", err)
282+
if !strings.Contains(string(content), "Immediate flush test") {
283+
t.Errorf("Log file does not contain message immediately after write - data not flushed")
305284
}
306-
307-
// Clean up
308-
syscall.Flock(int(file.Fd()), syscall.LOCK_UN)
309285
}

test/integration/binary_test.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,3 +678,163 @@ func parseSSEResponse(t *testing.T, body string) map[string]interface{} {
678678

679679
return result
680680
}
681+
682+
// TestBinaryInvocation_LogFileCreation tests that the mcp-gateway.log file is created when the binary runs
683+
func TestBinaryInvocation_LogFileCreation(t *testing.T) {
684+
if testing.Short() {
685+
t.Skip("Skipping binary integration test in short mode")
686+
}
687+
688+
binaryPath := findBinary(t)
689+
t.Logf("Using binary: %s", binaryPath)
690+
691+
// Create a temporary directory for logs
692+
tmpLogDir, err := os.MkdirTemp("", "awmg-log-test-*")
693+
if err != nil {
694+
t.Fatalf("Failed to create temp log directory: %v", err)
695+
}
696+
defer os.RemoveAll(tmpLogDir)
697+
698+
// Create a temporary config file
699+
configFile := createTempConfig(t, map[string]interface{}{
700+
"testserver": map[string]interface{}{
701+
"command": "echo",
702+
"args": []string{},
703+
},
704+
})
705+
defer os.Remove(configFile)
706+
707+
// Start the server process with custom log directory
708+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
709+
defer cancel()
710+
711+
port := "13006"
712+
cmd := exec.CommandContext(ctx, binaryPath,
713+
"--config", configFile,
714+
"--listen", "127.0.0.1:"+port,
715+
"--log-dir", tmpLogDir,
716+
"--routed",
717+
)
718+
719+
var stdout, stderr bytes.Buffer
720+
cmd.Stdout = &stdout
721+
cmd.Stderr = &stderr
722+
723+
if err := cmd.Start(); err != nil {
724+
t.Fatalf("Failed to start server: %v", err)
725+
}
726+
727+
defer func() {
728+
if cmd.Process != nil {
729+
cmd.Process.Kill()
730+
}
731+
}()
732+
733+
// Wait for server to start
734+
serverURL := "http://127.0.0.1:" + port
735+
if !waitForServer(t, serverURL+"/health", 5*time.Second) {
736+
t.Logf("STDOUT: %s", stdout.String())
737+
t.Logf("STDERR: %s", stderr.String())
738+
t.Fatal("Server did not start in time")
739+
}
740+
741+
t.Log("✓ Server started successfully")
742+
743+
// Check that the log file was created
744+
logFilePath := filepath.Join(tmpLogDir, "mcp-gateway.log")
745+
if _, err := os.Stat(logFilePath); os.IsNotExist(err) {
746+
t.Fatalf("Log file was not created at %s", logFilePath)
747+
}
748+
749+
t.Logf("✓ Log file created at: %s", logFilePath)
750+
751+
// Read the log file to verify it contains log entries
752+
logContent, err := os.ReadFile(logFilePath)
753+
if err != nil {
754+
t.Fatalf("Failed to read log file: %v", err)
755+
}
756+
757+
if len(logContent) == 0 {
758+
t.Error("Log file is empty")
759+
} else {
760+
t.Logf("✓ Log file contains %d bytes", len(logContent))
761+
}
762+
763+
// Verify log file contains expected startup messages
764+
logStr := string(logContent)
765+
expectedMessages := []string{
766+
"startup",
767+
"Starting MCPG",
768+
}
769+
770+
for _, msg := range expectedMessages {
771+
if !bytes.Contains(logContent, []byte(msg)) {
772+
t.Errorf("Log file does not contain expected message: %q", msg)
773+
t.Logf("Log content:\n%s", logStr)
774+
}
775+
}
776+
777+
t.Log("✓ Log file contains expected startup messages")
778+
779+
// Verify log file permissions (should be 0644)
780+
fileInfo, err := os.Stat(logFilePath)
781+
if err != nil {
782+
t.Fatalf("Failed to stat log file: %v", err)
783+
}
784+
785+
perms := fileInfo.Mode().Perm()
786+
expectedPerms := os.FileMode(0644)
787+
if perms != expectedPerms {
788+
t.Errorf("Log file has incorrect permissions: got %o, expected %o", perms, expectedPerms)
789+
} else {
790+
t.Logf("✓ Log file has correct permissions: %o", perms)
791+
}
792+
793+
// Test that the log file is readable by other processes
794+
// Try to open and read the file while the server is still running
795+
readFile, err := os.Open(logFilePath)
796+
if err != nil {
797+
t.Fatalf("Failed to open log file for reading (concurrent access): %v", err)
798+
}
799+
defer readFile.Close()
800+
801+
// Read content
802+
concurrentRead, err := io.ReadAll(readFile)
803+
if err != nil {
804+
t.Fatalf("Failed to read log file concurrently: %v", err)
805+
}
806+
807+
if len(concurrentRead) == 0 {
808+
t.Error("Concurrent read returned empty content")
809+
} else {
810+
t.Logf("✓ Log file is readable by other processes (%d bytes read concurrently)", len(concurrentRead))
811+
}
812+
813+
// Make an API call to generate more log entries
814+
resp, err := http.Get(serverURL + "/health")
815+
if err == nil {
816+
resp.Body.Close()
817+
}
818+
819+
// Wait a moment for logs to be written
820+
time.Sleep(200 * time.Millisecond)
821+
822+
// Read the log file again to verify new entries were added (or at least it's still readable)
823+
newLogContent, err := os.ReadFile(logFilePath)
824+
if err != nil {
825+
t.Fatalf("Failed to read log file after API call: %v", err)
826+
}
827+
828+
// Log file should either grow or stay the same (health endpoint may not always log)
829+
if len(newLogContent) >= len(logContent) {
830+
if len(newLogContent) > len(logContent) {
831+
t.Logf("✓ Log file grew from %d to %d bytes after API call (immediate flush working)", len(logContent), len(newLogContent))
832+
} else {
833+
t.Logf("✓ Log file size unchanged (%d bytes) but still readable after API call", len(logContent))
834+
}
835+
} else {
836+
t.Errorf("Log file shrank from %d to %d bytes - unexpected behavior", len(logContent), len(newLogContent))
837+
}
838+
839+
t.Log("✓ All log file checks passed")
840+
}

0 commit comments

Comments
 (0)