// Copyright Earl Warren // Copyright Loïc Dachary // SPDX-License-Identifier: MIT package logger import ( "bytes" "context" "fmt" "io" "log/slog" "os" "runtime" "strings" "time" ) type logger struct { levels map[Level]int writer io.Writer logger *slog.Logger level Level levelVar *slog.LevelVar } var levels = map[Level]int{ Trace: int(slog.LevelDebug) - 1, Debug: int(slog.LevelDebug), Info: int(slog.LevelInfo), Warn: int(slog.LevelWarn), Error: int(slog.LevelError), Fatal: int(slog.LevelError + 1), } var levelList = []Level{ Trace, Debug, Info, Warn, Error, Fatal, } func MoreVerbose(level Level) *Level { position := levelToPosition(level) - 1 if position > 0 { return &levelList[position] } return nil } func LessVerbose(level Level) *Level { position := levelToPosition(level) + 1 if position < len(levelList) { return &levelList[position] } return nil } func levelToPosition(level Level) int { var i int for i = 0; i < len(levelList); i++ { if levelList[i] == level { break } } if i >= len(levelList) { panic(fmt.Errorf("unknown verbosity %v", level)) } return i } func NewLogger() Interface { l := &logger{} l.Init() return l } type captureLogger struct { logger buf *bytes.Buffer } func NewCaptureLogger() CaptureInterface { l := &captureLogger{} l.buf = new(bytes.Buffer) l.writer = l.buf l.Init() return l } func (o *captureLogger) String() string { return o.buf.String() } func (o *captureLogger) GetBuffer() *bytes.Buffer { return o.buf } func (o *captureLogger) Reset() { o.buf.Reset() } var filenamePrefix string func init() { _, filename, _, _ := runtime.Caller(0) filenamePrefix = strings.TrimSuffix(filename, "logger.go") if filenamePrefix == filename { // in case the source code file is moved, we can not trim the suffix, the code above should also be updated. panic("unable to detect correct package prefix, please update file: " + filename) } } func (o *logger) Log(skip int, level Level, format string, args ...any) { slogLevel := levels[level] logger := o.logger if !logger.Handler().Enabled(context.Background(), slog.Level(slogLevel)) { return } var pcs [1]uintptr runtime.Callers(2+skip, pcs[:]) // 2 is to skip [Callers(), Log()] r := slog.NewRecord(time.Now(), slog.Level(slogLevel), fmt.Sprintf(format, args...), pcs[0]) _ = logger.Handler().Handle(context.Background(), r) } func (o *logger) Init() { replace := func(groups []string, a slog.Attr) slog.Attr { if a.Key == slog.TimeKey && len(groups) == 0 { return slog.Attr{} } if a.Key == slog.SourceKey { source := a.Value.Any().(*slog.Source) var function string dot := strings.LastIndex(source.Function, ".") if dot >= 0 { function = ":" + source.Function[dot+1:] } source.File = strings.TrimPrefix(source.File, projectPackagePrefix) + function } return a } o.levelVar = new(slog.LevelVar) if o.writer == nil { o.writer = os.Stdout } o.logger = slog.New(slog.NewTextHandler(o.writer, &slog.HandlerOptions{ Level: o.levelVar, AddSource: true, ReplaceAttr: replace, })) } func (o *logger) SetLevel(level Level) { o.level = level o.levelVar.Set(slog.Level(levels[o.level])) } func (o *logger) GetLevel() Level { return o.level } func (o *logger) SetWriter(out io.Writer) { o.writer = out o.Init() } func (o *logger) Message(message string, args ...any) { o.Log(1, Info, message, args...) } func (o *logger) Trace(message string, args ...any) { o.Log(1, Trace, message, args...) } func (o *logger) Debug(message string, args ...any) { o.Log(1, Debug, message, args...) } func (o *logger) Info(message string, args ...any) { o.Log(1, Info, message, args...) } func (o *logger) Warn(message string, args ...any) { o.Log(1, Warn, message, args...) } func (o *logger) Error(message string, args ...any) { o.Log(1, Error, message, args...) } func (o *logger) Fatal(message string, args ...any) { o.Log(1, Fatal, message, args...) } type Logger struct { logger Interface } func (o *Logger) GetLogger() Interface { return o.logger } func (o *Logger) SetLogger(logger Interface) { o.logger = logger } func (o *Logger) SetLevel(level Level) { o.logger.SetLevel(level) } func (o *Logger) GetLevel() Level { return o.logger.GetLevel() } func (o *Logger) Message(message string, args ...any) { o.logger.Log(1, Message, message, args...) } func (o *Logger) Trace(message string, args ...any) { o.logger.Log(1, Trace, message, args...) } func (o *Logger) Debug(message string, args ...any) { o.logger.Log(1, Debug, message, args...) } func (o *Logger) Info(message string, args ...any) { o.logger.Log(1, Info, message, args...) } func (o *Logger) Warn(message string, args ...any) { o.logger.Log(1, Warn, message, args...) } func (o *Logger) Error(message string, args ...any) { o.logger.Log(1, Error, message, args...) } func (o *Logger) Fatal(message string, args ...any) { o.logger.Log(1, Fatal, message, args...) } func (o *Logger) Log(skip int, level Level, message string, args ...any) { o.logger.Log(skip+1, level, message, args...) } var projectPackagePrefix string func init() { _, filename, _, _ := runtime.Caller(0) projectPackagePrefix = strings.TrimSuffix(filename, "logger/logger.go") if projectPackagePrefix == filename { // in case the source code file is moved, we can not trim the suffix, the code above should also be updated. panic("unable to detect correct package prefix, please update file: " + filename) } }