From a675c9ba33ee7e157dd48ae846b2e5e9a9fe0339 Mon Sep 17 00:00:00 2001 From: ukpratik Date: Sun, 1 Mar 2026 11:31:13 +0530 Subject: [PATCH] fix: add write mutex for log writing to file --- pkg/logger/logger.go | 22 ++++++++++++++++++++-- pkg/logger/logger_test.go | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 56dc87a53..a99c262ab 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -38,6 +38,14 @@ var ( type Logger struct { file *os.File + + // writeMu serializes file writes. + // Although POSIX systems typically guarantee atomicity + // for single write() syscalls on regular files, + // Windows and some filesystems may not. + // + // This prevents log line interleaving across platforms. + writeMu sync.Mutex } type LogEntry struct { @@ -116,13 +124,23 @@ func logMessage(level LogLevel, component string, message string, fields map[str } } - if logger.file != nil { + mu.RLock() + fileptr := logger.file + + if fileptr != nil { jsonData, err := json.Marshal(entry) if err == nil { - logger.file.Write(append(jsonData, '\n')) + logger.writeMu.Lock() + _, err = fileptr.WriteString(string(jsonData) + "\n") + logger.writeMu.Unlock() + if err != nil { + log.Println("Failed to write to file:", err.Error()) + } } } + mu.RUnlock() + var fieldStr string if len(fields) > 0 { fieldStr = " " + formatFields(fields) diff --git a/pkg/logger/logger_test.go b/pkg/logger/logger_test.go index 6e6f8dfa8..815ef5878 100644 --- a/pkg/logger/logger_test.go +++ b/pkg/logger/logger_test.go @@ -1,6 +1,11 @@ package logger import ( + "fmt" + "os" + "path/filepath" + "strings" + "sync" "testing" ) @@ -137,3 +142,31 @@ func TestLoggerHelperFunctions(t *testing.T) { DebugC("test", "Debug with component") WarnF("Warning with fields", map[string]any{"key": "value"}) } + +// TestConcurrentFileLogging validates concurrent file writes are race-safe and complete. +// Run: go test -race ./pkg/logger -run TestConcurrentFileLogging -count=1 +func TestConcurrentFileLogging(t *testing.T) { + dir := t.TempDir() + logFile := filepath.Join(dir, "test.log") + if err := EnableFileLogging(logFile); err != nil { + t.Fatal(err) + } + defer DisableFileLogging() + var wg sync.WaitGroup + for i := 0; i < 100; i++ { + wg.Add(1) + go func(n int) { + defer wg.Done() + InfoCF("test", fmt.Sprintf("msg-%d", n), map[string]any{"n": n}) + }(i) + } + wg.Wait() + data, err := os.ReadFile(logFile) + if err != nil { + t.Fatal(err) + } + lines := strings.Split(strings.TrimSpace(string(data)), "\n") + if len(lines) != 100 { + t.Errorf("expected 100 log lines, got %d", len(lines)) + } +}