check / check (push) Successful in 51s
script/test now runs go test -timeout 30s -race -cover ./..., quiet on success and rerunning verbose on failure. -race exposed a data race on the process-global apex/log logger: Init reconfigured it on every CLI run while other goroutines logged through it. internal/log now mutates the global under the write lock and reads it under the read lock; WithError is dropped because its Entry logged outside that lock, and run() reports a failed command's error via log.Errorf instead (still shown under -q). The corruption fuzz test is scaled to 1500 files / 100 iterations to fit the budget under -race; it pins the same claims at reduced breadth. Independent review ran the Docker lint gate uncached. Model: opus-4-8 (implementation, review) model: claude-fable-5
287 lines
6.7 KiB
Go
287 lines
6.7 KiB
Go
// Package log provides leveled logging with progress output helpers
|
|
// on top of apex/log and pterm.
|
|
package log
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"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"
|
|
}
|
|
}
|
|
|
|
// callerSkip is the runtime.Caller stack depth from the public Debug
|
|
// helpers to the caller of the log package.
|
|
const callerSkip = 2
|
|
|
|
//nolint:gochecknoglobals // package-level logger state by design
|
|
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 = 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.
|
|
//
|
|
// It reconfigures the process-global apex/log logger under the write lock so
|
|
// the global is never mutated while another goroutine holds the read lock to
|
|
// read it in emit. Without this, parallel callers (e.g. the test suite) race
|
|
// Init's SetLevel/SetHandler against concurrent log calls.
|
|
func Init() {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
log.SetHandler(acli.New(stderr))
|
|
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
|
|
}
|
|
|
|
// emit calls fn while holding the read lock if messages at level l are
|
|
// enabled. Holding the read lock across the apex/log call keeps the global
|
|
// logger from being read while Init reconfigures it under the write lock.
|
|
func emit(l Level, fn func()) {
|
|
mu.RLock()
|
|
defer mu.RUnlock()
|
|
|
|
if l >= currentLevel {
|
|
fn()
|
|
}
|
|
}
|
|
|
|
// Fatalf logs a formatted message at fatal level.
|
|
func Fatalf(format string, args ...any) {
|
|
emit(FatalLevel, func() { log.Fatalf(format, args...) })
|
|
}
|
|
|
|
// Fatal logs a message at fatal level.
|
|
func Fatal(arg string) {
|
|
emit(FatalLevel, func() { log.Fatal(arg) })
|
|
}
|
|
|
|
// Errorf logs a formatted message at error level.
|
|
func Errorf(format string, args ...any) {
|
|
emit(ErrorLevel, func() { log.Errorf(format, args...) })
|
|
}
|
|
|
|
// Error logs a message at error level.
|
|
func Error(arg string) {
|
|
emit(ErrorLevel, func() { log.Error(arg) })
|
|
}
|
|
|
|
// Warnf logs a formatted message at warn level.
|
|
func Warnf(format string, args ...any) {
|
|
emit(WarnLevel, func() { log.Warnf(format, args...) })
|
|
}
|
|
|
|
// Warn logs a message at warn level.
|
|
func Warn(arg string) {
|
|
emit(WarnLevel, func() { log.Warn(arg) })
|
|
}
|
|
|
|
// Infof logs a formatted message at info level.
|
|
func Infof(format string, args ...any) {
|
|
emit(InfoLevel, func() { log.Infof(format, args...) })
|
|
}
|
|
|
|
// Info logs a message at info level.
|
|
func Info(arg string) {
|
|
emit(InfoLevel, func() { log.Info(arg) })
|
|
}
|
|
|
|
// Verbosef logs a formatted message at verbose level.
|
|
func Verbosef(format string, args ...any) {
|
|
emit(VerboseLevel, func() { log.Infof(format, args...) })
|
|
}
|
|
|
|
// Verbose logs a message at verbose level.
|
|
func Verbose(arg string) {
|
|
emit(VerboseLevel, func() { log.Info(arg) })
|
|
}
|
|
|
|
// Debugf logs a formatted message at debug level with caller location.
|
|
func Debugf(format string, args ...any) {
|
|
if isEnabled(DebugLevel) {
|
|
DebugReal(fmt.Sprintf(format, args...), callerSkip)
|
|
}
|
|
}
|
|
|
|
// Debug logs a message at debug level with caller location.
|
|
func Debug(arg string) {
|
|
if isEnabled(DebugLevel) {
|
|
DebugReal(arg, callerSkip)
|
|
}
|
|
}
|
|
|
|
// DebugReal logs at debug level with caller info from the specified stack depth.
|
|
func DebugReal(arg string, cs int) {
|
|
mu.RLock()
|
|
defer mu.RUnlock()
|
|
|
|
if DebugLevel < currentLevel {
|
|
return
|
|
}
|
|
|
|
_, callerFile, callerLine, ok := runtime.Caller(cs)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
tag := fmt.Sprintf("%s:%d: ", filepath.Base(callerFile), callerLine)
|
|
log.Debug(tag + arg)
|
|
}
|
|
|
|
// Dump logs a spew dump of the arguments at debug level.
|
|
func Dump(args ...any) {
|
|
if isEnabled(DebugLevel) {
|
|
DebugReal(spew.Sdump(args...), callerSkip)
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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 ...any) {
|
|
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")
|
|
}
|