glog: add --log_rotate_hours flag for time-based log rotation (#8685)

* glog: add --log_rotate_hours flag for time-based log rotation

SeaweedFS previously only rotated log files when they reached MaxSize
(1.8 GB). Long-running deployments with low log volume could accumulate
log files indefinitely with no way to force rotation on a schedule.

This change adds the --log_rotate_hours flag. When set to a non-zero
value, the current log file is rotated once it has been open for the
specified number of hours, regardless of its size.

Implementation details:
- New flag --log_rotate_hours (int, default 0 = disabled) in glog_file.go
- Added createdAt time.Time field to syncBuffer to track file open time
- rotateFile() sets createdAt to the time the new file is opened
- Write() checks elapsed time and triggers rotation when the threshold
  is exceeded, consistent with the existing size-based check

This resolves the long-standing request for time-based rotation and
helps prevent unbounded log accumulation in /tmp on production systems.

Related: #3455, #5763, #8336

* glog: default log_rotate_hours to 168 (7 days)

Enable time-based rotation by default so log files don't accumulate
indefinitely in long-running deployments. Set to 0 to disable.

* glog: simplify rotation logic by combining size and time conditions

Merge the two separate rotation checks into a single block to
eliminate duplicated rotateFile error handling.

* glog: use timeNow() in syncBuffer.Write and add time-based rotation test

Use the existing testable timeNow variable instead of time.Now() in
syncBuffer.Write so that time-based rotation can be tested with a
mocked clock.

Add TestTimeBasedRollover that verifies:
- no rotation occurs before the interval elapses
- rotation triggers after the configured hours

---------

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
JARDEL ALVES
2026-03-18 17:19:14 -03:00
committed by GitHub
parent d34da671eb
commit bd3a6b1b33
3 changed files with 77 additions and 6 deletions

View File

@@ -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
}

View File

@@ -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])

View File

@@ -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())