227 lines
5.3 KiB
Go
227 lines
5.3 KiB
Go
// Package log implements logging facility as golang slog module wrapper.
|
|
package log
|
|
|
|
import (
|
|
"context"
|
|
"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
|
|
)
|
|
|
|
// slogWriter implements [io.Writer] to redirect standard log to slog.
|
|
type (
|
|
slogWriter struct {
|
|
handler slog.Handler
|
|
}
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
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 to debug level.
|
|
// This is useful for assigning to http.Server.ErrorLog to move TLS handshake
|
|
// and other client-side errors to debug level.
|
|
func DebugLogger() *log.Logger {
|
|
mu.RLock()
|
|
defer mu.RUnlock()
|
|
|
|
opts := &slog.HandlerOptions{
|
|
Level: slog.LevelDebug,
|
|
}
|
|
|
|
handler := slog.NewTextHandler(Log, opts)
|
|
|
|
return log.New(&slogWriter{handler: handler}, "", 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")
|
|
|
|
level := slog.LevelWarn
|
|
if isTLSError {
|
|
level = slog.LevelDebug
|
|
}
|
|
|
|
record := slog.NewRecord(time.Now(), level, msg, 0)
|
|
|
|
_ = w.handler.Handle(context.TODO(), record)
|
|
|
|
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: */
|