savinmax 3d14b7fcb8 feat(logging): add configurable log output and log level support
Add a 'logging' section to config.yaml supporting:
- output: stderr (default), stdout, or a file path
- level: debug, info, warn, error (default: info)

Implementation:
- New internal/logging package with Setup() for output destination
  and Logger struct with level-aware Debug/Info/Warn/Error methods
- Config struct extended with Logging section (output + level fields)
- Hub refactored to accept *logging.Logger via constructor injection
- main.go initializes logging early after config load

The leveled logger suppresses messages below the configured threshold
while maintaining the stdlib log format. File output uses append mode
with 0644 permissions for safe log rotation.

🤖 Assisted by the code-assist SOP
2026-06-11 19:21:20 +02:00

127 lines
2.8 KiB
Go

package logging
import (
"fmt"
"io"
"log"
"os"
"strings"
)
// Level represents the severity of a log message.
type Level int
const (
LevelDebug Level = iota
LevelInfo
LevelWarn
LevelError
)
// Logger provides level-aware logging.
type Logger struct {
level Level
logger *log.Logger
}
// Setup configures the global log output destination.
// Returns the opened file (if a file path was given) so the caller can defer Close.
// For "stdout", "stderr", or empty string, returns nil (no file to close).
func Setup(output string) (*os.File, error) {
switch strings.ToLower(output) {
case "", "stderr":
log.SetOutput(os.Stderr)
return nil, nil
case "stdout":
log.SetOutput(os.Stdout)
return nil, nil
default:
file, err := os.OpenFile(output, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return nil, fmt.Errorf("failed to open log file %s: %w", output, err)
}
log.SetOutput(file)
return file, nil
}
}
// NewLogger creates a new Logger with the given level and output writer.
// If level is empty or invalid, defaults to LevelInfo.
func NewLogger(level string, output io.Writer) *Logger {
return &Logger{
level: parseLevel(level),
logger: log.New(output, "", log.LstdFlags),
}
}
// Debug logs a message at debug level.
func (l *Logger) Debug(msg string) {
if l.level <= LevelDebug {
l.logger.Printf("[DEBUG] %s", msg)
}
}
// Debugf logs a formatted message at debug level.
func (l *Logger) Debugf(format string, args ...interface{}) {
if l.level <= LevelDebug {
l.logger.Printf("[DEBUG] "+format, args...)
}
}
// Info logs a message at info level.
func (l *Logger) Info(msg string) {
if l.level <= LevelInfo {
l.logger.Printf("[INFO] %s", msg)
}
}
// Infof logs a formatted message at info level.
func (l *Logger) Infof(format string, args ...interface{}) {
if l.level <= LevelInfo {
l.logger.Printf("[INFO] "+format, args...)
}
}
// Warn logs a message at warn level.
func (l *Logger) Warn(msg string) {
if l.level <= LevelWarn {
l.logger.Printf("[WARN] %s", msg)
}
}
// Warnf logs a formatted message at warn level.
func (l *Logger) Warnf(format string, args ...interface{}) {
if l.level <= LevelWarn {
l.logger.Printf("[WARN] "+format, args...)
}
}
// Error logs a message at error level.
func (l *Logger) Error(msg string) {
if l.level <= LevelError {
l.logger.Printf("[ERROR] %s", msg)
}
}
// Errorf logs a formatted message at error level.
func (l *Logger) Errorf(format string, args ...interface{}) {
if l.level <= LevelError {
l.logger.Printf("[ERROR] "+format, args...)
}
}
func parseLevel(level string) Level {
switch strings.ToLower(level) {
case "debug":
return LevelDebug
case "info":
return LevelInfo
case "warn":
return LevelWarn
case "error":
return LevelError
default:
return LevelInfo
}
}