Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions config/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
121 changes: 102 additions & 19 deletions log/file/file_log.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import (
type fileLog struct {
eventLogger *log.Logger
messageLogger *log.Logger
eventWriter *rollingWriter
messageWriter *rollingWriter
}

func (l fileLog) OnIncoming(msg []byte) {
Expand All @@ -46,64 +48,140 @@ 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) {
Expand All @@ -114,5 +192,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)
}
71 changes: 70 additions & 1 deletion log/file/file_log_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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"))
}
Loading
Loading