Files
backer/internal/log/main.go

228 lines
5.3 KiB
Go

// Package log implements logging facility as golang slog module wrapper.
package log
import (
"fmt"
"log"
"log/slog"
"os"
"strings"
"sync"
"time"
)
var (
// 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
// 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
// logLevel stores the configured log level for use by DebugLogger.
logLevel slog.Level
)
type (
// slogWriter implements [io.Writer] to redirect standard log to slog.
slogWriter struct{}
)
// Init sets up logger stuff.
// 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 {
mu.Lock()
defer mu.Unlock()
var (
err error
loglevel slog.Level
)
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)
if err != nil {
return fmt.Errorf("Unable to open log file: %w", err)
}
} else {
Log = os.Stderr
}
// no panic, no trace.
switch level {
case "error":
loglevel = slog.LevelError
case "warn":
loglevel = slog.LevelWarn
case "info":
loglevel = slog.LevelInfo
case "debug":
loglevel = slog.LevelDebug
default:
loglevel = slog.LevelInfo
}
logLevel = loglevel
opts := &slog.HandlerOptions{
// Use the ReplaceAttr function on the handler options
// to be able to replace any single attribute in the log output.
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { //nolint: revive
// Check that we are handling the time key.
if a.Key != slog.TimeKey {
return a
}
t := a.Value.Time()
// Change the value from a time.Time to a String
// where the string has the correct time format.
a.Value = slog.StringValue(t.Format(time.DateTime))
return a
},
Level: loglevel,
}
slog.SetDefault(
slog.New(
slog.NewTextHandler(
Log,
opts,
),
),
)
return err
}
// Error logs args at error level using [slog.Error()].
func Error(args ...any) {
slog.Error(fmt.Sprint(args...))
}
// Errorf logs message on error log level and allows to supply arguments in printf() style.
func Errorf(format string, a ...any) {
slog.Error(fmt.Sprintf(format, a...))
}
// Warn logs args at warn level using [slog.Warn()].
func Warn(args ...any) {
slog.Warn(fmt.Sprint(args...))
}
// Warnf logs message on warn log level and allows to supply arguments in printf() style.
func Warnf(format string, a ...any) {
slog.Warn(fmt.Sprintf(format, a...))
}
// Info logs args at info level using [slog.Info()].
func Info(args ...any) {
slog.Info(fmt.Sprint(args...))
}
// Infof logs message on info log level and allows to supply arguments in printf() style.
func Infof(format string, a ...any) {
slog.Info(fmt.Sprintf(format, a...))
}
// Debug logs args at debug level using [slog.Debug()].
func Debug(args ...any) {
slog.Debug(fmt.Sprint(args...))
}
// Debugf logs message on debug log level and allows to supply arguments in printf() style.
func Debugf(format string, a ...any) {
slog.Debug(fmt.Sprintf(format, a...))
}
// DebugLogger returns a standard library logger that writes at debug/warn level.
// TLS-related errors are logged at debug level, others at warn level.
// Both are filtered by the configured log level: set loglevel to debug to see all messages.
func DebugLogger() *log.Logger {
mu.RLock()
defer mu.RUnlock()
opts := &slog.HandlerOptions{
Level: logLevel,
}
handler := slog.NewTextHandler(Log, opts)
_ = handler // satisfy linter (handler configures level for potential future use).
return log.New(&slogWriter{}, "", 0)
}
// Write implements [io.Writer]. It differentiates TLS/SSL errors from other errors.
// TLS-related errors are logged at debug level, while other errors (handler issues,
// unexpected behavior) are logged at warn level.
func (w *slogWriter) Write(p []byte) (int, error) {
msg := string(p)
isTLSError := strings.Contains(msg, "tls") ||
strings.Contains(msg, "ssl") ||
strings.Contains(msg, "TLS") ||
strings.Contains(msg, "SSL") ||
strings.Contains(msg, "handshake") ||
strings.Contains(msg, "certificate")
if isTLSError {
Debug(msg)
} else {
Warn(msg)
}
return len(p), nil
}
// Fatal logs args at error level and quits with status code 1.
func Fatal(args ...any) {
slog.Error(fmt.Sprint(args...))
Close()
os.Exit(1)
}
// Fatalf logs message on error log level and allows to supply arguments in printf() style. After that it quits with
// status code 1.
func Fatalf(format string, a ...any) {
slog.Error(fmt.Sprintf(format, a...))
Close()
os.Exit(1)
}
// Close closes log file descriptor.
// Thread-safe: uses mutex to coordinate with Init and DebugLogger.
func Close() {
mu.Lock()
defer mu.Unlock()
if Log == nil || Log == os.Stderr {
return
}
if err := Log.Close(); err != nil {
msg := fmt.Sprintln("Failed to close log file:", err)
slog.Error(msg)
}
Log = nil
}
/* vim: setlocal ft=go noet ai ts=4 sw=4 sts=4: */