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...) }
|