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