check / check (push) Failing after 0s
script/test now runs `go test -timeout 30s -race -cover ./...`, quiet on success and rerunning verbose with `exit 1` on failure, per policy. -race surfaced a real data race on the process-global apex/log logger: Init reconfigures it (SetHandler/SetLevel) each CLI run while other goroutines read it to log. internal/log now mutates the global under the write lock and reads it under the read lock (new emit helper, DebugReal), and drops WithError, whose returned Entry logged outside that lock. That Entry was also the only thing printing a failed command's error to stderr, so run() now reports it via log.Errorf (shown under -q too). The corruption fuzz test shrinks (20000->1500 files, 500->100 iters) to keep the suite under 20s with -race. Model: opus-4-8
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")
|
|
}
|