Files
at-container-registry/pkg/logging/logger.go
T

164 lines
3.8 KiB
Go

// Package logging provides centralized structured logging using slog
// with configurable log levels. Call InitLogger() from main() to configure.
//
// Dynamic debug logging:
// Send SIGUSR1 to toggle debug mode at runtime (auto-reverts after 30 minutes).
// Example: docker kill -s SIGUSR1 <container>
package logging
import (
"io"
"log/slog"
"os"
"os/signal"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
)
const debugTimeout = 30 * time.Minute
var (
levelVar *slog.LevelVar
originalLevel slog.Level
debugEnabled atomic.Bool
revertTimer *time.Timer
revertMu sync.Mutex
)
// InitLogger initializes the global slog default logger with the specified log level.
// Valid levels: debug, info, warn, error (case-insensitive)
// If level is empty or invalid, defaults to INFO.
// Call this from main() at startup.
//
// Also starts a signal handler for SIGUSR1 to toggle debug mode at runtime.
func InitLogger(level string) {
var logLevel slog.Level
switch strings.ToLower(strings.TrimSpace(level)) {
case "debug":
logLevel = slog.LevelDebug
case "info", "":
logLevel = slog.LevelInfo
case "warn", "warning":
logLevel = slog.LevelWarn
case "error":
logLevel = slog.LevelError
default:
logLevel = slog.LevelInfo
}
// Store original level for toggle-back and use LevelVar for dynamic changes
originalLevel = logLevel
levelVar = new(slog.LevelVar)
levelVar.Set(logLevel)
opts := &slog.HandlerOptions{
Level: levelVar,
}
handler := slog.NewTextHandler(os.Stdout, opts)
slog.SetDefault(slog.New(handler))
// Start signal handler for dynamic debug toggle
go handleDebugSignal()
}
func handleDebugSignal() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGUSR1)
for range sigChan {
ToggleDebug()
}
}
// ToggleDebug toggles between the original log level and DEBUG.
// When enabling debug, starts a 30-minute timer that auto-reverts.
// Typically called via SIGUSR1 signal.
func ToggleDebug() {
revertMu.Lock()
defer revertMu.Unlock()
wasDebug := debugEnabled.Swap(!debugEnabled.Load())
// Cancel any existing revert timer
if revertTimer != nil {
revertTimer.Stop()
revertTimer = nil
}
if wasDebug {
// Turning debug OFF
levelVar.Set(originalLevel)
slog.Info("Log level changed",
"from", "DEBUG",
"to", levelToString(originalLevel),
"trigger", "SIGUSR1")
} else {
// Turning debug ON - start auto-revert timer
levelVar.Set(slog.LevelDebug)
revertTimer = time.AfterFunc(debugTimeout, autoRevert)
slog.Info("Log level changed",
"from", levelToString(originalLevel),
"to", "DEBUG",
"trigger", "SIGUSR1",
"auto_revert_in", debugTimeout)
}
}
func autoRevert() {
revertMu.Lock()
defer revertMu.Unlock()
if !debugEnabled.Load() {
return // Already reverted manually
}
debugEnabled.Store(false)
levelVar.Set(originalLevel)
revertTimer = nil
slog.Info("Log level changed",
"from", "DEBUG",
"to", levelToString(originalLevel),
"trigger", "auto-revert")
}
func levelToString(l slog.Level) string {
switch l {
case slog.LevelDebug:
return "DEBUG"
case slog.LevelInfo:
return "INFO"
case slog.LevelWarn:
return "WARN"
case slog.LevelError:
return "ERROR"
default:
return l.String()
}
}
// SetupTestLogger configures logging for tests to reduce noise.
// Sets log level to WARN and outputs to io.Discard to suppress DEBUG and INFO messages.
// Returns a cleanup function that should be called when the test completes (use t.Cleanup).
func SetupTestLogger() func() {
// Save original logger to restore later
originalLogger := slog.Default()
// Set level to WARN and discard output to silence tests
opts := &slog.HandlerOptions{
Level: slog.LevelWarn,
}
handler := slog.NewTextHandler(io.Discard, opts)
slog.SetDefault(slog.New(handler))
// Return cleanup function
return func() {
slog.SetDefault(originalLogger)
}
}