Make logging thread safe, useful to run tests in parallel

This commit is contained in:
eleksir
2026-04-04 15:32:42 +03:00
parent c1444f8c1d
commit 5e7dd4b960

View File

@@ -8,12 +8,20 @@ import (
"log/slog" "log/slog"
"os" "os"
"strings" "strings"
"sync"
"time" "time"
) )
var ( var (
// Log contains *[os.File] pointing to log file descriptor. // Log contains *[os.File] pointing to log file descriptor.
// Protected by mu for thread-safe access, primarily to enable parallel tests.
// In production, this is set once at startup and never modified thereafter.
Log *os.File Log *os.File
// mu protects Log during initialization and when DebugLogger is called.
// This is necessary to allow parallel test execution where each test
// may initialize the logger with a different log file.
mu sync.RWMutex
) )
// slogWriter implements [io.Writer] to redirect standard log to slog. // slogWriter implements [io.Writer] to redirect standard log to slog.
@@ -25,13 +33,22 @@ type (
// Init sets up logger stuff. // Init sets up logger stuff.
// level can be error, warn, info, debug; if something other is supplied, info level is selected. // level can be error, warn, info, debug; if something other is supplied, info level is selected.
// Thread-safe: uses mutex to allow parallel test execution.
func Init(level, filename string) error { func Init(level, filename string) error {
mu.Lock()
defer mu.Unlock()
var ( var (
err error err error
loglevel slog.Level loglevel slog.Level
) )
if filename != "" { if filename != "" {
// Close existing log file if re-initializing (e.g., in tests).
if Log != nil && Log != os.Stderr {
Log.Close()
}
Log, err = os.OpenFile(filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644) Log, err = os.OpenFile(filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
if err != nil { if err != nil {
@@ -136,6 +153,9 @@ func Debugf(format string, a ...any) {
// This is useful for assigning to http.Server.ErrorLog to move TLS handshake // This is useful for assigning to http.Server.ErrorLog to move TLS handshake
// and other client-side errors to debug level. // and other client-side errors to debug level.
func DebugLogger() *log.Logger { func DebugLogger() *log.Logger {
mu.RLock()
defer mu.RUnlock()
opts := &slog.HandlerOptions{ opts := &slog.HandlerOptions{
Level: slog.LevelDebug, Level: slog.LevelDebug,
} }
@@ -186,7 +206,11 @@ func Fatalf(format string, a ...any) {
} }
// Close closes log file descriptor. // Close closes log file descriptor.
// Thread-safe: uses mutex to coordinate with Init and DebugLogger.
func Close() { func Close() {
mu.Lock()
defer mu.Unlock()
if Log == nil || Log == os.Stderr { if Log == nil || Log == os.Stderr {
return return
} }
@@ -195,6 +219,8 @@ func Close() {
msg := fmt.Sprintln("Failed to close log file:", err) msg := fmt.Sprintln("Failed to close log file:", err)
slog.Error(msg) slog.Error(msg)
} }
Log = nil
} }
/* vim: setlocal ft=go noet ai ts=4 sw=4 sts=4: */ /* vim: setlocal ft=go noet ai ts=4 sw=4 sts=4: */