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 <noreply@anthropic.com>
This commit is contained in:
Brandon McGinty
2026-08-20 14:18:01 -04:00
co-authored by Claude Opus 5
parent 30a02eba14
commit 995ee1bffc
2 changed files with 160 additions and 0 deletions
+130
View File
@@ -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...)
}
}
+30
View File
@@ -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))