Files
backer/internal/log/main.go

201 lines
4.5 KiB
Go

// Package log implements logging facility as golang slog module wrapper.
package log
import (
"context"
"fmt"
"log"
"log/slog"
"os"
"strings"
"time"
)
var (
// Log contains *[os.File] pointing to log file descriptor.
Log *os.File
)
// 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.
func Init(level, filename string) error {
var (
err error
loglevel slog.Level
)
if filename != "" {
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 {
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.
func Close() {
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)
}
}
/* vim: setlocal ft=go noet ai ts=4 sw=4 sts=4: */