diff --git a/weed/glog/glog.go b/weed/glog/glog.go index e04df39e6..2206e42f3 100644 --- a/weed/glog/glog.go +++ b/weed/glog/glog.go @@ -811,11 +811,12 @@ func (l *loggingT) exit(err error) { // file rotation. There are conflicting methods, so the file cannot be embedded. // l.mu is held for all its methods. type syncBuffer struct { - logger *loggingT + logger *loggingT *bufio.Writer - file *os.File - sev severity - nbytes uint64 // The number of bytes written to this file + file *os.File + sev severity + nbytes uint64 // The number of bytes written to this file + createdAt time.Time // When the current log file was opened (used for time-based rotation) } func (sb *syncBuffer) Sync() error { @@ -830,8 +831,14 @@ func (sb *syncBuffer) Write(p []byte) (n int, err error) { if sb.Writer == nil { return 0, errors.New("log writer is nil") } - if sb.nbytes+uint64(len(p)) >= MaxSize { - if err := sb.rotateFile(time.Now()); err != nil { + now := timeNow() + // Size-based rotation: rotate when the file would exceed MaxSize. + sizeRotation := sb.nbytes+uint64(len(p)) >= MaxSize + // Time-based rotation: rotate when the file is older than --log_rotate_hours. + h := LogRotateHours() + timeRotation := h > 0 && !sb.createdAt.IsZero() && now.Sub(sb.createdAt) >= time.Duration(h)*time.Hour + if sizeRotation || timeRotation { + if err := sb.rotateFile(now); err != nil { sb.logger.exit(err) return 0, err } @@ -853,6 +860,7 @@ func (sb *syncBuffer) rotateFile(now time.Time) error { var err error sb.file, _, err = create(severityName[sb.sev], now) sb.nbytes = 0 + sb.createdAt = now if err != nil { return err } diff --git a/weed/glog/glog_file.go b/weed/glog/glog_file.go index f91acf82a..e0de6e958 100644 --- a/weed/glog/glog_file.go +++ b/weed/glog/glog_file.go @@ -57,6 +57,13 @@ var logMaxSizeMB = flag.Uint64("log_max_size_mb", 1800, "Maximum size in megabyt // Defaults to 5. var logMaxFiles = flag.Int("log_max_files", 5, "Maximum number of log files to keep per severity level before older ones are deleted (0 = use default of 5)") +// logRotateHours controls time-based log rotation. +// When non-zero, each log file is rotated after the given number of hours +// regardless of its size. This prevents log files from accumulating in +// long-running deployments even when log volume is low. +// The default is 168 hours (7 days). Set to 0 to disable time-based rotation. +var logRotateHours = flag.Int("log_rotate_hours", 168, "Rotate log files after this many hours (default: 168 = 7 days, 0 = disabled)") + func createLogDirs() { // Apply flag values now that flags have been parsed. if *logMaxSizeMB > 0 { @@ -73,6 +80,12 @@ func createLogDirs() { } } +// LogRotateHours returns the configured time-based rotation interval. +// This is used by syncBuffer to decide when to rotate open log files. +func LogRotateHours() int { + return *logRotateHours +} + var ( pid = os.Getpid() program = filepath.Base(os.Args[0]) diff --git a/weed/glog/glog_test.go b/weed/glog/glog_test.go index 4a667259b..48d1c9ec5 100644 --- a/weed/glog/glog_test.go +++ b/weed/glog/glog_test.go @@ -371,6 +371,56 @@ func TestRollover(t *testing.T) { } } +func TestTimeBasedRollover(t *testing.T) { + setFlags() + var err error + defer func(previous func(error)) { logExitFunc = previous }(logExitFunc) + logExitFunc = func(e error) { + err = e + } + + // Disable size-based rotation by setting a very large MaxSize. + defer func(previous uint64) { MaxSize = previous }(MaxSize) + MaxSize = 1024 * 1024 * 1024 + + // Enable time-based rotation with a 1-hour interval. + defer func(previous int) { *logRotateHours = previous }(*logRotateHours) + *logRotateHours = 1 + + Info("x") // Create initial file. + info, ok := logging.file[infoLog].(*syncBuffer) + if !ok { + t.Fatal("info wasn't created") + } + if err != nil { + t.Fatalf("info has initial error: %v", err) + } + fname0 := info.file.Name() + createdAt := info.createdAt + + // Mock time to 30 minutes after file creation — should NOT rotate. + defer func(previous func() time.Time) { timeNow = previous }(timeNow) + timeNow = func() time.Time { return createdAt.Add(30 * time.Minute) } + Info("still within interval") + if err != nil { + t.Fatalf("error after write within interval: %v", err) + } + if info.file.Name() != fname0 { + t.Error("file rotated before interval elapsed") + } + + // Advance mock time past the 1-hour interval — should rotate. + timeNow = func() time.Time { return createdAt.Add(61 * time.Minute) } + Info("past interval") + if err != nil { + t.Fatalf("error after time-based rotation: %v", err) + } + fname1 := info.file.Name() + if fname0 == fname1 { + t.Error("file did not rotate after interval elapsed") + } +} + func TestLogBacktraceAt(t *testing.T) { setFlags() defer logging.swap(logging.newBuffers())