fix(logging): make file logger close terminal

This commit is contained in:
修雨
2026-07-15 17:16:53 +08:00
parent bf105d3d29
commit ec29dc0e22
4 changed files with 72 additions and 11 deletions
+7 -9
View File
@@ -748,20 +748,18 @@ func configureLogLevel(flags *GlobalFlags) {
// Initialize file logger — writes to ~/.dws/logs/dws.log at DEBUG level
// regardless of stderr level. All slog calls are captured for diagnostics.
logger := replaceFileLogger(defaultConfigDir())
logger := logging.Setup(defaultConfigDir())
fileHandler := slog.NewJSONHandler(logger.Writer(), &slog.HandlerOptions{Level: slog.LevelDebug})
defaultLogger := slog.New(logging.NewMultiHandler(stderrHandler, fileHandler))
slog.SetDefault(slog.New(logging.NewMultiHandler(stderrHandler, fileHandler)))
}
func replaceFileLogger(configDir string) *logging.FileLogger {
fileLoggerMu.Lock()
defer fileLoggerMu.Unlock()
if fileLogger != nil {
_ = fileLogger.Close()
previous := fileLogger
fileLogger = logger
slog.SetDefault(defaultLogger)
if previous != nil {
_ = previous.Close()
}
fileLogger = logging.Setup(configDir)
return fileLogger
}
// FileLoggerInstance returns the package-level file logger, or nil if not initialized.
+23 -2
View File
@@ -15,6 +15,7 @@ package app
import (
"os"
"path/filepath"
"runtime"
"testing"
)
@@ -35,10 +36,30 @@ func TestConfigureLogLevelReleasesPreviousFileLogger(t *testing.T) {
t.Setenv("DWS_CONFIG_DIR", firstConfig)
configureLogLevel(&GlobalFlags{})
firstLogger := FileLoggerInstance()
t.Setenv("DWS_CONFIG_DIR", secondConfig)
configureLogLevel(&GlobalFlags{})
if err := os.RemoveAll(firstConfig); err != nil {
t.Fatalf("remove previous logger directory: %v", err)
firstLogPath := filepath.Join(firstConfig, "logs", "dws.log")
if err := os.Remove(firstLogPath); err != nil {
t.Fatalf("remove previous logger file: %v", err)
}
firstLogger.Info("late write to replaced logger")
if _, err := os.Stat(firstLogPath); !os.IsNotExist(err) {
t.Fatalf("replaced logger recreated its log file: %v", err)
}
previousSameDirLogger := FileLoggerInstance()
configureLogLevel(&GlobalFlags{})
currentLogger := FileLoggerInstance()
CloseFileLogger()
secondLogPath := filepath.Join(secondConfig, "logs", "dws.log")
if err := os.Remove(secondLogPath); err != nil {
t.Fatalf("remove current logger file: %v", err)
}
previousSameDirLogger.Info("late write to same-directory replaced logger")
currentLogger.Info("late write to closed current logger")
if _, err := os.Stat(secondLogPath); !os.IsNotExist(err) {
t.Fatalf("closed logger recreated same-directory log file: %v", err)
}
}
+14
View File
@@ -120,6 +120,7 @@ type rotatingWriter struct {
mu sync.Mutex
file *os.File
written int64
closed bool
}
func newRotatingWriter(path string, maxSize int64, maxBackups int) *rotatingWriter {
@@ -133,6 +134,9 @@ func newRotatingWriter(path string, maxSize int64, maxBackups int) *rotatingWrit
func (w *rotatingWriter) open() error {
w.mu.Lock()
defer w.mu.Unlock()
if w.closed {
return os.ErrClosed
}
f, err := os.OpenFile(w.path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, config.FilePerm)
if err != nil {
@@ -151,6 +155,9 @@ func (w *rotatingWriter) open() error {
func (w *rotatingWriter) Write(p []byte) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
if w.closed {
return 0, os.ErrClosed
}
// Attempt to recover if file was lost (e.g. rotation failed due to full disk).
if w.file == nil {
@@ -175,6 +182,9 @@ func (w *rotatingWriter) Write(p []byte) (int, error) {
// reopenLocked tries to reopen the log file. Caller must hold w.mu.
func (w *rotatingWriter) reopenLocked() error {
if w.closed {
return os.ErrClosed
}
f, err := os.OpenFile(w.path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, config.FilePerm)
if err != nil {
return err
@@ -197,6 +207,10 @@ func (w *rotatingWriter) close() error {
w.mu.Lock()
defer w.mu.Unlock()
if w.closed {
return nil
}
w.closed = true
if w.file == nil {
return nil
}
+28
View File
@@ -14,6 +14,7 @@
package logging
import (
"errors"
"os"
"path/filepath"
"strings"
@@ -121,3 +122,30 @@ func TestCloseOnNilLogger(t *testing.T) {
t.Errorf("Close on nil should not error: %v", err)
}
}
func TestFileLoggerCloseIsTerminal(t *testing.T) {
t.Parallel()
dir := t.TempDir()
fl := Setup(dir)
logPath := filepath.Join(dir, logSubDir, logFileName)
fl.Logger.Info("before close")
if err := fl.Close(); err != nil {
t.Fatalf("close logger: %v", err)
}
if err := os.Remove(logPath); err != nil {
t.Fatalf("remove closed log file: %v", err)
}
n, err := fl.Writer().Write([]byte("late write"))
if n != 0 || !errors.Is(err, os.ErrClosed) {
t.Fatalf("write after close = (%d, %v), want (0, os.ErrClosed)", n, err)
}
fl.Logger.Info("late structured write")
if _, err := os.Stat(logPath); !os.IsNotExist(err) {
t.Fatalf("closed logger recreated log file: %v", err)
}
if err := fl.Close(); err != nil {
t.Fatalf("second close: %v", err)
}
}