- Atomic writes for mfer gen: writes to temp file, renames on success, cleans up temp on error/interrupt. Prevents empty manifests on Ctrl-C. - Humanized byte sizes using dustin/go-humanize (e.g., "10 MiB" not "10485760") - Progress lines clear when done (using ANSI escape \r\033[K]) - Debug logging when files are added to manifest (mfer gen -vv) - Move -v/-q flags from global to per-command for better UX - Add tests for atomic write behavior with failing filesystem mock
274 lines
5.9 KiB
Go
274 lines
5.9 KiB
Go
package log
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"runtime"
|
|
"sync"
|
|
|
|
"github.com/apex/log"
|
|
acli "github.com/apex/log/handlers/cli"
|
|
"github.com/davecgh/go-spew/spew"
|
|
"github.com/pterm/pterm"
|
|
)
|
|
|
|
// Level represents log severity levels.
|
|
// Lower values are more verbose.
|
|
type Level int
|
|
|
|
const (
|
|
// DebugLevel is for low-level tracing and structure inspection
|
|
DebugLevel Level = iota
|
|
// VerboseLevel is for detailed operational info (file listings, etc)
|
|
VerboseLevel
|
|
// InfoLevel is for operational summaries (default)
|
|
InfoLevel
|
|
// WarnLevel is for warnings
|
|
WarnLevel
|
|
// ErrorLevel is for errors
|
|
ErrorLevel
|
|
// FatalLevel is for fatal errors
|
|
FatalLevel
|
|
)
|
|
|
|
func (l Level) String() string {
|
|
switch l {
|
|
case DebugLevel:
|
|
return "debug"
|
|
case VerboseLevel:
|
|
return "verbose"
|
|
case InfoLevel:
|
|
return "info"
|
|
case WarnLevel:
|
|
return "warn"
|
|
case ErrorLevel:
|
|
return "error"
|
|
case FatalLevel:
|
|
return "fatal"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
var (
|
|
// mu protects the output writers and level
|
|
mu sync.RWMutex
|
|
// stdout is the writer for progress output
|
|
stdout io.Writer = os.Stdout
|
|
// stderr is the writer for log output
|
|
stderr io.Writer = os.Stderr
|
|
// currentLevel is our log level (includes Verbose)
|
|
currentLevel Level = InfoLevel
|
|
)
|
|
|
|
// SetOutput configures the output writers for the log package.
|
|
// stdout is used for progress output, stderr is used for log messages.
|
|
func SetOutput(out, err io.Writer) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
stdout = out
|
|
stderr = err
|
|
pterm.SetDefaultOutput(out)
|
|
}
|
|
|
|
// GetStdout returns the configured stdout writer.
|
|
func GetStdout() io.Writer {
|
|
mu.RLock()
|
|
defer mu.RUnlock()
|
|
return stdout
|
|
}
|
|
|
|
// GetStderr returns the configured stderr writer.
|
|
func GetStderr() io.Writer {
|
|
mu.RLock()
|
|
defer mu.RUnlock()
|
|
return stderr
|
|
}
|
|
|
|
// DisableStyling turns off colors and styling for terminal output.
|
|
func DisableStyling() {
|
|
pterm.DisableColor()
|
|
pterm.DisableStyling()
|
|
pterm.Debug.Prefix.Text = ""
|
|
pterm.Info.Prefix.Text = ""
|
|
pterm.Success.Prefix.Text = ""
|
|
pterm.Warning.Prefix.Text = ""
|
|
pterm.Error.Prefix.Text = ""
|
|
pterm.Fatal.Prefix.Text = ""
|
|
}
|
|
|
|
// Init initializes the logger with the CLI handler and default log level.
|
|
func Init() {
|
|
mu.RLock()
|
|
w := stderr
|
|
mu.RUnlock()
|
|
log.SetHandler(acli.New(w))
|
|
log.SetLevel(log.DebugLevel) // Let apex/log pass everything; we filter ourselves
|
|
}
|
|
|
|
// isEnabled returns true if messages at the given level should be logged.
|
|
func isEnabled(l Level) bool {
|
|
mu.RLock()
|
|
defer mu.RUnlock()
|
|
return l >= currentLevel
|
|
}
|
|
|
|
// Fatalf logs a formatted message at fatal level.
|
|
func Fatalf(format string, args ...interface{}) {
|
|
if isEnabled(FatalLevel) {
|
|
log.Fatalf(format, args...)
|
|
}
|
|
}
|
|
|
|
// Fatal logs a message at fatal level.
|
|
func Fatal(arg string) {
|
|
if isEnabled(FatalLevel) {
|
|
log.Fatal(arg)
|
|
}
|
|
}
|
|
|
|
// Errorf logs a formatted message at error level.
|
|
func Errorf(format string, args ...interface{}) {
|
|
if isEnabled(ErrorLevel) {
|
|
log.Errorf(format, args...)
|
|
}
|
|
}
|
|
|
|
// Error logs a message at error level.
|
|
func Error(arg string) {
|
|
if isEnabled(ErrorLevel) {
|
|
log.Error(arg)
|
|
}
|
|
}
|
|
|
|
// Warnf logs a formatted message at warn level.
|
|
func Warnf(format string, args ...interface{}) {
|
|
if isEnabled(WarnLevel) {
|
|
log.Warnf(format, args...)
|
|
}
|
|
}
|
|
|
|
// Warn logs a message at warn level.
|
|
func Warn(arg string) {
|
|
if isEnabled(WarnLevel) {
|
|
log.Warn(arg)
|
|
}
|
|
}
|
|
|
|
// Infof logs a formatted message at info level.
|
|
func Infof(format string, args ...interface{}) {
|
|
if isEnabled(InfoLevel) {
|
|
log.Infof(format, args...)
|
|
}
|
|
}
|
|
|
|
// Info logs a message at info level.
|
|
func Info(arg string) {
|
|
if isEnabled(InfoLevel) {
|
|
log.Info(arg)
|
|
}
|
|
}
|
|
|
|
// Verbosef logs a formatted message at verbose level.
|
|
func Verbosef(format string, args ...interface{}) {
|
|
if isEnabled(VerboseLevel) {
|
|
log.Infof(format, args...)
|
|
}
|
|
}
|
|
|
|
// Verbose logs a message at verbose level.
|
|
func Verbose(arg string) {
|
|
if isEnabled(VerboseLevel) {
|
|
log.Info(arg)
|
|
}
|
|
}
|
|
|
|
// Debugf logs a formatted message at debug level with caller location.
|
|
func Debugf(format string, args ...interface{}) {
|
|
if isEnabled(DebugLevel) {
|
|
DebugReal(fmt.Sprintf(format, args...), 2)
|
|
}
|
|
}
|
|
|
|
// Debug logs a message at debug level with caller location.
|
|
func Debug(arg string) {
|
|
if isEnabled(DebugLevel) {
|
|
DebugReal(arg, 2)
|
|
}
|
|
}
|
|
|
|
// DebugReal logs at debug level with caller info from the specified stack depth.
|
|
func DebugReal(arg string, cs int) {
|
|
if !isEnabled(DebugLevel) {
|
|
return
|
|
}
|
|
_, callerFile, callerLine, ok := runtime.Caller(cs)
|
|
if !ok {
|
|
return
|
|
}
|
|
tag := fmt.Sprintf("%s:%d: ", callerFile, callerLine)
|
|
log.Debug(tag + arg)
|
|
}
|
|
|
|
// Dump logs a spew dump of the arguments at debug level.
|
|
func Dump(args ...interface{}) {
|
|
if isEnabled(DebugLevel) {
|
|
DebugReal(spew.Sdump(args...), 2)
|
|
}
|
|
}
|
|
|
|
// EnableDebugLogging sets the log level to debug.
|
|
func EnableDebugLogging() {
|
|
SetLevel(DebugLevel)
|
|
}
|
|
|
|
// VerbosityStepsToLogLevel converts a -v count to a log level.
|
|
// 0 returns InfoLevel, 1 returns VerboseLevel, 2+ returns DebugLevel.
|
|
func VerbosityStepsToLogLevel(l int) Level {
|
|
switch l {
|
|
case 0:
|
|
return InfoLevel
|
|
case 1:
|
|
return VerboseLevel
|
|
default:
|
|
return DebugLevel
|
|
}
|
|
}
|
|
|
|
// SetLevelFromVerbosity sets the log level based on -v flag count.
|
|
func SetLevelFromVerbosity(l int) {
|
|
SetLevel(VerbosityStepsToLogLevel(l))
|
|
}
|
|
|
|
// SetLevel sets the global log level.
|
|
func SetLevel(l Level) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
currentLevel = l
|
|
}
|
|
|
|
// GetLevel returns the current log level.
|
|
func GetLevel() Level {
|
|
mu.RLock()
|
|
defer mu.RUnlock()
|
|
return currentLevel
|
|
}
|
|
|
|
// WithError returns a log entry with the error attached.
|
|
func WithError(e error) *log.Entry {
|
|
return log.Log.WithError(e)
|
|
}
|
|
|
|
// Progressf prints a progress message that overwrites the current line.
|
|
// Use ProgressDone() when progress is complete to move to the next line.
|
|
func Progressf(format string, args ...interface{}) {
|
|
pterm.Printf("\r"+format, args...)
|
|
}
|
|
|
|
// ProgressDone clears the progress line when progress is complete.
|
|
func ProgressDone() {
|
|
// Clear the line with spaces and return to beginning
|
|
pterm.Print("\r\033[K")
|
|
}
|