mirror of
https://github.com/axllent/mailpit.git
synced 2026-06-27 22:46:09 +00:00
Chore: Remove logrus dependency and implement slog-based logging
This commit is contained in:
1
go.mod
1
go.mod
@@ -19,7 +19,6 @@ require (
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/rqlite/gorqlite v0.0.0-20250609141355-ac86a4a1c9a8
|
||||
github.com/sirupsen/logrus v1.9.4
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/tg123/go-htpasswd v1.2.4
|
||||
|
||||
2
go.sum
2
go.sum
@@ -117,8 +117,6 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
||||
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
|
||||
@@ -1,73 +1,177 @@
|
||||
// Package logger handles the logging
|
||||
// Mailpit now uses slog for logging, but this package provides a logrus-compatible API and formatting to avoid changing all existing log calls
|
||||
// and provide backwards compatibility with logrus formatting and features like log levels and file output.
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Logger wraps slog.Logger providing a logrus-compatible API
|
||||
type Logger struct {
|
||||
sl *slog.Logger
|
||||
}
|
||||
|
||||
var (
|
||||
log *logrus.Logger
|
||||
log *Logger
|
||||
// VerboseLogging for verbose logging
|
||||
VerboseLogging bool
|
||||
// QuietLogging shows only errors
|
||||
QuietLogging bool
|
||||
// NoLogging shows only fatal errors
|
||||
// NoLogging disables all logging (tests)
|
||||
NoLogging bool
|
||||
// LogFile sets a log file
|
||||
LogFile string
|
||||
)
|
||||
|
||||
// Log returns the logger instance
|
||||
func Log() *logrus.Logger {
|
||||
// Log returns the logger instance, initialising it on first call. The level and
|
||||
// output destination are determined once from VerboseLogging, QuietLogging,
|
||||
// NoLogging, and LogFile at the time of first use.
|
||||
func Log() *Logger {
|
||||
if log == nil {
|
||||
log = logrus.New()
|
||||
log.SetLevel(logrus.InfoLevel)
|
||||
if VerboseLogging {
|
||||
// verbose logging (debug)
|
||||
log.SetLevel(logrus.DebugLevel)
|
||||
} else if QuietLogging {
|
||||
// show errors only
|
||||
log.SetLevel(logrus.ErrorLevel)
|
||||
} else if NoLogging {
|
||||
// disable all logging (tests)
|
||||
log.SetLevel(logrus.PanicLevel)
|
||||
level := slog.LevelInfo
|
||||
switch {
|
||||
case VerboseLogging:
|
||||
level = slog.LevelDebug
|
||||
case QuietLogging:
|
||||
level = slog.LevelError
|
||||
case NoLogging:
|
||||
level = slog.Level(100) // above all real levels — silences all output
|
||||
}
|
||||
|
||||
out := os.Stdout
|
||||
if LogFile != "" {
|
||||
file, err := os.OpenFile(filepath.Clean(LogFile), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0664) // #nosec
|
||||
if err == nil {
|
||||
log.Out = file
|
||||
out = file
|
||||
} else {
|
||||
log.Out = os.Stdout
|
||||
log.Warn("Failed to log to file, using default stderr")
|
||||
fmt.Fprintln(os.Stderr, "failed to log to file, using default stdout")
|
||||
}
|
||||
} else {
|
||||
log.Out = os.Stdout
|
||||
}
|
||||
|
||||
log.SetFormatter(&logrus.TextFormatter{
|
||||
FullTimestamp: true,
|
||||
TimestampFormat: "2006/01/02 15:04:05",
|
||||
})
|
||||
log = &Logger{
|
||||
sl: slog.New(&logrusHandler{
|
||||
out: out,
|
||||
level: level,
|
||||
color: isTerminal(out),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
return log
|
||||
}
|
||||
|
||||
// PrettyPrint for debugging
|
||||
// logrusHandler is a slog.Handler that formats output to match logrus TextFormatter.
|
||||
// TTY output: INFO[2006/01/02 15:04:05] message
|
||||
// File output: time="2006/01/02 15:04:05" level=info msg="message"
|
||||
type logrusHandler struct {
|
||||
mu sync.Mutex
|
||||
out *os.File
|
||||
level slog.Level
|
||||
color bool
|
||||
}
|
||||
|
||||
// Enabled reports whether the handler will emit a record at the given level.
|
||||
func (h *logrusHandler) Enabled(_ context.Context, level slog.Level) bool {
|
||||
return level >= h.level
|
||||
}
|
||||
|
||||
// Handle formats and writes a log record. TTY output is coloured; file output
|
||||
// uses the logrus key=value text format.
|
||||
func (h *logrusHandler) Handle(_ context.Context, r slog.Record) error {
|
||||
label, name, code := logrusLevel(r.Level)
|
||||
ts := r.Time.Format("2006/01/02 15:04:05")
|
||||
|
||||
var line string
|
||||
if h.color {
|
||||
line = fmt.Sprintf("\x1b[%dm%s\x1b[0m[%s] %s\n", code, label, ts, r.Message)
|
||||
} else {
|
||||
line = fmt.Sprintf("time=%q level=%s msg=%q\n", ts, name, r.Message)
|
||||
}
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
_, err := fmt.Fprint(h.out, line)
|
||||
return err
|
||||
}
|
||||
|
||||
// WithAttrs returns the handler unchanged; structured attributes are not used.
|
||||
func (h *logrusHandler) WithAttrs(_ []slog.Attr) slog.Handler { return h }
|
||||
|
||||
// WithGroup returns the handler unchanged; groups are not used.
|
||||
func (h *logrusHandler) WithGroup(_ string) slog.Handler { return h }
|
||||
|
||||
// logrusLevel maps slog levels to the 4-char TTY label, lowercase file label, and ANSI colour code.
|
||||
func logrusLevel(level slog.Level) (string, string, int) {
|
||||
switch {
|
||||
case level < slog.LevelInfo:
|
||||
return "DEBU", "debug", 37 // gray
|
||||
case level < slog.LevelWarn:
|
||||
return "INFO", "info", 36 // cyan
|
||||
case level < slog.LevelError:
|
||||
return "WARN", "warning", 33 // yellow
|
||||
default:
|
||||
return "ERRO", "error", 31 // red
|
||||
}
|
||||
}
|
||||
|
||||
// isTerminal reports whether f is connected to a terminal.
|
||||
func isTerminal(f *os.File) bool {
|
||||
info, err := f.Stat()
|
||||
return err == nil && info.Mode()&os.ModeCharDevice != 0
|
||||
}
|
||||
|
||||
// Info logs a message at INFO level.
|
||||
func (l *Logger) Info(args ...any) { l.sl.Info(fmt.Sprint(args...)) }
|
||||
|
||||
// Infof logs a formatted message at INFO level.
|
||||
func (l *Logger) Infof(format string, args ...any) { l.sl.Info(fmt.Sprintf(format, args...)) }
|
||||
|
||||
// Debug logs a message at DEBUG level.
|
||||
func (l *Logger) Debug(args ...any) { l.sl.Debug(fmt.Sprint(args...)) }
|
||||
|
||||
// Debugf logs a formatted message at DEBUG level.
|
||||
func (l *Logger) Debugf(format string, args ...any) { l.sl.Debug(fmt.Sprintf(format, args...)) }
|
||||
|
||||
// Warn logs a message at WARN level.
|
||||
func (l *Logger) Warn(args ...any) { l.sl.Warn(fmt.Sprint(args...)) }
|
||||
|
||||
// Warnf logs a formatted message at WARN level.
|
||||
func (l *Logger) Warnf(format string, args ...any) { l.sl.Warn(fmt.Sprintf(format, args...)) }
|
||||
|
||||
// Error logs a message at ERROR level.
|
||||
func (l *Logger) Error(args ...any) { l.sl.Error(fmt.Sprint(args...)) }
|
||||
|
||||
// Errorf logs a formatted message at ERROR level.
|
||||
func (l *Logger) Errorf(format string, args ...any) { l.sl.Error(fmt.Sprintf(format, args...)) }
|
||||
|
||||
// Printf logs a formatted message at INFO level.
|
||||
func (l *Logger) Printf(format string, args ...any) { l.sl.Info(fmt.Sprintf(format, args...)) }
|
||||
|
||||
// Fatal logs a message at ERROR level then exits with status 1.
|
||||
func (l *Logger) Fatal(args ...any) { l.sl.Error(fmt.Sprint(args...)); os.Exit(1) }
|
||||
|
||||
// Fatalf logs a formatted message at ERROR level then exits with status 1.
|
||||
func (l *Logger) Fatalf(format string, args ...any) {
|
||||
l.sl.Error(fmt.Sprintf(format, args...))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// PrettyPrint prints any value as indented JSON to stdout, for debugging.
|
||||
func PrettyPrint(i any) {
|
||||
s, _ := json.MarshalIndent(i, "", "\t")
|
||||
fmt.Println(string(s))
|
||||
}
|
||||
|
||||
// CleanHTTPIP returns a human-readable IP for the logging interface
|
||||
// when starting services. It translates [::]:<port> to "localhost:<port>"
|
||||
// CleanHTTPIP returns a human-readable address for log output.
|
||||
// It translates [::]:<port> to localhost:<port>.
|
||||
func CleanHTTPIP(s string) string {
|
||||
re := regexp.MustCompile(`^\[\:\:\]\:\d+`)
|
||||
if re.MatchString(s) {
|
||||
|
||||
Reference in New Issue
Block a user