feat(logger): add runtime log level control API
Add independent log level control for stderr and OTLP loggers. Both can be configured via environment variables or programmatically at runtime. - Add SetLevel() and SetOTLPLevel() for runtime control - Add ParseLevel() to convert strings to slog.Level - Support LOG_LEVEL and OTLP_LOG_LEVEL env vars - Maintain backward compatibility with DEBUG env var - Add comprehensive test coverage
This commit is contained in:
235
logger/level_test.go
Normal file
235
logger/level_test.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestParseLevel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected slog.Level
|
||||
expectError bool
|
||||
}{
|
||||
{"empty string", "", slog.LevelInfo, false},
|
||||
{"DEBUG upper", "DEBUG", slog.LevelDebug, false},
|
||||
{"debug lower", "debug", slog.LevelDebug, false},
|
||||
{"INFO upper", "INFO", slog.LevelInfo, false},
|
||||
{"info lower", "info", slog.LevelInfo, false},
|
||||
{"WARN upper", "WARN", slog.LevelWarn, false},
|
||||
{"warn lower", "warn", slog.LevelWarn, false},
|
||||
{"ERROR upper", "ERROR", slog.LevelError, false},
|
||||
{"error lower", "error", slog.LevelError, false},
|
||||
{"invalid level", "invalid", slog.LevelInfo, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
level, err := ParseLevel(tt.input)
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
t.Errorf("expected error for input %q, got nil", tt.input)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error for input %q: %v", tt.input, err)
|
||||
}
|
||||
if level != tt.expected {
|
||||
t.Errorf("expected level %v for input %q, got %v", tt.expected, tt.input, level)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetLevel(t *testing.T) {
|
||||
// Store original level to restore later
|
||||
originalLevel := Level.Level()
|
||||
defer Level.Set(originalLevel)
|
||||
|
||||
SetLevel(slog.LevelDebug)
|
||||
if Level.Level() != slog.LevelDebug {
|
||||
t.Errorf("expected Level to be Debug, got %v", Level.Level())
|
||||
}
|
||||
|
||||
SetLevel(slog.LevelError)
|
||||
if Level.Level() != slog.LevelError {
|
||||
t.Errorf("expected Level to be Error, got %v", Level.Level())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetOTLPLevel(t *testing.T) {
|
||||
// Store original level to restore later
|
||||
originalLevel := OTLPLevel.Level()
|
||||
defer OTLPLevel.Set(originalLevel)
|
||||
|
||||
SetOTLPLevel(slog.LevelWarn)
|
||||
if OTLPLevel.Level() != slog.LevelWarn {
|
||||
t.Errorf("expected OTLPLevel to be Warn, got %v", OTLPLevel.Level())
|
||||
}
|
||||
|
||||
SetOTLPLevel(slog.LevelDebug)
|
||||
if OTLPLevel.Level() != slog.LevelDebug {
|
||||
t.Errorf("expected OTLPLevel to be Debug, got %v", OTLPLevel.Level())
|
||||
}
|
||||
}
|
||||
|
||||
func TestOTLPLevelHandler(t *testing.T) {
|
||||
// Create a mock handler that counts calls
|
||||
callCount := 0
|
||||
mockHandler := &mockHandler{
|
||||
handleFunc: func(ctx context.Context, r slog.Record) error {
|
||||
callCount++
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Set OTLP level to Warn
|
||||
originalLevel := OTLPLevel.Level()
|
||||
defer OTLPLevel.Set(originalLevel)
|
||||
OTLPLevel.Set(slog.LevelWarn)
|
||||
|
||||
// Create OTLP level handler
|
||||
handler := newOTLPLevelHandler(mockHandler)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test that Debug and Info are filtered out
|
||||
if handler.Enabled(ctx, slog.LevelDebug) {
|
||||
t.Error("Debug level should be disabled when OTLP level is Warn")
|
||||
}
|
||||
if handler.Enabled(ctx, slog.LevelInfo) {
|
||||
t.Error("Info level should be disabled when OTLP level is Warn")
|
||||
}
|
||||
|
||||
// Test that Warn and Error are enabled
|
||||
if !handler.Enabled(ctx, slog.LevelWarn) {
|
||||
t.Error("Warn level should be enabled when OTLP level is Warn")
|
||||
}
|
||||
if !handler.Enabled(ctx, slog.LevelError) {
|
||||
t.Error("Error level should be enabled when OTLP level is Warn")
|
||||
}
|
||||
|
||||
// Test that Handle respects level filtering
|
||||
now := time.Now()
|
||||
debugRecord := slog.NewRecord(now, slog.LevelDebug, "debug message", 0)
|
||||
warnRecord := slog.NewRecord(now, slog.LevelWarn, "warn message", 0)
|
||||
|
||||
handler.Handle(ctx, debugRecord)
|
||||
if callCount != 0 {
|
||||
t.Error("Debug record should not be passed to underlying handler")
|
||||
}
|
||||
|
||||
handler.Handle(ctx, warnRecord)
|
||||
if callCount != 1 {
|
||||
t.Error("Warn record should be passed to underlying handler")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvironmentVariables(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
envVar string
|
||||
envValue string
|
||||
configPrefix string
|
||||
testFunc func(t *testing.T)
|
||||
}{
|
||||
{
|
||||
name: "LOG_LEVEL sets stderr level",
|
||||
envVar: "LOG_LEVEL",
|
||||
envValue: "ERROR",
|
||||
testFunc: func(t *testing.T) {
|
||||
// Reset the setup state
|
||||
resetLoggerSetup()
|
||||
|
||||
// Call setupStdErrHandler which should read the env var
|
||||
handler := setupStdErrHandler()
|
||||
if handler == nil {
|
||||
t.Fatal("setupStdErrHandler returned nil")
|
||||
}
|
||||
|
||||
if Level.Level() != slog.LevelError {
|
||||
t.Errorf("expected Level to be Error after setting LOG_LEVEL=ERROR, got %v", Level.Level())
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Prefixed LOG_LEVEL",
|
||||
envVar: "TEST_LOG_LEVEL",
|
||||
envValue: "DEBUG",
|
||||
configPrefix: "TEST",
|
||||
testFunc: func(t *testing.T) {
|
||||
ConfigPrefix = "TEST"
|
||||
defer func() { ConfigPrefix = "" }()
|
||||
|
||||
resetLoggerSetup()
|
||||
handler := setupStdErrHandler()
|
||||
if handler == nil {
|
||||
t.Fatal("setupStdErrHandler returned nil")
|
||||
}
|
||||
|
||||
if Level.Level() != slog.LevelDebug {
|
||||
t.Errorf("expected Level to be Debug after setting TEST_LOG_LEVEL=DEBUG, got %v", Level.Level())
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Store original env value and level
|
||||
originalEnv := os.Getenv(tt.envVar)
|
||||
originalLevel := Level.Level()
|
||||
defer func() {
|
||||
os.Setenv(tt.envVar, originalEnv)
|
||||
Level.Set(originalLevel)
|
||||
}()
|
||||
|
||||
// Set test environment variable
|
||||
os.Setenv(tt.envVar, tt.envValue)
|
||||
|
||||
// Run the test
|
||||
tt.testFunc(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// mockHandler is a simple mock implementation of slog.Handler for testing
|
||||
type mockHandler struct {
|
||||
handleFunc func(ctx context.Context, r slog.Record) error
|
||||
}
|
||||
|
||||
func (m *mockHandler) Enabled(ctx context.Context, level slog.Level) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *mockHandler) Handle(ctx context.Context, r slog.Record) error {
|
||||
if m.handleFunc != nil {
|
||||
return m.handleFunc(ctx, r)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *mockHandler) WithGroup(name string) slog.Handler {
|
||||
return m
|
||||
}
|
||||
|
||||
// resetLoggerSetup resets the sync.Once instances for testing
|
||||
func resetLoggerSetup() {
|
||||
// Reset package-level variables
|
||||
textLogger = nil
|
||||
otlpLogger = nil
|
||||
multiLogger = nil
|
||||
|
||||
// Note: We can't easily reset sync.Once instances in tests,
|
||||
// but for the specific test we're doing (environment variable parsing)
|
||||
// we can test the setupStdErrHandler function directly
|
||||
}
|
Reference in New Issue
Block a user