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
279 lines
6.3 KiB
Go
279 lines
6.3 KiB
Go
package logging
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestSetupStdout(t *testing.T) {
|
|
file, err := Setup("stdout")
|
|
if err != nil {
|
|
t.Fatalf("Setup(stdout) failed: %v", err)
|
|
}
|
|
if file != nil {
|
|
t.Error("Expected nil file for stdout")
|
|
}
|
|
}
|
|
|
|
func TestSetupStderr(t *testing.T) {
|
|
file, err := Setup("stderr")
|
|
if err != nil {
|
|
t.Fatalf("Setup(stderr) failed: %v", err)
|
|
}
|
|
if file != nil {
|
|
t.Error("Expected nil file for stderr")
|
|
}
|
|
}
|
|
|
|
func TestSetupEmpty(t *testing.T) {
|
|
file, err := Setup("")
|
|
if err != nil {
|
|
t.Fatalf("Setup('') failed: %v", err)
|
|
}
|
|
if file != nil {
|
|
t.Error("Expected nil file for empty string")
|
|
}
|
|
}
|
|
|
|
func TestSetupFilePath(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
logPath := filepath.Join(tmpDir, "test.log")
|
|
|
|
file, err := Setup(logPath)
|
|
if err != nil {
|
|
t.Fatalf("Setup(file) failed: %v", err)
|
|
}
|
|
if file == nil {
|
|
t.Fatal("Expected non-nil file for file path")
|
|
}
|
|
defer file.Close()
|
|
|
|
// Verify file was created
|
|
if _, err := os.Stat(logPath); os.IsNotExist(err) {
|
|
t.Error("Expected log file to be created")
|
|
}
|
|
}
|
|
|
|
func TestSetupInvalidPath(t *testing.T) {
|
|
_, err := Setup("/nonexistent/directory/path/test.log")
|
|
if err == nil {
|
|
t.Error("Expected error for invalid path")
|
|
}
|
|
}
|
|
|
|
func TestNewLoggerDefaultLevel(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := NewLogger("", &buf)
|
|
|
|
if logger.level != LevelInfo {
|
|
t.Errorf("Expected default level Info, got %d", logger.level)
|
|
}
|
|
}
|
|
|
|
func TestNewLoggerInvalidLevel(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := NewLogger("invalid", &buf)
|
|
|
|
if logger.level != LevelInfo {
|
|
t.Errorf("Expected default level Info for invalid input, got %d", logger.level)
|
|
}
|
|
}
|
|
|
|
func TestNewLoggerDebugLevel(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := NewLogger("debug", &buf)
|
|
|
|
if logger.level != LevelDebug {
|
|
t.Errorf("Expected level Debug, got %d", logger.level)
|
|
}
|
|
}
|
|
|
|
func TestNewLoggerWarnLevel(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := NewLogger("warn", &buf)
|
|
|
|
if logger.level != LevelWarn {
|
|
t.Errorf("Expected level Warn, got %d", logger.level)
|
|
}
|
|
}
|
|
|
|
func TestNewLoggerErrorLevel(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := NewLogger("error", &buf)
|
|
|
|
if logger.level != LevelError {
|
|
t.Errorf("Expected level Error, got %d", logger.level)
|
|
}
|
|
}
|
|
|
|
func TestLoggerDebugOutputAtDebugLevel(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := NewLogger("debug", &buf)
|
|
|
|
logger.Debug("test message")
|
|
output := buf.String()
|
|
|
|
if !strings.Contains(output, "[DEBUG]") {
|
|
t.Errorf("Expected [DEBUG] prefix, got: %s", output)
|
|
}
|
|
if !strings.Contains(output, "test message") {
|
|
t.Errorf("Expected 'test message' in output, got: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestLoggerDebugSuppressedAtInfoLevel(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := NewLogger("info", &buf)
|
|
|
|
logger.Debug("should not appear")
|
|
output := buf.String()
|
|
|
|
if output != "" {
|
|
t.Errorf("Expected no output for debug at info level, got: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestLoggerInfoOutputAtInfoLevel(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := NewLogger("info", &buf)
|
|
|
|
logger.Info("info message")
|
|
output := buf.String()
|
|
|
|
if !strings.Contains(output, "[INFO]") {
|
|
t.Errorf("Expected [INFO] prefix, got: %s", output)
|
|
}
|
|
if !strings.Contains(output, "info message") {
|
|
t.Errorf("Expected 'info message' in output, got: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestLoggerInfoSuppressedAtWarnLevel(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := NewLogger("warn", &buf)
|
|
|
|
logger.Info("should not appear")
|
|
output := buf.String()
|
|
|
|
if output != "" {
|
|
t.Errorf("Expected no output for info at warn level, got: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestLoggerWarnOutputAtWarnLevel(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := NewLogger("warn", &buf)
|
|
|
|
logger.Warn("warn message")
|
|
output := buf.String()
|
|
|
|
if !strings.Contains(output, "[WARN]") {
|
|
t.Errorf("Expected [WARN] prefix, got: %s", output)
|
|
}
|
|
if !strings.Contains(output, "warn message") {
|
|
t.Errorf("Expected 'warn message' in output, got: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestLoggerWarnSuppressedAtErrorLevel(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := NewLogger("error", &buf)
|
|
|
|
logger.Warn("should not appear")
|
|
output := buf.String()
|
|
|
|
if output != "" {
|
|
t.Errorf("Expected no output for warn at error level, got: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestLoggerErrorOutputAtErrorLevel(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := NewLogger("error", &buf)
|
|
|
|
logger.Error("error message")
|
|
output := buf.String()
|
|
|
|
if !strings.Contains(output, "[ERROR]") {
|
|
t.Errorf("Expected [ERROR] prefix, got: %s", output)
|
|
}
|
|
if !strings.Contains(output, "error message") {
|
|
t.Errorf("Expected 'error message' in output, got: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestLoggerErrorAlwaysOutputs(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := NewLogger("error", &buf)
|
|
|
|
logger.Error("critical failure")
|
|
output := buf.String()
|
|
|
|
if !strings.Contains(output, "critical failure") {
|
|
t.Errorf("Expected error message in output, got: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestLoggerDebugLevelAllMessages(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := NewLogger("debug", &buf)
|
|
|
|
logger.Debug("d")
|
|
logger.Info("i")
|
|
logger.Warn("w")
|
|
logger.Error("e")
|
|
|
|
output := buf.String()
|
|
if !strings.Contains(output, "[DEBUG]") {
|
|
t.Error("Expected [DEBUG] in output")
|
|
}
|
|
if !strings.Contains(output, "[INFO]") {
|
|
t.Error("Expected [INFO] in output")
|
|
}
|
|
if !strings.Contains(output, "[WARN]") {
|
|
t.Error("Expected [WARN] in output")
|
|
}
|
|
if !strings.Contains(output, "[ERROR]") {
|
|
t.Error("Expected [ERROR] in output")
|
|
}
|
|
}
|
|
|
|
func TestLoggerFormatf(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := NewLogger("info", &buf)
|
|
|
|
logger.Infof("count: %d", 42)
|
|
output := buf.String()
|
|
|
|
if !strings.Contains(output, "count: 42") {
|
|
t.Errorf("Expected formatted output 'count: 42', got: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestLoggerLevelCaseInsensitive(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
|
|
logger := NewLogger("DEBUG", &buf)
|
|
if logger.level != LevelDebug {
|
|
t.Errorf("Expected level Debug for 'DEBUG', got %d", logger.level)
|
|
}
|
|
|
|
logger = NewLogger("Info", &buf)
|
|
if logger.level != LevelInfo {
|
|
t.Errorf("Expected level Info for 'Info', got %d", logger.level)
|
|
}
|
|
|
|
logger = NewLogger("WARN", &buf)
|
|
if logger.level != LevelWarn {
|
|
t.Errorf("Expected level Warn for 'WARN', got %d", logger.level)
|
|
}
|
|
|
|
logger = NewLogger("ERROR", &buf)
|
|
if logger.level != LevelError {
|
|
t.Errorf("Expected level Error for 'ERROR', got %d", logger.level)
|
|
}
|
|
}
|