Files
goget/internal/log/logger.go
T

230 lines
6.1 KiB
Go
Raw Permalink Normal View History

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