Files
counters/internal/config/logger.go
T

117 lines
3.1 KiB
Go

package config
import (
"fmt"
"log/slog"
"os"
"strings"
"sync"
)
// LogWriter wraps a log destination and supports reopening for log rotation.
// When the output is a file, Reopen closes the old handle and opens a new one
// so that newsyslog rotation works after SIGHUP. Shared by the application log
// and the counting transaction log. (Same implementation as rcptd.)
type LogWriter struct {
mu sync.Mutex
path string // empty for stdout/stderr
file *os.File // current output file (or os.Stdout/os.Stderr)
}
func (w *LogWriter) Write(p []byte) (n int, err error) {
w.mu.Lock()
defer w.mu.Unlock()
return w.file.Write(p)
}
// Reopen closes and reopens the log file. No-op for stdout/stderr.
func (w *LogWriter) Reopen() error {
if w.path == "" {
return nil
}
w.mu.Lock()
defer w.mu.Unlock()
newFile, err := os.OpenFile(w.path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
if err != nil {
return fmt.Errorf("reopen log file %s: %w", w.path, err)
}
w.file.Close()
w.file = newFile
return nil
}
// Close closes the log file. No-op for stdout/stderr.
func (w *LogWriter) Close() error {
if w.path == "" {
return nil
}
w.mu.Lock()
defer w.mu.Unlock()
return w.file.Close()
}
// NewWriter opens a log destination: "stderr", "stdout" or a file path.
func NewWriter(output string) (*LogWriter, error) { return newLogWriter(output) }
// NewLogger creates the application slog.Logger from cfg and installs it as
// the slog default. The returned LogWriter must be closed by the caller.
func NewLogger(cfg LoggingConfig) (*slog.Logger, *LogWriter, error) {
level, err := parseLogLevel(cfg.Level)
if err != nil {
return nil, nil, fmt.Errorf("invalid log level: %w", err)
}
writer, err := newLogWriter(cfg.Output)
if err != nil {
return nil, nil, fmt.Errorf("failed to create log writer: %w", err)
}
var handler slog.Handler
opts := &slog.HandlerOptions{Level: level}
switch strings.ToLower(cfg.Format) {
case "json":
handler = slog.NewJSONHandler(writer, opts)
case "console", "":
handler = slog.NewTextHandler(writer, opts)
default:
writer.Close()
return nil, nil, fmt.Errorf("invalid log format: %s (must be 'console' or 'json')", cfg.Format)
}
logger := slog.New(handler)
slog.SetDefault(logger)
return logger, writer, nil
}
func parseLogLevel(level string) (slog.Level, error) {
switch strings.ToLower(level) {
case "debug":
return slog.LevelDebug, nil
case "info", "":
return slog.LevelInfo, nil
case "warn", "warning":
return slog.LevelWarn, nil
case "error":
return slog.LevelError, nil
default:
return slog.LevelInfo, fmt.Errorf("unknown level: %s (must be debug, info, warn, or error)", level)
}
}
func newLogWriter(output string) (*LogWriter, error) {
switch strings.ToLower(output) {
case "stderr", "":
return &LogWriter{file: os.Stderr}, nil
case "stdout":
return &LogWriter{file: os.Stdout}, nil
default:
file, err := os.OpenFile(output, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
if err != nil {
return nil, fmt.Errorf("failed to open log file %s: %w", output, err)
}
return &LogWriter{path: output, file: file}, nil
}
}