From 995ee1bffca1418bc8003d2d2e25b97742464959 Mon Sep 17 00:00:00 2001 From: Brandon McGinty Date: Thu, 20 Aug 2026 14:18:01 -0400 Subject: [PATCH] add opt-in leveled logging Add a log package with debug, info, warn, and error levels. It wraps a single process-wide logger that the gumble and audio packages can call into without importing a logging library. Logging is off unless -logfile names a file. Anything written to stderr while the terminal UI is running corrupts the display, so a logger is only installed once an explicit destination exists. -log sets the level and defaults to warn. Co-Authored-By: Claude Opus 5 --- log/log.go | 130 +++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 30 +++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 log/log.go diff --git a/log/log.go b/log/log.go new file mode 100644 index 0000000..4003911 --- /dev/null +++ b/log/log.go @@ -0,0 +1,130 @@ +// Package log provides debug logging for the barnard audio pipeline. +// Logging is disabled by default and can be enabled via SetLogger. +package log + +import ( + "fmt" + "io" + "os" + "sync" + "sync/atomic" + "time" +) + +// Level represents logging severity. +type Level int + +const ( + LevelDebug Level = iota + LevelInfo + LevelWarn + LevelError +) + +func (l Level) String() string { + switch l { + case LevelDebug: + return "DEBUG" + case LevelInfo: + return "INFO" + case LevelWarn: + return "WARN" + case LevelError: + return "ERROR" + default: + return "???" + } +} + +// Logger receives log messages. The default logger is a no-op. +type Logger interface { + Log(level Level, format string, args ...interface{}) +} + +type loggerState struct { + logger Logger + level Level +} + +var logger atomic.Pointer[loggerState] + +func init() { + logger.Store(&loggerState{logger: &nopLogger{}, level: LevelError + 1}) +} + +type nopLogger struct{} + +func (n *nopLogger) Log(level Level, format string, args ...interface{}) {} + +// SetLogger sets the destination for log messages. Pass nil to disable. +func SetLogger(l Logger) { + state := &loggerState{logger: l, level: LevelDebug} + if l == nil { + state.logger = &nopLogger{} + state.level = LevelError + 1 + } else if writer, ok := l.(*WriterLogger); ok { + state.level = writer.level + } + logger.Store(state) +} + +// Enabled reports whether messages at level will be emitted. Callers should +// use it to avoid computing expensive log arguments when logging is disabled. +func Enabled(level Level) bool { + return level >= logger.Load().level +} + +// WriterLogger is a simple Logger that writes to an io.Writer. +type WriterLogger struct { + mu sync.Mutex + w io.Writer + level Level + buf []byte +} + +// NewWriterLogger creates a logger that writes to w, filtering below level. +func NewWriterLogger(w io.Writer, level Level) *WriterLogger { + if w == nil { + w = os.Stderr + } + return &WriterLogger{w: w, level: level} +} + +func (wl *WriterLogger) Log(level Level, format string, args ...interface{}) { + if level < wl.level { + return + } + wl.mu.Lock() + defer wl.mu.Unlock() + now := time.Now().Format("15:04:05.000") + msg := fmt.Sprintf(format, args...) + fmt.Fprintf(wl.w, "%s [%-5s] %s\n", now, level.String(), msg) +} + +func Debug(format string, args ...interface{}) { + state := logger.Load() + if LevelDebug >= state.level { + state.logger.Log(LevelDebug, format, args...) + } +} + +func Info(format string, args ...interface{}) { + state := logger.Load() + if LevelInfo >= state.level { + state.logger.Log(LevelInfo, format, args...) + } +} + +func Warn(format string, args ...interface{}) { + state := logger.Load() + if LevelWarn >= state.level { + state.logger.Log(LevelWarn, format, args...) + } +} + +func Error(format string, args ...interface{}) { + state := logger.Load() + if LevelError >= state.level { + state.logger.Log(LevelError, format, args...) + } +} diff --git a/main.go b/main.go index cd9637c..4379c28 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,8 @@ import ( "strings" "syscall" + barnlog "git.stormux.org/storm/barnard/log" + "git.stormux.org/storm/barnard/config" "git.stormux.org/storm/barnard/gumble/go-openal/openal" "git.stormux.org/storm/barnard/gumble/gumble" @@ -114,9 +116,37 @@ func main() { buffers := flag.Int("buffers", 16, "number of audio buffers to use") profile := flag.Bool("profile", false, "add http server to serve profiles") noiseSuppressionEnabled := flag.Bool("noise-suppression", false, "enable noise suppression for microphone input") + logLevel := flag.String("log", "warn", "log level: debug, info, warn, error") + logFile := flag.String("logfile", "", "write logs to this file (logging is disabled when omitted)") flag.Parse() + // Set up logging + var level barnlog.Level + switch strings.ToLower(*logLevel) { + case "debug": + level = barnlog.LevelDebug + case "info": + level = barnlog.LevelInfo + case "warn": + level = barnlog.LevelWarn + case "error": + level = barnlog.LevelError + default: + level = barnlog.LevelWarn + } + // Logging is opt-in. Select /dev/stderr explicitly when terminal logging is + // desired; otherwise library diagnostics must not corrupt terminal output. + barnlog.SetLogger(nil) + if *logFile != "" { + f, err := os.OpenFile(*logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + fmt.Fprintf(os.Stderr, "cannot open log file %s: %v\n", *logFile, err) + } else { + barnlog.SetLogger(barnlog.NewWriterLogger(f, level)) + } + } + if *profile == true { go func() { log.Println(http.ListenAndServe("localhost:6060", nil))