From 9ff82c483b56f6e3e48448bdc022386e9d8b11f0 Mon Sep 17 00:00:00 2001 From: codernull <1481828312@qq.com> Date: Wed, 21 Jan 2026 05:44:42 +0000 Subject: [PATCH 1/2] Add Log File Rolling Support --- config/configuration.go | 53 ++++++ log/file/file_log.go | 126 ++++++++++--- log/file/file_log_test.go | 71 ++++++- log/file/rolling_writer.go | 318 ++++++++++++++++++++++++++++++++ log/file/rolling_writer_test.go | 164 ++++++++++++++++ 5 files changed, 710 insertions(+), 22 deletions(-) create mode 100644 log/file/rolling_writer.go create mode 100644 log/file/rolling_writer_test.go diff --git a/config/configuration.go b/config/configuration.go index 6282bab1e..ffe9392cc 100644 --- a/config/configuration.go +++ b/config/configuration.go @@ -884,6 +884,59 @@ const ( // - A valid path FileLogPath string = "FileLogPath" + // FileLogMaxSize sets the maximum size in megabytes before a log file is rotated. + // When set to 0, size-based rotation is disabled. + // FileLogMaxSize is only relevant if also using quickfix.NewFileLogFactory(..) in code + // when creating your LogFactory for your initiator or acceptor. + // + // Required: No + // + // Default: 0 (disabled) + // + // Valid Values: + // - A positive integer representing megabytes + FileLogMaxSize string = "FileLogMaxSize" + + // FileLogMaxBackups sets the maximum number of old log files to retain. + // When set to 0, all old log files are retained. + // FileLogMaxBackups is only relevant if also using quickfix.NewFileLogFactory(..) in code + // when creating your LogFactory for your initiator or acceptor. + // + // Required: No + // + // Default: 0 (retain all) + // + // Valid Values: + // - A non-negative integer + FileLogMaxBackups string = "FileLogMaxBackups" + + // FileLogMaxAge sets the maximum number of days to retain old log files based on the timestamp + // encoded in their filename. Files older than this will be deleted. + // When set to 0, age-based cleanup is disabled. + // FileLogMaxAge is only relevant if also using quickfix.NewFileLogFactory(..) in code + // when creating your LogFactory for your initiator or acceptor. + // + // Required: No + // + // Default: 0 (disabled) + // + // Valid Values: + // - A non-negative integer representing days + FileLogMaxAge string = "FileLogMaxAge" + + // FileLogCompress sets whether to compress rotated log files using gzip. + // FileLogCompress is only relevant if also using quickfix.NewFileLogFactory(..) in code + // when creating your LogFactory for your initiator or acceptor. + // + // Required: No + // + // Default: N (false) + // + // Valid Values: + // - Y (true) + // - N (false) + FileLogCompress string = "FileLogCompress" + // SQLLogDriver sets the name of the database driver to use for application logs (see https://go.dev/wiki/SQLDrivers for the list of available drivers). // SQLLogDriver is only relevant if also using sql.NewLogFactory(..) in code // when creating your LogFactory for your initiator or acceptor. diff --git a/log/file/file_log.go b/log/file/file_log.go index 9fd36a851..8640e8825 100644 --- a/log/file/file_log.go +++ b/log/file/file_log.go @@ -26,8 +26,10 @@ import ( ) type fileLog struct { - eventLogger *log.Logger - messageLogger *log.Logger + eventLogger *log.Logger + messageLogger *log.Logger + eventWriter *rollingWriter + messageWriter *rollingWriter } func (l fileLog) OnIncoming(msg []byte) { @@ -46,64 +48,141 @@ func (l fileLog) OnEventf(format string, v ...interface{}) { l.eventLogger.Printf(format, v...) } +type rollingConfig struct { + maxSize int + maxBackups int + maxAge int + compress bool +} + type fileLogFactory struct { globalLogPath string sessionLogPaths map[quickfix.SessionID]string + globalConfig rollingConfig + sessionConfigs map[quickfix.SessionID]rollingConfig } // NewLogFactory creates an instance of LogFactory that writes messages and events to file. // The location of global and session log files is configured via FileLogPath. +// Optional rolling configuration can be set via FileLogMaxSize, FileLogMaxBackups, FileLogMaxAge, and FileLogCompress. func NewLogFactory(settings *quickfix.Settings) (quickfix.LogFactory, error) { logFactory := fileLogFactory{} var err error - if logFactory.globalLogPath, err = settings.GlobalSettings().Setting(config.FileLogPath); err != nil { + globalSettings := settings.GlobalSettings() + if logFactory.globalLogPath, err = globalSettings.Setting(config.FileLogPath); err != nil { return logFactory, err } + // Read global rolling configuration + logFactory.globalConfig = readRollingConfig(globalSettings) + logFactory.sessionLogPaths = make(map[quickfix.SessionID]string) + logFactory.sessionConfigs = make(map[quickfix.SessionID]rollingConfig) + // SessionSettings() already merges global settings with session-specific settings for sid, sessionSettings := range settings.SessionSettings() { logPath, err := sessionSettings.Setting(config.FileLogPath) if err != nil { return logFactory, err } logFactory.sessionLogPaths[sid] = logPath + // Read merged rolling configuration (global + session overrides) + logFactory.sessionConfigs[sid] = readRollingConfig(sessionSettings) } return logFactory, nil } -func newFileLog(prefix string, logPath string) (fileLog, error) { - l := fileLog{} +// readRollingConfig reads rolling configuration from settings. +// Returns default values (all zeros/false) if settings are not present. +func readRollingConfig(settings *quickfix.SessionSettings) rollingConfig { + cfg := rollingConfig{} - eventLogName := path.Join(logPath, prefix+".event.current.log") - messageLogName := path.Join(logPath, prefix+".messages.current.log") + if settings.HasSetting(config.FileLogMaxSize) { + if maxSize, err := settings.IntSetting(config.FileLogMaxSize); err == nil { + cfg.maxSize = maxSize + } + } - if err := os.MkdirAll(logPath, os.ModePerm); err != nil { - return l, err + if settings.HasSetting(config.FileLogMaxBackups) { + if maxBackups, err := settings.IntSetting(config.FileLogMaxBackups); err == nil { + cfg.maxBackups = maxBackups + } } - fileFlags := os.O_RDWR | os.O_CREATE | os.O_APPEND - eventFile, err := os.OpenFile(eventLogName, fileFlags, os.ModePerm) - if err != nil { - return l, err + if settings.HasSetting(config.FileLogMaxAge) { + if maxAge, err := settings.IntSetting(config.FileLogMaxAge); err == nil { + cfg.maxAge = maxAge + } } - messageFile, err := os.OpenFile(messageLogName, fileFlags, os.ModePerm) - if err != nil { - return l, err + if settings.HasSetting(config.FileLogCompress) { + if compress, err := settings.BoolSetting(config.FileLogCompress); err == nil { + cfg.compress = compress + } } - logFlag := log.Ldate | log.Ltime | log.Lmicroseconds | log.LUTC - l.eventLogger = log.New(eventFile, "", logFlag) - l.messageLogger = log.New(messageFile, "", logFlag) + return cfg +} + + +func newFileLog(prefix string, logPath string, config rollingConfig) (fileLog, error) { + l := fileLog{} + + eventLogName := path.Join(logPath, prefix+".event.current.log") + messageLogName := path.Join(logPath, prefix+".messages.current.log") + + // Use rolling writer if any rolling option is enabled + useRolling := config.maxSize > 0 || config.maxAge > 0 || config.maxBackups > 0 || config.compress + + if useRolling { + // Create rolling writers + eventWriter, err := newRollingWriter(eventLogName, config.maxSize, config.maxBackups, config.maxAge, config.compress) + if err != nil { + return l, err + } + + messageWriter, err := newRollingWriter(messageLogName, config.maxSize, config.maxBackups, config.maxAge, config.compress) + if err != nil { + eventWriter.Close() + return l, err + } + + l.eventWriter = eventWriter + l.messageWriter = messageWriter + + logFlag := log.Ldate | log.Ltime | log.Lmicroseconds | log.LUTC + l.eventLogger = log.New(eventWriter, "", logFlag) + l.messageLogger = log.New(messageWriter, "", logFlag) + } else { + // Use regular file writers (backward compatible) + if err := os.MkdirAll(logPath, os.ModePerm); err != nil { + return l, err + } + + fileFlags := os.O_RDWR | os.O_CREATE | os.O_APPEND + eventFile, err := os.OpenFile(eventLogName, fileFlags, os.ModePerm) + if err != nil { + return l, err + } + + messageFile, err := os.OpenFile(messageLogName, fileFlags, os.ModePerm) + if err != nil { + eventFile.Close() + return l, err + } + + logFlag := log.Ldate | log.Ltime | log.Lmicroseconds | log.LUTC + l.eventLogger = log.New(eventFile, "", logFlag) + l.messageLogger = log.New(messageFile, "", logFlag) + } return l, nil } func (f fileLogFactory) Create() (quickfix.Log, error) { - return newFileLog("GLOBAL", f.globalLogPath) + return newFileLog("GLOBAL", f.globalLogPath, f.globalConfig) } func (f fileLogFactory) CreateSessionLog(sessionID quickfix.SessionID) (quickfix.Log, error) { @@ -114,5 +193,10 @@ func (f fileLogFactory) CreateSessionLog(sessionID quickfix.SessionID) (quickfix } prefix := sessionIDFilenamePrefix(sessionID) - return newFileLog(prefix, logPath) + // Use session config (already merged with global in NewLogFactory) + config := f.globalConfig + if sessionConfig, hasSessionConfig := f.sessionConfigs[sessionID]; hasSessionConfig { + config = sessionConfig + } + return newFileLog(prefix, logPath, config) } diff --git a/log/file/file_log_test.go b/log/file/file_log_test.go index cf25e8474..e89836799 100644 --- a/log/file/file_log_test.go +++ b/log/file/file_log_test.go @@ -77,7 +77,9 @@ func newFileLogHelper(t *testing.T) *fileLogHelper { prefix := "myprefix" logPath := path.Join(os.TempDir(), fmt.Sprintf("TestLogStore-%d", os.Getpid())) - log, err := newFileLog(prefix, logPath) + // Use default config (no rolling) for backward compatibility test + config := rollingConfig{} + log, err := newFileLog(prefix, logPath, config) if err != nil { t.Error("Unexpected error", err) } @@ -145,3 +147,70 @@ func TestFileLog_Append(t *testing.T) { t.Error("Unexpected EOF") } } + +func TestFileLog_RollingConfig(t *testing.T) { + cfg := ` +[DEFAULT] +ConnectionType=initiator +FileLogPath=. +FileLogMaxSize=1 +FileLogMaxBackups=3 +FileLogMaxAge=7 +FileLogCompress=Y + +[SESSION] +BeginString=FIX.4.1 +TargetCompID=ARCA +SenderCompID=TW +` + stringReader := strings.NewReader(cfg) + settings, err := quickfix.ParseSettings(stringReader) + if err != nil { + t.Fatal("Failed to parse settings", err) + } + + factory, err := NewLogFactory(settings) + if err != nil { + t.Fatal("Did not expect error", err) + } + + if factory == nil { + t.Fatal("Should have returned factory") + } + + // Test that factory was created with rolling config + _ = factory +} + +func TestFileLog_RollingBackwardCompatible(t *testing.T) { + // Test that without rolling config, behavior is unchanged + cfg := ` +[DEFAULT] +ConnectionType=initiator +FileLogPath=. + +[SESSION] +BeginString=FIX.4.1 +TargetCompID=ARCA +SenderCompID=TW +` + stringReader := strings.NewReader(cfg) + settings, err := quickfix.ParseSettings(stringReader) + if err != nil { + t.Fatal("Failed to parse settings", err) + } + + factory, err := NewLogFactory(settings) + if err != nil { + t.Fatal("Did not expect error", err) + } + + log, err := factory.Create() + if err != nil { + t.Fatal("Did not expect error creating log", err) + } + + // Should work without rolling + log.OnEvent("Test event") + log.OnIncoming([]byte("Test message")) +} diff --git a/log/file/rolling_writer.go b/log/file/rolling_writer.go new file mode 100644 index 000000000..c3303b273 --- /dev/null +++ b/log/file/rolling_writer.go @@ -0,0 +1,318 @@ +// Copyright (c) quickfixengine.org All rights reserved. +// +// This file may be distributed under the terms of the quickfixengine.org +// license as defined by quickfixengine.org and appearing in the file +// LICENSE included in the packaging of this file. +// +// This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING +// THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE. +// +// See http://www.quickfixengine.org/LICENSE for licensing information. +// +// Contact ask@quickfixengine.org if any conditions of this licensing +// are not clear to you. + +package file + +import ( + "compress/gzip" + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strings" + "sync" + "time" +) + +// rollingWriter wraps a file with rotation support based on size and age. +type rollingWriter struct { + mu sync.Mutex + + filename string + currentFile *os.File + maxSize int64 // in bytes, 0 means disabled + maxBackups int + maxAge int // in days, 0 means disabled + compress bool + + currentSize int64 + lastRotate time.Time +} + +// newRollingWriter creates a new rolling writer. +// filename: base filename (e.g., "prefix.event.current.log") +// maxSize: maximum size in megabytes before rotation (0 = disabled) +// maxBackups: maximum number of old log files to keep (0 = keep all) +// maxAge: maximum age in days for old log files (0 = disabled) +// compress: whether to compress rotated files +func newRollingWriter(filename string, maxSize int, maxBackups int, maxAge int, compress bool) (*rollingWriter, error) { + w := &rollingWriter{ + filename: filename, + maxSize: int64(maxSize) * 1024 * 1024, // convert MB to bytes + maxBackups: maxBackups, + maxAge: maxAge, + compress: compress, + lastRotate: time.Now(), + } + + if err := w.openCurrentFile(); err != nil { + return nil, err + } + + // Get initial file size + if info, err := w.currentFile.Stat(); err == nil { + w.currentSize = info.Size() + } + + return w, nil +} + +// openCurrentFile opens the current log file in append mode. +func (w *rollingWriter) openCurrentFile() error { + dir := filepath.Dir(w.filename) + if err := os.MkdirAll(dir, os.ModePerm); err != nil { + return err + } + + fileFlags := os.O_RDWR | os.O_CREATE | os.O_APPEND + file, err := os.OpenFile(w.filename, fileFlags, os.ModePerm) + if err != nil { + return err + } + + w.currentFile = file + return nil +} + +// Write writes data to the log file, rotating if necessary. +func (w *rollingWriter) Write(p []byte) (n int, err error) { + w.mu.Lock() + defer w.mu.Unlock() + + // Check if rotation is needed before writing + if err := w.maybeRotate(); err != nil { + return 0, err + } + + n, err = w.currentFile.Write(p) + if err == nil { + w.currentSize += int64(n) + // Check again after writing in case we exceeded the limit + if err = w.maybeRotate(); err != nil { + return n, err + } + } + return n, err +} + +// maybeRotate checks if rotation is needed and performs it. +func (w *rollingWriter) maybeRotate() error { + now := time.Now() + needsRotate := false + + // Check size-based rotation + if w.maxSize > 0 && w.currentSize >= w.maxSize { + needsRotate = true + } + + // Check time-based rotation (daily) + if !needsRotate && w.maxAge > 0 { + // Rotate if it's a new day + if now.YearDay() != w.lastRotate.YearDay() || now.Year() != w.lastRotate.Year() { + needsRotate = true + } + } + + if !needsRotate { + return nil + } + + return w.rotate() +} + +// rotate performs the actual rotation. +func (w *rollingWriter) rotate() error { + // Close current file + if w.currentFile != nil { + if err := w.currentFile.Close(); err != nil { + return err + } + } + + // Generate rotated filename with timestamp + timestamp := time.Now().Format("20060102-150405") + ext := filepath.Ext(w.filename) + base := strings.TrimSuffix(w.filename, ext) + rotatedFilename := fmt.Sprintf("%s.%s%s", base, timestamp, ext) + + // Rename current file to rotated filename + if err := os.Rename(w.filename, rotatedFilename); err != nil { + // If rename fails, try to open current file again + w.openCurrentFile() + return err + } + + // Compress if enabled + if w.compress { + if err := w.compressFile(rotatedFilename); err != nil { + // Log error but continue + _ = err + } else { + // Remove uncompressed file after compression + if err := os.Remove(rotatedFilename); err != nil { + // Log error but continue + _ = err + } + } + } + + // Clean up old files + if err := w.cleanup(); err != nil { + // Log error but continue + _ = err + } + + // Open new current file + if err := w.openCurrentFile(); err != nil { + return err + } + + // Reset size and update last rotate time + if info, err := w.currentFile.Stat(); err == nil { + w.currentSize = info.Size() + } else { + w.currentSize = 0 + } + w.lastRotate = time.Now() + + return nil +} + +// compressFile compresses a file using gzip. +func (w *rollingWriter) compressFile(filename string) error { + file, err := os.Open(filename) + if err != nil { + return err + } + defer file.Close() + + compressedFilename := filename + ".gz" + compressedFile, err := os.Create(compressedFilename) + if err != nil { + return err + } + defer compressedFile.Close() + + gzWriter := gzip.NewWriter(compressedFile) + defer gzWriter.Close() + + _, err = io.Copy(gzWriter, file) + return err +} + +// cleanup removes old log files based on maxBackups and maxAge. +func (w *rollingWriter) cleanup() error { + dir := filepath.Dir(w.filename) + base := filepath.Base(w.filename) + ext := filepath.Ext(base) + baseName := strings.TrimSuffix(base, ext) + + // Pattern to match rotated files: baseName.YYYYMMDD-HHMMSS.ext or baseName.YYYYMMDD-HHMMSS.ext.gz + pattern := filepath.Join(dir, baseName+".*"+ext+"*") + matches, err := filepath.Glob(pattern) + if err != nil { + return err + } + + // Filter out current file + var rotatedFiles []string + for _, match := range matches { + if match != w.filename { + rotatedFiles = append(rotatedFiles, match) + } + } + + // Sort by modification time (oldest first) + sort.Slice(rotatedFiles, func(i, j int) bool { + infoI, errI := os.Stat(rotatedFiles[i]) + infoJ, errJ := os.Stat(rotatedFiles[j]) + if errI != nil || errJ != nil { + return false + } + return infoI.ModTime().Before(infoJ.ModTime()) + }) + + now := time.Now() + var toDelete []string + + // Apply maxAge filter + if w.maxAge > 0 { + for _, file := range rotatedFiles { + info, err := os.Stat(file) + if err != nil { + continue + } + age := now.Sub(info.ModTime()) + if age > time.Duration(w.maxAge)*24*time.Hour { + toDelete = append(toDelete, file) + } + } + } + + // Apply maxBackups filter + if w.maxBackups > 0 { + // Remove files that exceed maxBackups (after removing those deleted by maxAge) + remaining := make([]string, 0) + for _, file := range rotatedFiles { + isDeleted := false + for _, deleted := range toDelete { + if file == deleted { + isDeleted = true + break + } + } + if !isDeleted { + remaining = append(remaining, file) + } + } + + if len(remaining) > w.maxBackups { + excess := len(remaining) - w.maxBackups + for i := 0; i < excess; i++ { + toDelete = append(toDelete, remaining[i]) + } + } + } + + // Delete files + for _, file := range toDelete { + _ = os.Remove(file) + } + + return nil +} + +// Close closes the current log file. +func (w *rollingWriter) Close() error { + w.mu.Lock() + defer w.mu.Unlock() + + if w.currentFile != nil { + return w.currentFile.Close() + } + return nil +} + +// Sync syncs the current log file. +func (w *rollingWriter) Sync() error { + w.mu.Lock() + defer w.mu.Unlock() + + if w.currentFile != nil { + return w.currentFile.Sync() + } + return nil +} diff --git a/log/file/rolling_writer_test.go b/log/file/rolling_writer_test.go new file mode 100644 index 000000000..433b2e4e1 --- /dev/null +++ b/log/file/rolling_writer_test.go @@ -0,0 +1,164 @@ +// Copyright (c) quickfixengine.org All rights reserved. +// +// This file may be distributed under the terms of the quickfixengine.org +// license as defined by quickfixengine.org and appearing in the file +// LICENSE included in the packaging of this file. +// +// This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING +// THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE. +// +// See http://www.quickfixengine.org/LICENSE for licensing information. +// +// Contact ask@quickfixengine.org if any conditions of this licensing +// are not clear to you. + +package file + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +func TestRollingWriter_SizeBasedRotation(t *testing.T) { + tmpDir := filepath.Join(os.TempDir(), fmt.Sprintf("TestRolling-%d", os.Getpid())) + defer os.RemoveAll(tmpDir) + + filename := filepath.Join(tmpDir, "test.log") + // Use very small max size (1MB) for testing + writer, err := newRollingWriter(filename, 1, 3, 0, false) + if err != nil { + t.Fatal("Failed to create rolling writer", err) + } + defer writer.Close() + + // Write data that exceeds 1MB + data := strings.Repeat("x", 1024*1024+1) + _, err = writer.Write([]byte(data)) + if err != nil { + t.Fatal("Failed to write", err) + } + + // Sync to ensure rotation happens + writer.Sync() + + // Small delay to ensure file operations complete + time.Sleep(100 * time.Millisecond) + + // Check if rotation occurred + matches, err := filepath.Glob(filepath.Join(tmpDir, "test.*.log")) + if err != nil { + t.Fatal("Failed to glob", err) + } + + // Should have at least one rotated file + if len(matches) == 0 { + t.Error("Expected rotated file, but found none") + } +} + +func TestRollingWriter_MaxBackups(t *testing.T) { + tmpDir := filepath.Join(os.TempDir(), fmt.Sprintf("TestRolling-%d", os.Getpid())) + defer os.RemoveAll(tmpDir) + + filename := filepath.Join(tmpDir, "test.log") + // Use very small max size and limit backups to 2 + writer, err := newRollingWriter(filename, 1, 2, 0, false) + if err != nil { + t.Fatal("Failed to create rolling writer", err) + } + defer writer.Close() + + // Trigger multiple rotations + for i := 0; i < 5; i++ { + data := strings.Repeat("x", 1024*1024+1) + _, err = writer.Write([]byte(data)) + if err != nil { + t.Fatal("Failed to write", err) + } + time.Sleep(10 * time.Millisecond) // Small delay to ensure different timestamps + } + + // Check rotated files + matches, err := filepath.Glob(filepath.Join(tmpDir, "test.*.log")) + if err != nil { + t.Fatal("Failed to glob", err) + } + + // Should have at most 2 rotated files (maxBackups=2) + if len(matches) > 2 { + t.Errorf("Expected at most 2 rotated files, but found %d", len(matches)) + } +} + +func TestRollingWriter_Compression(t *testing.T) { + tmpDir := filepath.Join(os.TempDir(), fmt.Sprintf("TestRolling-%d", os.Getpid())) + defer os.RemoveAll(tmpDir) + + filename := filepath.Join(tmpDir, "test.log") + // Enable compression + writer, err := newRollingWriter(filename, 1, 1, 0, true) + if err != nil { + t.Fatal("Failed to create rolling writer", err) + } + defer writer.Close() + + // Trigger rotation + data := strings.Repeat("x", 1024*1024+1) + _, err = writer.Write([]byte(data)) + if err != nil { + t.Fatal("Failed to write", err) + } + + // Sync to ensure rotation happens + writer.Sync() + + // Small delay to ensure file operations complete + time.Sleep(100 * time.Millisecond) + + // Check if compressed file exists + matches, err := filepath.Glob(filepath.Join(tmpDir, "test.*.log.gz")) + if err != nil { + t.Fatal("Failed to glob", err) + } + + // Should have at least one compressed file + if len(matches) == 0 { + t.Error("Expected compressed file, but found none") + } +} + +func TestRollingWriter_NoRotationWhenDisabled(t *testing.T) { + tmpDir := filepath.Join(os.TempDir(), fmt.Sprintf("TestRolling-%d", os.Getpid())) + defer os.RemoveAll(tmpDir) + + filename := filepath.Join(tmpDir, "test.log") + // All rolling options disabled + writer, err := newRollingWriter(filename, 0, 0, 0, false) + if err != nil { + t.Fatal("Failed to create rolling writer", err) + } + defer writer.Close() + + // Write large amount of data + data := strings.Repeat("x", 10*1024*1024) + _, err = writer.Write([]byte(data)) + if err != nil { + t.Fatal("Failed to write", err) + } + + // Check that no rotation occurred + matches, err := filepath.Glob(filepath.Join(tmpDir, "test.*.log")) + if err != nil { + t.Fatal("Failed to glob", err) + } + + // Should have no rotated files + if len(matches) > 0 { + t.Error("Expected no rotated files when rotation is disabled") + } +} From 35546b71d759d677ffed60b80bf88f66fff483cc Mon Sep 17 00:00:00 2001 From: codernull <1481828312@qq.com> Date: Wed, 21 Jan 2026 14:12:34 +0800 Subject: [PATCH 2/2] format --- log/file/file_log.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/log/file/file_log.go b/log/file/file_log.go index 8640e8825..74dd3e1e7 100644 --- a/log/file/file_log.go +++ b/log/file/file_log.go @@ -26,8 +26,8 @@ import ( ) type fileLog struct { - eventLogger *log.Logger - messageLogger *log.Logger + eventLogger *log.Logger + messageLogger *log.Logger eventWriter *rollingWriter messageWriter *rollingWriter } @@ -126,7 +126,6 @@ func readRollingConfig(settings *quickfix.SessionSettings) rollingConfig { return cfg } - func newFileLog(prefix string, logPath string, config rollingConfig) (fileLog, error) { l := fileLog{}