package logging import ( "bytes" "compress/gzip" "errors" "fmt" "io" "os" "path/filepath" "reflect" "strings" "sync" "testing" "time" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "golang.org/x/term" ) func resetLoggingState() { mu.Lock() defer mu.Unlock() // Always use the real close function during test cleanup so active writers // can be closed even when a test has stubbed closeFileFn. closeFileFn = defaultCloseFileFn if fileCloser != nil { _ = fileCloser.Close() fileCloser = nil } baseWriter = os.Stderr baseComponent = "" baseLogger = zerolog.New(baseWriter).With().Timestamp().Logger() log.Logger = baseLogger zerolog.TimeFieldFormat = defaultTimeFmt zerolog.SetGlobalLevel(zerolog.InfoLevel) nowFn = time.Now isTerminalFn = term.IsTerminal mkdirAllFn = os.MkdirAll chmodFn = os.Chmod openFileFn = os.OpenFile openFn = os.Open statFn = os.Stat lstatFn = os.Lstat readDirFn = os.ReadDir renameFn = os.Rename removeFn = os.Remove copyFn = io.Copy gzipNewWriterFn = gzip.NewWriter statFileFn = defaultStatFileFn chmodFileFn = func(file *os.File, mode os.FileMode) error { return file.Chmod(mode) } closeFileFn = defaultCloseFileFn compressFn = compressAndRemove GetBroadcaster().Shutdown() } func baseWriterDebugString() string { return fmt.Sprintf("%#v", baseWriter) } func captureStderr(t *testing.T, fn func()) string { t.Helper() originalStderr := os.Stderr readPipe, writePipe, err := os.Pipe() if err != nil { t.Fatalf("failed to create stderr pipe: %v", err) } os.Stderr = writePipe defer func() { os.Stderr = originalStderr _ = readPipe.Close() _ = writePipe.Close() }() fn() if err := writePipe.Close(); err != nil { t.Fatalf("failed to close write pipe: %v", err) } output, err := io.ReadAll(readPipe) if err != nil { t.Fatalf("failed to read stderr output: %v", err) } return string(output) } func TestInitJSONFormatSetsLevelAndComponent(t *testing.T) { t.Cleanup(resetLoggingState) Init(Config{ Format: "json", Level: "debug", Component: "apiserver", }) mu.RLock() defer mu.RUnlock() repr := baseWriterDebugString() if !strings.Contains(repr, fmt.Sprintf("(%p)", os.Stderr)) { t.Fatalf("expected base writer to include os.Stderr, got %#v", baseWriter) } if !strings.Contains(repr, "LogBroadcaster") { t.Fatalf("expected base writer to include broadcaster, got %#v", baseWriter) } if zerolog.GlobalLevel() != zerolog.DebugLevel { t.Fatalf("expected global level debug, got %s", zerolog.GlobalLevel()) } if baseComponent != "apiserver" { t.Fatalf("expected base component apiserver, got %s", baseComponent) } if !reflect.DeepEqual(log.Logger, baseLogger) { t.Fatal("expected global log.Logger to match baseLogger") } } func TestInitConsoleFormatUsesConsoleWriter(t *testing.T) { t.Cleanup(resetLoggingState) Init(Config{ Format: "console", Level: "info", }) mu.RLock() defer mu.RUnlock() repr := baseWriterDebugString() if !strings.Contains(repr, "zerolog.ConsoleWriter") { t.Fatalf("expected console writer, got %#v", baseWriter) } if !strings.Contains(repr, "LogBroadcaster") { t.Fatalf("expected base writer to include broadcaster, got %#v", baseWriter) } } func TestInitAutoFormatWithPipe(t *testing.T) { t.Cleanup(resetLoggingState) origStderr := os.Stderr r, w, err := os.Pipe() if err != nil { t.Fatalf("failed to create pipe: %v", err) } os.Stderr = w defer func() { os.Stderr = origStderr _ = r.Close() _ = w.Close() }() Init(Config{ Format: "auto", Level: "info", }) mu.RLock() defer mu.RUnlock() repr := baseWriterDebugString() if !strings.Contains(repr, fmt.Sprintf("(%p)", w)) { t.Fatalf("expected base writer to use provided pipe, got %#v", baseWriter) } if !strings.Contains(repr, "LogBroadcaster") { t.Fatalf("expected base writer to include broadcaster, got %#v", baseWriter) } } func TestWithRequestID(t *testing.T) { t.Cleanup(resetLoggingState) Init(Config{ Format: "json", Level: "info", }) ctx, generated := WithRequestID(nil, "") if generated == "" { t.Fatal("expected generated request id") } if ctx == nil { t.Fatal("expected non-nil context") } ctx2, id := WithRequestID(nil, "custom-123") if id != "custom-123" { t.Fatalf("expected custom-123, got %s", id) } if ctx2 == nil { t.Fatal("expected non-nil context") } } func TestWithRequestIDTrimsWhitespace(t *testing.T) { t.Cleanup(resetLoggingState) Init(Config{}) _, id := WithRequestID(nil, " ") if id == "" { t.Fatal("expected generated id for whitespace input") } } func TestInitThreadSafety(t *testing.T) { t.Cleanup(resetLoggingState) var wg sync.WaitGroup configs := []Config{ {Format: "json", Level: "debug", Component: "worker"}, {Format: "json", Level: "warn", Component: "api"}, } for i := 0; i < 50; i++ { wg.Add(1) go func(idx int) { defer wg.Done() Init(configs[idx%len(configs)]) }(i) } wg.Wait() mu.RLock() defer mu.RUnlock() // Ensure baseLogger is valid and global logger matches it. if reflect.DeepEqual(baseLogger, zerolog.Logger{}) { t.Fatal("expected initialized base logger") } if !reflect.DeepEqual(log.Logger, baseLogger) { t.Fatal("expected global log.Logger to match baseLogger after concurrent init") } } func TestIsLevelEnabled(t *testing.T) { t.Cleanup(resetLoggingState) // Set global level to Info zerolog.SetGlobalLevel(zerolog.InfoLevel) if !IsLevelEnabled(zerolog.InfoLevel) { t.Fatal("expected info level to be enabled") } if !IsLevelEnabled(zerolog.WarnLevel) { t.Fatal("expected warn level to be enabled") } if !IsLevelEnabled(zerolog.ErrorLevel) { t.Fatal("expected error level to be enabled") } if IsLevelEnabled(zerolog.DebugLevel) { t.Fatal("expected debug level to be disabled") } // Change to debug level zerolog.SetGlobalLevel(zerolog.DebugLevel) if !IsLevelEnabled(zerolog.DebugLevel) { t.Fatal("expected debug level to be enabled after setting global level") } } func TestRollingFileWriter(t *testing.T) { t.Cleanup(resetLoggingState) dir := t.TempDir() logFile := dir + "/test.log" cfg := Config{ Format: "json", Level: "info", FilePath: logFile, MaxSizeMB: 1, MaxAgeDays: 7, Compress: false, } Init(cfg) // Write some log output log.Info().Msg("test message") // Check file exists (this confirms rolling file writer was created) if _, err := os.Stat(logFile); os.IsNotExist(err) { t.Fatal("expected log file to be created") } // Check file has content data, err := os.ReadFile(logFile) if err != nil { t.Fatalf("failed to read log file: %v", err) } if len(data) == 0 { t.Fatal("expected log file to have content") } } func TestInitClosesPreviousRollingFileWriter(t *testing.T) { t.Cleanup(resetLoggingState) dir := t.TempDir() logFile := filepath.Join(dir, "app.log") closeCalls := 0 closeFileFn = func(file *os.File) error { closeCalls++ return file.Close() } Init(Config{Format: "json", FilePath: logFile}) Init(Config{Format: "json", FilePath: logFile}) if closeCalls != 1 { t.Fatalf("expected previous file writer to be closed once, got %d", closeCalls) } } func TestShutdownClosesActiveRollingFileWriterAndSubscribers(t *testing.T) { t.Cleanup(resetLoggingState) dir := t.TempDir() logFile := filepath.Join(dir, "app.log") closeCalls := 0 closeFileFn = func(file *os.File) error { closeCalls++ return file.Close() } Init(Config{Format: "json", FilePath: logFile}) _, ch, _ := GetBroadcaster().Subscribe() Shutdown() Shutdown() if closeCalls != 1 { t.Fatalf("expected active file writer to be closed once, got %d", closeCalls) } if fileCloser != nil { t.Fatal("expected fileCloser to be cleared after shutdown") } select { case _, ok := <-ch: if ok { t.Fatal("expected subscriber channel to be closed on shutdown") } default: t.Fatal("expected subscriber channel to be closed immediately") } } func TestParseLevelDefaults(t *testing.T) { tests := []struct { input string want zerolog.Level }{ {"", zerolog.InfoLevel}, {"debug", zerolog.DebugLevel}, {"DEBUG", zerolog.DebugLevel}, {"trace", zerolog.TraceLevel}, {"info", zerolog.InfoLevel}, {"INFO", zerolog.InfoLevel}, {"warn", zerolog.WarnLevel}, {"warning", zerolog.WarnLevel}, {"WARN", zerolog.WarnLevel}, {"error", zerolog.ErrorLevel}, {"ERROR", zerolog.ErrorLevel}, {"fatal", zerolog.FatalLevel}, {"panic", zerolog.PanicLevel}, {"disabled", zerolog.Disabled}, {"unknown", zerolog.InfoLevel}, } for _, tc := range tests { got := parseLevel(tc.input) if got != tc.want { t.Errorf("parseLevel(%q) = %v, want %v", tc.input, got, tc.want) } } } func TestParseLevelWarnsOnInvalidValue(t *testing.T) { t.Cleanup(resetLoggingState) stderr := captureStderr(t, func() { if got := parseLevel(" VERBOSE "); got != zerolog.InfoLevel { t.Fatalf("expected info fallback for invalid level, got %s", got) } }) if !strings.Contains(stderr, `logging: invalid level "verbose"; using "info"`) { t.Fatalf("expected invalid level warning, got %q", stderr) } } func TestSelectWriter(t *testing.T) { tests := []struct { format string isTTY bool wantErr bool }{ {"json", false, false}, {"console", false, false}, {"auto", false, false}, } for _, tc := range tests { w := selectWriter(tc.format) if w == nil { t.Errorf("selectWriter(%q) returned nil", tc.format) } } } func TestSelectWriterAutoTerminal(t *testing.T) { t.Cleanup(resetLoggingState) isTerminalFn = func(int) bool { return true } w := selectWriter("auto") if _, ok := w.(zerolog.ConsoleWriter); !ok { t.Fatalf("expected console writer, got %#v", w) } } func TestSelectWriterDefault(t *testing.T) { t.Cleanup(resetLoggingState) w := selectWriter("unknown") if w != os.Stderr { t.Fatalf("expected default writer to be os.Stderr, got %#v", w) } } func TestSelectWriterWarnsOnInvalidFormat(t *testing.T) { t.Cleanup(resetLoggingState) stderr := captureStderr(t, func() { w := selectWriter("pretty") if w != os.Stderr { t.Fatalf("expected invalid format fallback to os.Stderr, got %#v", w) } }) if !strings.Contains(stderr, `logging: invalid format "pretty"; using "json"`) { t.Fatalf("expected invalid format warning, got %q", stderr) } } func TestIsTerminalNil(t *testing.T) { t.Cleanup(resetLoggingState) if isTerminal(nil) { t.Fatal("expected nil file to report false") } } func TestNewRollingFileWriter_EmptyPath(t *testing.T) { t.Cleanup(resetLoggingState) writer, err := newRollingFileWriter(Config{}) if err != nil { t.Fatalf("unexpected error: %v", err) } if writer != nil { t.Fatalf("expected nil writer, got %#v", writer) } } func TestNewRollingFileWriter_MkdirError(t *testing.T) { t.Cleanup(resetLoggingState) mkdirAllFn = func(string, os.FileMode) error { return errors.New("mkdir failed") } _, err := newRollingFileWriter(Config{FilePath: "/tmp/logs/test.log"}) if err == nil { t.Fatal("expected error from mkdir") } } func TestNewRollingFileWriter_HardensDirectoryPermissions(t *testing.T) { t.Cleanup(resetLoggingState) root := t.TempDir() logDir := filepath.Join(root, "logs") if err := os.MkdirAll(logDir, 0o755); err != nil { t.Fatalf("failed to create log dir: %v", err) } writer, err := newRollingFileWriter(Config{FilePath: filepath.Join(logDir, "app.log")}) if err != nil { t.Fatalf("unexpected error: %v", err) } w, ok := writer.(*rollingFileWriter) if !ok { t.Fatalf("expected rollingFileWriter, got %#v", writer) } defer func() { _ = w.closeLocked() }() info, err := os.Stat(logDir) if err != nil { t.Fatalf("failed to stat log dir: %v", err) } if got := info.Mode().Perm(); got != logDirPerm { t.Fatalf("expected log dir mode %o, got %o", logDirPerm, got) } } func TestNewRollingFileWriter_RejectsSymlinkFile(t *testing.T) { t.Cleanup(resetLoggingState) root := t.TempDir() target := filepath.Join(root, "target.log") if err := os.WriteFile(target, []byte("data"), 0o600); err != nil { t.Fatalf("failed to create target file: %v", err) } link := filepath.Join(root, "symlink.log") requireSymlinkOrSkip(t, target, link) if _, err := newRollingFileWriter(Config{FilePath: link}); err == nil { t.Fatal("expected error for symlink log file path") } } func TestNewRollingFileWriter_RejectsSymlinkDir(t *testing.T) { t.Cleanup(resetLoggingState) root := t.TempDir() realDir := filepath.Join(root, "real-logs") if err := os.MkdirAll(realDir, 0o755); err != nil { t.Fatalf("failed to create real log dir: %v", err) } linkDir := filepath.Join(root, "logs-link") requireSymlinkOrSkip(t, realDir, linkDir) if _, err := newRollingFileWriter(Config{FilePath: filepath.Join(linkDir, "app.log")}); err == nil { t.Fatal("expected error for symlink log directory path") } } func TestInitFileWriterError(t *testing.T) { t.Cleanup(resetLoggingState) mkdirAllFn = func(string, os.FileMode) error { return errors.New("mkdir failed") } Init(Config{ Format: "json", FilePath: "/tmp/logs/test.log", }) } func TestNewRollingFileWriter_DefaultMaxSize(t *testing.T) { t.Cleanup(resetLoggingState) dir := t.TempDir() writer, err := newRollingFileWriter(Config{ FilePath: filepath.Join(dir, "app.log"), MaxSizeMB: 0, }) if err != nil { t.Fatalf("unexpected error: %v", err) } w, ok := writer.(*rollingFileWriter) if !ok { t.Fatalf("expected rollingFileWriter, got %#v", writer) } if w.maxBytes != int64(defaultMaxSizeMB)*bytesPerMB { t.Fatalf("expected default max bytes, got %d", w.maxBytes) } _ = w.closeLocked() } func TestNewRollingFileWriter_SecureDirectoryPermissions(t *testing.T) { t.Cleanup(resetLoggingState) var ( called bool mode os.FileMode ) origMkdirAll := mkdirAllFn mkdirAllFn = func(path string, perm os.FileMode) error { called = true mode = perm return origMkdirAll(path, perm) } logFile := filepath.Join(t.TempDir(), "logs", "app.log") writer, err := newRollingFileWriter(Config{FilePath: logFile}) if err != nil { t.Fatalf("unexpected error: %v", err) } if !called { t.Fatal("expected mkdirAllFn to be called") } if mode != 0o700 { t.Fatalf("expected log directory mode 0700, got %#o", mode) } w, ok := writer.(*rollingFileWriter) if !ok { t.Fatalf("expected rollingFileWriter, got %#v", writer) } if w.maxBytes != int64(defaultMaxSizeMB)*bytesPerMB { t.Fatalf("expected default max bytes, got %d", w.maxBytes) } _ = w.closeLocked() } func TestNewRollingFileWriter_MaxSizeOverflowUsesDefault(t *testing.T) { t.Cleanup(resetLoggingState) dir := t.TempDir() writer, err := newRollingFileWriter(Config{ FilePath: filepath.Join(dir, "app.log"), MaxSizeMB: int(maxSafeSizeMB + 1), }) if err != nil { t.Fatalf("unexpected error: %v", err) } w, ok := writer.(*rollingFileWriter) if !ok { t.Fatalf("expected rollingFileWriter, got %#v", writer) } if w.maxBytes != int64(defaultMaxSizeMB)*bytesPerMB { t.Fatalf("expected default max bytes, got %d", w.maxBytes) } _ = w.closeLocked() } func TestNewRollingFileWriter_NegativeMaxAgeUsesDefault(t *testing.T) { t.Cleanup(resetLoggingState) dir := t.TempDir() writer, err := newRollingFileWriter(Config{ FilePath: filepath.Join(dir, "app.log"), MaxSizeMB: 1, MaxAgeDays: -1, }) if err != nil { t.Fatalf("unexpected error: %v", err) } w, ok := writer.(*rollingFileWriter) if !ok { t.Fatalf("expected rollingFileWriter, got %#v", writer) } if w.maxAge != time.Duration(defaultMaxAgeDays)*24*time.Hour { t.Fatalf("expected default max age, got %s", w.maxAge) } _ = w.closeLocked() } func TestNewRollingFileWriter_ZeroMaxAgeDisablesCleanup(t *testing.T) { t.Cleanup(resetLoggingState) dir := t.TempDir() writer, err := newRollingFileWriter(Config{ FilePath: filepath.Join(dir, "app.log"), MaxSizeMB: 1, MaxAgeDays: 0, }) if err != nil { t.Fatalf("unexpected error: %v", err) } w, ok := writer.(*rollingFileWriter) if !ok { t.Fatalf("expected rollingFileWriter, got %#v", writer) } if w.maxAge != 0 { t.Fatalf("expected max age 0, got %s", w.maxAge) } _ = w.closeLocked() } func TestNewRollingFileWriter_MaxAgeOverflowClamps(t *testing.T) { t.Cleanup(resetLoggingState) dir := t.TempDir() writer, err := newRollingFileWriter(Config{ FilePath: filepath.Join(dir, "app.log"), MaxSizeMB: 1, MaxAgeDays: maxDurationDays + 1, }) if err != nil { t.Fatalf("unexpected error: %v", err) } w, ok := writer.(*rollingFileWriter) if !ok { t.Fatalf("expected rollingFileWriter, got %#v", writer) } if w.maxAge != time.Duration(maxDurationDays)*24*time.Hour { t.Fatalf("expected clamped max age, got %s", w.maxAge) } _ = w.closeLocked() } func TestNewRollingFileWriter_OpenError(t *testing.T) { t.Cleanup(resetLoggingState) openFileFn = func(string, int, os.FileMode) (*os.File, error) { return nil, errors.New("open failed") } _, err := newRollingFileWriter(Config{FilePath: filepath.Join(t.TempDir(), "app.log")}) if err == nil { t.Fatal("expected error from openOrCreateLocked") } } func TestOpenOrCreateLocked_StatError(t *testing.T) { t.Cleanup(resetLoggingState) dir := t.TempDir() w := &rollingFileWriter{path: filepath.Join(dir, "app.log")} statFileFn = func(*os.File) (os.FileInfo, error) { return nil, errors.New("stat failed") } if err := w.openOrCreateLocked(); err != nil { t.Fatalf("unexpected error: %v", err) } if w.currentSize != 0 { t.Fatalf("expected current size 0, got %d", w.currentSize) } _ = w.closeLocked() } func TestOpenOrCreateLocked_AlreadyOpen(t *testing.T) { t.Cleanup(resetLoggingState) file, err := os.CreateTemp(t.TempDir(), "log") if err != nil { t.Fatalf("failed to create temp file: %v", err) } w := &rollingFileWriter{path: file.Name(), file: file} if err := w.openOrCreateLocked(); err != nil { t.Fatalf("unexpected error: %v", err) } _ = w.closeLocked() } func TestOpenOrCreateLocked_HardensFilePermissions(t *testing.T) { t.Cleanup(resetLoggingState) path := filepath.Join(t.TempDir(), "app.log") if err := os.WriteFile(path, []byte("existing"), 0o644); err != nil { t.Fatalf("failed to create log file: %v", err) } w := &rollingFileWriter{path: path} if err := w.openOrCreateLocked(); err != nil { t.Fatalf("openOrCreateLocked error: %v", err) } defer func() { _ = w.closeLocked() }() info, err := os.Stat(path) if err != nil { t.Fatalf("failed to stat log file: %v", err) } if got := info.Mode().Perm(); got != logFilePerm { t.Fatalf("expected log file mode %o, got %o", logFilePerm, got) } } func TestRollingFileWriter_WriteOpenError(t *testing.T) { t.Cleanup(resetLoggingState) openFileFn = func(string, int, os.FileMode) (*os.File, error) { return nil, errors.New("open failed") } w := &rollingFileWriter{path: filepath.Join(t.TempDir(), "app.log")} if _, err := w.Write([]byte("data")); err == nil { t.Fatal("expected write error") } } func TestRollingFileWriter_WriteRotateError(t *testing.T) { t.Cleanup(resetLoggingState) dir := t.TempDir() path := filepath.Join(dir, "app.log") callCount := 0 openFileFn = func(name string, flag int, perm os.FileMode) (*os.File, error) { callCount++ if callCount == 1 { return os.OpenFile(name, flag, perm) } return nil, errors.New("open failed") } w := &rollingFileWriter{path: path, maxBytes: 1} if _, err := w.Write([]byte("too big")); err == nil { t.Fatal("expected rotate error") } } func TestRollingFileWriter_RotateCompress(t *testing.T) { t.Cleanup(resetLoggingState) dir := t.TempDir() path := filepath.Join(dir, "app.log") if err := os.WriteFile(path, []byte("data"), 0600); err != nil { t.Fatalf("failed to write file: %v", err) } w := &rollingFileWriter{path: path, maxBytes: 1, compress: true} if err := w.openOrCreateLocked(); err != nil { t.Fatalf("openOrCreateLocked error: %v", err) } ch := make(chan string, 1) compressFn = func(p string) { ch <- p } if err := w.rotateLocked(); err != nil { t.Fatalf("rotateLocked error: %v", err) } select { case <-ch: case <-time.After(2 * time.Second): t.Fatal("expected compress to be triggered") } _ = w.closeLocked() } func TestRollingFileWriter_RotateRenameError(t *testing.T) { t.Cleanup(resetLoggingState) dir := t.TempDir() path := filepath.Join(dir, "app.log") if err := os.WriteFile(path, []byte("data"), 0600); err != nil { t.Fatalf("failed to write file: %v", err) } w := &rollingFileWriter{path: path, maxBytes: 1, compress: true} if err := w.openOrCreateLocked(); err != nil { t.Fatalf("openOrCreateLocked error: %v", err) } renameCalled := false renameFn = func(oldpath, newpath string) error { renameCalled = true return errors.New("rename failed") } compressCalled := make(chan struct{}, 1) compressFn = func(string) { compressCalled <- struct{}{} } if err := w.rotateLocked(); err != nil { t.Fatalf("rotateLocked error: %v", err) } if !renameCalled { t.Fatal("expected rename to be attempted") } select { case <-compressCalled: t.Fatal("expected compression to be skipped on rename error") default: } if _, err := os.Stat(path); err != nil { t.Fatalf("expected log file to exist after rename failure: %v", err) } _ = w.closeLocked() } func TestRotateLockedCloseError(t *testing.T) { t.Cleanup(resetLoggingState) dir := t.TempDir() path := filepath.Join(dir, "app.log") file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0600) if err != nil { t.Fatalf("failed to open file: %v", err) } w := &rollingFileWriter{path: path, file: file} closeFileFn = func(*os.File) error { return errors.New("close failed") } if err := w.rotateLocked(); err == nil { t.Fatal("expected close error") } _ = file.Close() } func TestCloseLocked(t *testing.T) { t.Cleanup(resetLoggingState) w := &rollingFileWriter{} if err := w.closeLocked(); err != nil { t.Fatalf("unexpected error: %v", err) } file, err := os.CreateTemp(t.TempDir(), "log") if err != nil { t.Fatalf("failed to create temp file: %v", err) } w.file = file if err := w.closeLocked(); err != nil { t.Fatalf("unexpected close error: %v", err) } if w.file != nil { t.Fatal("expected file to be cleared") } if w.currentSize != 0 { t.Fatalf("expected size reset, got %d", w.currentSize) } } func TestCleanupOldFilesNoMaxAge(t *testing.T) { t.Cleanup(resetLoggingState) w := &rollingFileWriter{path: filepath.Join(t.TempDir(), "app.log"), maxAge: 0} w.cleanupOldFiles() } func TestCleanupOldFilesReadDirError(t *testing.T) { t.Cleanup(resetLoggingState) readDirFn = func(string) ([]os.DirEntry, error) { return nil, errors.New("read dir failed") } w := &rollingFileWriter{path: filepath.Join(t.TempDir(), "app.log"), maxAge: time.Hour} w.cleanupOldFiles() } func TestCleanupOldFilesInfoError(t *testing.T) { t.Cleanup(resetLoggingState) readDirFn = func(string) ([]os.DirEntry, error) { return []os.DirEntry{errDirEntry{name: "app.log.20200101"}}, nil } w := &rollingFileWriter{path: filepath.Join(t.TempDir(), "app.log"), maxAge: time.Hour} w.cleanupOldFiles() } func TestCleanupOldFilesRemovesOld(t *testing.T) { t.Cleanup(resetLoggingState) dir := t.TempDir() path := filepath.Join(dir, "app.log") oldFile := filepath.Join(dir, "app.log.20200101-000000") newFile := filepath.Join(dir, "app.log.20250101-000000") otherFile := filepath.Join(dir, "other.log.20200101") if err := os.WriteFile(oldFile, []byte("old"), 0600); err != nil { t.Fatalf("failed to write old file: %v", err) } if err := os.WriteFile(newFile, []byte("new"), 0600); err != nil { t.Fatalf("failed to write new file: %v", err) } if err := os.WriteFile(otherFile, []byte("other"), 0600); err != nil { t.Fatalf("failed to write other file: %v", err) } fixedNow := time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC) nowFn = func() time.Time { return fixedNow } if err := os.Chtimes(oldFile, fixedNow.Add(-48*time.Hour), fixedNow.Add(-48*time.Hour)); err != nil { t.Fatalf("failed to set old file time: %v", err) } if err := os.Chtimes(newFile, fixedNow.Add(-time.Hour), fixedNow.Add(-time.Hour)); err != nil { t.Fatalf("failed to set new file time: %v", err) } w := &rollingFileWriter{path: path, maxAge: 24 * time.Hour} w.cleanupOldFiles() if _, err := os.Stat(oldFile); !os.IsNotExist(err) { t.Fatalf("expected old file to be removed") } if _, err := os.Stat(newFile); err != nil { t.Fatalf("expected new file to remain: %v", err) } if _, err := os.Stat(otherFile); err != nil { t.Fatalf("expected other file to remain: %v", err) } } func TestStatFileFnDefault(t *testing.T) { t.Cleanup(resetLoggingState) file, err := os.CreateTemp(t.TempDir(), "log") if err != nil { t.Fatalf("failed to create temp file: %v", err) } t.Cleanup(func() { _ = file.Close() }) if _, err := statFileFn(file); err != nil { t.Fatalf("statFileFn error: %v", err) } } func TestCompressAndRemove(t *testing.T) { t.Run("OpenError", func(t *testing.T) { t.Cleanup(resetLoggingState) openFn = func(string) (*os.File, error) { return nil, errors.New("open failed") } compressAndRemove("/does/not/exist") }) t.Run("OpenFileError", func(t *testing.T) { t.Cleanup(resetLoggingState) dir := t.TempDir() path := filepath.Join(dir, "app.log") if err := os.WriteFile(path, []byte("data"), 0600); err != nil { t.Fatalf("failed to write file: %v", err) } openFileFn = func(string, int, os.FileMode) (*os.File, error) { return nil, errors.New("open file failed") } compressAndRemove(path) }) t.Run("CopyError", func(t *testing.T) { t.Cleanup(resetLoggingState) dir := t.TempDir() path := filepath.Join(dir, "app.log") if err := os.WriteFile(path, []byte("data"), 0600); err != nil { t.Fatalf("failed to write file: %v", err) } copyFn = func(io.Writer, io.Reader) (int64, error) { return 0, errors.New("copy failed") } compressAndRemove(path) }) t.Run("CloseError", func(t *testing.T) { t.Cleanup(resetLoggingState) dir := t.TempDir() path := filepath.Join(dir, "app.log") if err := os.WriteFile(path, []byte("data"), 0600); err != nil { t.Fatalf("failed to write file: %v", err) } errWriter := errWriteCloser{err: errors.New("write failed")} gzipNewWriterFn = func(io.Writer) *gzip.Writer { return gzip.NewWriter(errWriter) } copyFn = func(io.Writer, io.Reader) (int64, error) { return 0, nil } compressAndRemove(path) }) t.Run("Success", func(t *testing.T) { t.Cleanup(resetLoggingState) dir := t.TempDir() path := filepath.Join(dir, "app.log") if err := os.WriteFile(path, []byte("data"), 0600); err != nil { t.Fatalf("failed to write file: %v", err) } compressAndRemove(path) if _, err := os.Stat(path); !os.IsNotExist(err) { t.Fatal("expected original file to be removed") } if _, err := os.Stat(path + ".gz"); err != nil { t.Fatalf("expected gzip file to exist: %v", err) } }) t.Run("RejectsSymlinkSource", func(t *testing.T) { t.Cleanup(resetLoggingState) dir := t.TempDir() target := filepath.Join(dir, "target.log") if err := os.WriteFile(target, []byte("data"), 0o600); err != nil { t.Fatalf("failed to write target file: %v", err) } symlinkPath := filepath.Join(dir, "app.log") requireSymlinkOrSkip(t, target, symlinkPath) compressAndRemove(symlinkPath) if _, err := os.Stat(symlinkPath + ".gz"); err == nil { t.Fatal("expected no gzip output for symlink source path") } }) t.Run("RejectsSymlinkDestination", func(t *testing.T) { t.Cleanup(resetLoggingState) dir := t.TempDir() path := filepath.Join(dir, "app.log") if err := os.WriteFile(path, []byte("data"), 0o600); err != nil { t.Fatalf("failed to write source file: %v", err) } target := filepath.Join(dir, "target.gz") if err := os.WriteFile(target, []byte("target"), 0o600); err != nil { t.Fatalf("failed to write destination target file: %v", err) } requireSymlinkOrSkip(t, target, path+".gz") compressAndRemove(path) if _, err := os.Stat(path); err != nil { t.Fatalf("expected source file to remain when destination path is unsafe: %v", err) } if _, err := os.Stat(path + ".gz"); err != nil { t.Fatalf("expected symlink destination path to remain: %v", err) } }) } // Test that the logging package doesn't panic under concurrent use func TestConcurrentLogging(t *testing.T) { t.Cleanup(resetLoggingState) buf := &lockedBuffer{} mu.Lock() baseWriter = buf baseLogger = zerolog.New(buf).With().Timestamp().Logger() log.Logger = baseLogger mu.Unlock() var wg sync.WaitGroup for i := 0; i < 100; i++ { wg.Add(1) go func(n int) { defer wg.Done() log.Info().Int("iteration", n).Msg("concurrent log") }(i) } wg.Wait() if buf.Len() == 0 { t.Fatal("expected log output from concurrent logging") } } type lockedBuffer struct { mu sync.Mutex buf bytes.Buffer } func (b *lockedBuffer) Write(p []byte) (int, error) { b.mu.Lock() defer b.mu.Unlock() return b.buf.Write(p) } func (b *lockedBuffer) Len() int { b.mu.Lock() defer b.mu.Unlock() return b.buf.Len() } type errDirEntry struct { name string } func (e errDirEntry) Name() string { return e.name } func (e errDirEntry) IsDir() bool { return false } func (e errDirEntry) Type() os.FileMode { return 0 } func (e errDirEntry) Info() (os.FileInfo, error) { return nil, errors.New("info error") } type errWriteCloser struct { err error } func (e errWriteCloser) Write(p []byte) (int, error) { return 0, e.err } func requireSymlinkOrSkip(t *testing.T, target, link string) { t.Helper() if err := os.Symlink(target, link); err != nil { t.Skipf("symlink not supported in this environment: %v", err) } }