Files

230 lines
6.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//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...) }