package log import ( "fmt" "io" "os" "path/filepath" "runtime" "strings" "sync" "sync/atomic" ) // NewLogger creates and returns a new instance of Logger. // Default level is InfoLevel (Debug messages are suppressed unless SetVerbose is called). func NewLogger(base Base) *Logger { if base == nil { base = newBase(os.Stderr) } return &Logger{base: base, minLevel: InfoLevel} } // Logger logs messages to io.Writer at various log levels. type Logger struct { base Base // Minimum log level for this logger. // Messages with level lower than this won't be outputted. minLevel Level } // canLogAt reports whether logger can log at level v. func (l *Logger) canLogAt(v Level) bool { return v >= Level(atomic.LoadInt32((*int32)(&l.minLevel))) } // SetLevel sets the logger level. // It panics if v is less than DebugLevel or greater than FatalLevel. func (l *Logger) SetLevel(v Level) { if v < DebugLevel || v > FatalLevel { panic("log: invalid log level") } atomic.StoreInt32((*int32)(&l.minLevel), int32(v)) } // baseCallerSkip is the number of frames to skip in runtime.Caller to reach // the actual call site. Both package-level functions (log.Xxx -> logMsg -> base.Log) // and Logger methods (Logger.Xxx -> logMsg -> base.Log) add exactly one frame // above logMsg, so the skip is the same: base.Log(0) -> logMsg(1) -> caller_wrapper(2) -> caller(3). const baseCallerSkip = 3 // logMsg is the internal method all public methods delegate to. func (l *Logger) logMsg(lvl Level, msg string) { if !l.canLogAt(lvl) { return } l.base.Log(baseCallerSkip, lvl, msg) } func (l *Logger) Debug(args ...any) { l.logMsg(DebugLevel, fmt.Sprint(args...)) } func (l *Logger) Debugf(format string, args ...any) { l.logMsg(DebugLevel, fmt.Sprintf(format, args...)) } func (l *Logger) Info(args ...any) { l.logMsg(InfoLevel, fmt.Sprint(args...)) } func (l *Logger) Infof(format string, args ...any) { l.logMsg(InfoLevel, fmt.Sprintf(format, args...)) } func (l *Logger) Warn(args ...any) { l.logMsg(WarnLevel, fmt.Sprint(args...)) } func (l *Logger) Warnf(format string, args ...any) { l.logMsg(WarnLevel, fmt.Sprintf(format, args...)) } func (l *Logger) Error(args ...any) { l.logMsg(ErrorLevel, fmt.Sprint(args...)) } func (l *Logger) Errorf(format string, args ...any) { l.logMsg(ErrorLevel, fmt.Sprintf(format, args...)) } func (l *Logger) Fatal(args ...any) { l.logMsg(FatalLevel, fmt.Sprint(args...)) osExit(1) } func (l *Logger) Fatalf(format string, args ...any) { l.logMsg(FatalLevel, fmt.Sprintf(format, args...)) osExit(1) } // Base is the interface that underlies the Logger. It receives the caller // skip count, log level, and formatted message. type Base interface { Log(callerSkip int, lvl Level, msg string) } // baseLogger writes formatted log messages to an io.Writer. // Output format: // // [DBG] file.go:42: message // [INF] message // [WRN] message // [ERR] message // [FTL] message type baseLogger struct { out io.Writer mu sync.Mutex } func newBase(out io.Writer) *baseLogger { return &baseLogger{out: out} } var osExit = os.Exit // continuation is the indent for multi-line messages. // Width matches "[DBG] " (6 chars). const continuation = " " func (l *baseLogger) Log(callerSkip int, lvl Level, msg string) { msg = strings.TrimRight(msg, "\n") if strings.Contains(msg, "\n") { msg = strings.ReplaceAll(msg, "\n", "\n"+continuation) } label := l.formatLabel(lvl) var line string if lvl == DebugLevel { _, file, num, ok := runtime.Caller(callerSkip) if ok { file = filepath.Base(file) } else { file = "???" num = 0 } line = fmt.Sprintf("%s %s:%d: %s\n", label, file, num, msg) } else { line = fmt.Sprintf("%s %s\n", label, msg) } l.mu.Lock() _, _ = io.WriteString(l.out, line) l.mu.Unlock() } // formatLabel returns the bracketed level label, e.g. "[DBG]". func (l *baseLogger) formatLabel(lvl Level) string { return "[" + lvl.String() + "]" }