230 lines
6.1 KiB
Go
230 lines
6.1 KiB
Go
//go:build linux || freebsd
|
||||
|
|
// +build linux freebsd
|
|||
|
|
|
|||
|
|
// Package log provides a structured logger with text and JSON output modes.
|
|||
|
|
// It replaces ad-hoc fmt.Fprintf and cli.Print* calls across the codebase.
|
|||
|
|
package log
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"encoding/json"
|
|||
|
|
"fmt"
|
|||
|
|
"io"
|
|||
|
|
"os"
|
|||
|
|
"sync"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Level represents the severity of a log message.
|
|||
|
|
type Level int
|
|||
|
|
|
|||
|
|
const (
|
|||
|
|
ErrorLevel Level = iota
|
|||
|
|
WarnLevel
|
|||
|
|
InfoLevel
|
|||
|
|
VerboseLevel
|
|||
|
|
DebugLevel
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func (l Level) String() string {
|
|||
|
|
switch l {
|
|||
|
|
case ErrorLevel:
|
|||
|
|
return "error"
|
|||
|
|
case WarnLevel:
|
|||
|
|
return "warn"
|
|||
|
|
case InfoLevel:
|
|||
|
|
return "info"
|
|||
|
|
case VerboseLevel:
|
|||
|
|
return "verbose"
|
|||
|
|
case DebugLevel:
|
|||
|
|
return "debug"
|
|||
|
|
default:
|
|||
|
|
return "unknown"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Logger writes structured log messages.
|
|||
|
|
type Logger struct {
|
|||
|
|
mu sync.Mutex
|
|||
|
|
w io.Writer
|
|||
|
|
level Level
|
|||
|
|
color bool
|
|||
|
|
json bool
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Option configures a Logger.
|
|||
|
|
type Option func(*Logger)
|
|||
|
|
|
|||
|
|
// WithLevel sets the minimum log level. Messages below this level are
|
|||
|
|
// silently discarded.
|
|||
|
|
func WithLevel(l Level) Option {
|
|||
|
|
return func(log *Logger) { log.level = l }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// WithColor enables or disables ANSI color output in text mode.
|
|||
|
|
func WithColor(c bool) Option {
|
|||
|
|
return func(log *Logger) { log.color = c }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// WithJSON enables JSON output mode. When enabled, each message is written as
|
|||
|
|
// a JSON object on a single line.
|
|||
|
|
func WithJSON(j bool) Option {
|
|||
|
|
return func(log *Logger) { log.json = j }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// New creates a new Logger that writes to w.
|
|||
|
|
// Defaults: InfoLevel, color enabled, text mode.
|
|||
|
|
func New(w io.Writer, opts ...Option) *Logger {
|
|||
|
|
l := &Logger{w: w, level: InfoLevel, color: true}
|
|||
|
|
for _, o := range opts {
|
|||
|
|
o(l)
|
|||
|
|
}
|
|||
|
|
return l
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Error logs a message at error level.
|
|||
|
|
func (l *Logger) Error(msg string) { l.log(ErrorLevel, msg, nil) }
|
|||
|
|
|
|||
|
|
// Errorf logs a formatted message at error level.
|
|||
|
|
func (l *Logger) Errorf(format string, args ...interface{}) {
|
|||
|
|
l.log(ErrorLevel, fmt.Sprintf(format, args...), nil)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Warn logs a message at warning level.
|
|||
|
|
func (l *Logger) Warn(msg string) { l.log(WarnLevel, msg, nil) }
|
|||
|
|
|
|||
|
|
// Warnf logs a formatted message at warning level.
|
|||
|
|
func (l *Logger) Warnf(format string, args ...interface{}) {
|
|||
|
|
l.log(WarnLevel, fmt.Sprintf(format, args...), nil)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Info logs a message at info level.
|
|||
|
|
func (l *Logger) Info(msg string) { l.log(InfoLevel, msg, nil) }
|
|||
|
|
|
|||
|
|
// Infof logs a formatted message at info level.
|
|||
|
|
func (l *Logger) Infof(format string, args ...interface{}) {
|
|||
|
|
l.log(InfoLevel, fmt.Sprintf(format, args...), nil)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Verbose logs a message at verbose level. Only shown when the logger is
|
|||
|
|
// configured at VerboseLevel or below.
|
|||
|
|
func (l *Logger) Verbose(msg string) { l.log(VerboseLevel, msg, nil) }
|
|||
|
|
|
|||
|
|
// Verbosef logs a formatted message at verbose level.
|
|||
|
|
func (l *Logger) Verbosef(format string, args ...interface{}) {
|
|||
|
|
l.log(VerboseLevel, fmt.Sprintf(format, args...), nil)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Debug logs a message at debug level. Only shown when the logger is
|
|||
|
|
// configured at DebugLevel.
|
|||
|
|
func (l *Logger) Debug(msg string) { l.log(DebugLevel, msg, nil) }
|
|||
|
|
|
|||
|
|
// Debugf logs a formatted message at debug level.
|
|||
|
|
func (l *Logger) Debugf(format string, args ...interface{}) {
|
|||
|
|
l.log(DebugLevel, fmt.Sprintf(format, args...), nil)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Success logs a success message at info level with a checkmark prefix.
|
|||
|
|
// In JSON mode, it adds a "status":"success" field.
|
|||
|
|
func (l *Logger) Success(msg string) {
|
|||
|
|
l.log(InfoLevel, msg, map[string]interface{}{"status": "success"})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Successf logs a formatted success message.
|
|||
|
|
func (l *Logger) Successf(format string, args ...interface{}) {
|
|||
|
|
l.Success(fmt.Sprintf(format, args...))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (l *Logger) log(level Level, msg string, fields map[string]interface{}) {
|
|||
|
|
if level > l.level {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
l.mu.Lock()
|
|||
|
|
defer l.mu.Unlock()
|
|||
|
|
|
|||
|
|
if l.json {
|
|||
|
|
l.logJSON(level, msg, fields)
|
|||
|
|
} else {
|
|||
|
|
l.logText(level, msg)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (l *Logger) logText(level Level, msg string) {
|
|||
|
|
var prefix, suffix string
|
|||
|
|
if l.color {
|
|||
|
|
suffix = colorReset
|
|||
|
|
switch level {
|
|||
|
|
case ErrorLevel:
|
|||
|
|
prefix = colorRed + colorBold
|
|||
|
|
case WarnLevel:
|
|||
|
|
prefix = colorYellow + colorBold
|
|||
|
|
case InfoLevel:
|
|||
|
|
prefix = colorCyan + colorBold
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
icon := levelIcon(level)
|
|||
|
|
if prefix != "" {
|
|||
|
|
fmt.Fprintf(l.w, "%s%s%s %s\n", prefix, icon, suffix, msg)
|
|||
|
|
} else {
|
|||
|
|
fmt.Fprintf(l.w, "%s %s\n", icon, msg)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type jsonEntry struct {
|
|||
|
|
Level string `json:"level"`
|
|||
|
|
Msg string `json:"msg"`
|
|||
|
|
Fields map[string]interface{} `json:"fields,omitempty"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (l *Logger) logJSON(level Level, msg string, fields map[string]interface{}) {
|
|||
|
|
entry := jsonEntry{
|
|||
|
|
Level: level.String(),
|
|||
|
|
Msg: msg,
|
|||
|
|
}
|
|||
|
|
if fields != nil {
|
|||
|
|
entry.Fields = fields
|
|||
|
|
}
|
|||
|
|
// Best-effort encoding; errors are silently dropped to avoid log loops.
|
|||
|
|
data, _ := json.Marshal(entry)
|
|||
|
|
fmt.Fprintln(l.w, string(data))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func levelIcon(level Level) string {
|
|||
|
|
switch level {
|
|||
|
|
case ErrorLevel:
|
|||
|
|
return "✗ Error:"
|
|||
|
|
case WarnLevel:
|
|||
|
|
return "⚠ Warning:"
|
|||
|
|
case InfoLevel, VerboseLevel, DebugLevel:
|
|||
|
|
return "ℹ"
|
|||
|
|
default:
|
|||
|
|
return "•"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ANSI escape codes.
|
|||
|
|
const (
|
|||
|
|
colorReset = "\033[0m"
|
|||
|
|
colorRed = "\033[31m"
|
|||
|
|
colorYellow = "\033[33m"
|
|||
|
|
colorCyan = "\033[36m"
|
|||
|
|
colorBold = "\033[1m"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// DefaultLogger is the global logger used by package-level convenience
|
|||
|
|
// functions. It writes to stderr at InfoLevel in text mode.
|
|||
|
|
var DefaultLogger = New(os.Stderr)
|
|||
|
|
|
|||
|
|
// Package-level convenience functions that delegate to DefaultLogger.
|
|||
|
|
func Error(msg string) { DefaultLogger.Error(msg) }
|
|||
|
|
func Errorf(f string, a ...interface{}) { DefaultLogger.Errorf(f, a...) }
|
|||
|
|
func Warn(msg string) { DefaultLogger.Warn(msg) }
|
|||
|
|
func Warnf(f string, a ...interface{}) { DefaultLogger.Warnf(f, a...) }
|
|||
|
|
func Info(msg string) { DefaultLogger.Info(msg) }
|
|||
|
|
func Infof(f string, a ...interface{}) { DefaultLogger.Infof(f, a...) }
|
|||
|
|
func Verbose(msg string) { DefaultLogger.Verbose(msg) }
|
|||
|
|
func Verbosef(f string, a ...interface{}) { DefaultLogger.Verbosef(f, a...) }
|
|||
|
|
func Debug(msg string) { DefaultLogger.Debug(msg) }
|
|||
|
|
func Debugf(f string, a ...interface{}) { DefaultLogger.Debugf(f, a...) }
|
|||
|
|
func Success(msg string) { DefaultLogger.Success(msg) }
|
|||
|
|
func Successf(f string, a ...interface{}) { DefaultLogger.Successf(f, a...) }
|