Pulse/internal/logging/logging_test.go
rcourtman 7d422d2909 feat: add professional logging with runtime configuration and performance optimization
Implements structured logging package with LOG_LEVEL/LOG_FORMAT env support, debug level guards for hot paths, enriched error messages with actionable context, and stack trace capture for production debugging. Improves observability and reduces log overhead in high-frequency polling loops.
2025-10-20 15:13:38 +00:00

476 lines
11 KiB
Go

package logging
import (
"bytes"
"context"
"encoding/json"
"io"
"os"
"reflect"
"strings"
"sync"
"testing"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func resetLoggingState() {
mu.Lock()
defer mu.Unlock()
baseWriter = os.Stderr
baseComponent = ""
baseLogger = zerolog.New(baseWriter).With().Timestamp().Logger()
log.Logger = baseLogger
zerolog.TimeFieldFormat = defaultTimeFmt
zerolog.SetGlobalLevel(zerolog.InfoLevel)
}
func readJSONLine(t *testing.T, buf *bytes.Buffer) map[string]interface{} {
t.Helper()
line := strings.TrimSpace(buf.String())
if idx := strings.IndexByte(line, '\n'); idx >= 0 {
line = line[:idx]
}
if line == "" {
t.Fatalf("expected log output, got empty string")
}
var event map[string]interface{}
if err := json.Unmarshal([]byte(line), &event); err != nil {
t.Fatalf("failed to unmarshal log line: %v", err)
}
return event
}
func TestInitJSONFormatSetsLevelAndComponent(t *testing.T) {
t.Cleanup(resetLoggingState)
Init(Config{
Format: "json",
Level: "debug",
Component: "apiserver",
})
mu.RLock()
defer mu.RUnlock()
if baseWriter != os.Stderr {
t.Fatalf("expected base writer to be os.Stderr, got %#v", baseWriter)
}
if zerolog.GlobalLevel() != zerolog.DebugLevel {
t.Fatalf("expected global level debug, got %s", zerolog.GlobalLevel())
}
if baseComponent != "apiserver" {
t.Fatalf("expected base component apiserver, got %s", baseComponent)
}
if !reflect.DeepEqual(log.Logger, baseLogger) {
t.Fatal("expected global log.Logger to match baseLogger")
}
}
func TestInitConsoleFormatUsesConsoleWriter(t *testing.T) {
t.Cleanup(resetLoggingState)
Init(Config{
Format: "console",
Level: "info",
})
mu.RLock()
defer mu.RUnlock()
if _, ok := baseWriter.(zerolog.ConsoleWriter); !ok {
t.Fatalf("expected console writer, got %#v", baseWriter)
}
}
func TestInitAutoFormatWithPipe(t *testing.T) {
t.Cleanup(resetLoggingState)
origStderr := os.Stderr
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("failed to create pipe: %v", err)
}
os.Stderr = w
defer func() {
os.Stderr = origStderr
_ = r.Close()
_ = w.Close()
}()
Init(Config{
Format: "auto",
Level: "info",
})
mu.RLock()
defer mu.RUnlock()
if baseWriter != w {
t.Fatalf("expected base writer to use provided pipe, got %#v", baseWriter)
}
}
func TestNewLoggerWithComponentAndFields(t *testing.T) {
t.Cleanup(resetLoggingState)
Init(Config{
Format: "json",
Level: "info",
Component: "root",
})
var buf bytes.Buffer
logger := New("worker", WithWriter(&buf), WithFields(map[string]interface{}{
"request": "sync",
}))
logger.Info().Msg("processing")
event := readJSONLine(t, &buf)
if event["component"] != "worker" {
t.Fatalf("expected component worker, got %v", event["component"])
}
if event["request"] != "sync" {
t.Fatalf("expected request field, got %v", event["request"])
}
if event["level"] != "info" {
t.Fatalf("expected level info, got %v", event["level"])
}
if event["message"] != "processing" {
t.Fatalf("expected message processing, got %v", event["message"])
}
}
func TestNewLoggerInheritsComponentWhenEmpty(t *testing.T) {
t.Cleanup(resetLoggingState)
Init(Config{
Format: "json",
Level: "info",
Component: "core",
})
var buf bytes.Buffer
logger := New("", WithWriter(&buf))
logger.Warn().Msg("warn")
event := readJSONLine(t, &buf)
if event["component"] != "core" {
t.Fatalf("expected inherited component core, got %v", event["component"])
}
}
func TestNewLoggerWithCaller(t *testing.T) {
t.Cleanup(resetLoggingState)
Init(Config{
Format: "json",
Level: "info",
})
var buf bytes.Buffer
logger := New("svc", WithWriter(&buf), WithCaller())
logger.Error().Msg("boom")
event := readJSONLine(t, &buf)
caller, ok := event["caller"].(string)
if !ok || !strings.Contains(caller, "logging_test.go") {
t.Fatalf("expected caller information, got %v", event["caller"])
}
}
func TestNewLoggerWithCustomWriter(t *testing.T) {
t.Cleanup(resetLoggingState)
Init(Config{
Format: "json",
Level: "info",
})
var buf bytes.Buffer
logger := New("custom", WithWriter(&buf))
logger.Info().Msg("hello")
if buf.Len() == 0 {
t.Fatal("expected output on custom writer")
}
}
func TestContextHelpersWithRequestID(t *testing.T) {
t.Cleanup(resetLoggingState)
Init(Config{
Format: "json",
Level: "info",
})
ctx := context.Background()
ctx, generated := WithRequestID(ctx, "")
if generated == "" {
t.Fatal("expected generated request id")
}
if got := GetRequestID(ctx); got != generated {
t.Fatalf("expected stored request id %s, got %s", generated, got)
}
var buf bytes.Buffer
logger := New("api", WithWriter(&buf))
ctx = WithLogger(ctx, logger)
info := FromContext(ctx)
info.Info().Msg("ctx-log")
event := readJSONLine(t, &buf)
if event["request_id"] != generated {
t.Fatalf("expected request_id %s, got %v", generated, event["request_id"])
}
}
func TestContextHelpersWithExistingLogger(t *testing.T) {
t.Cleanup(resetLoggingState)
Init(Config{
Format: "json",
Level: "debug",
})
var buf bytes.Buffer
base := New("svc", WithWriter(&buf))
ctx := WithLogger(context.Background(), base)
ctx, id := WithRequestID(ctx, "custom-123")
logger := FromContext(ctx)
logger.Debug().Msg("debug")
event := readJSONLine(t, &buf)
if event["component"] != "svc" {
t.Fatalf("expected component svc, got %v", event["component"])
}
if event["request_id"] != "custom-123" {
t.Fatalf("expected request_id custom-123, got %v", event["request_id"])
}
if event["level"] != "debug" {
t.Fatalf("expected level debug, got %v", event["level"])
}
if id != "custom-123" {
t.Fatalf("expected returned id to match input, got %s", id)
}
}
func TestWithLoggerNilContext(t *testing.T) {
t.Cleanup(resetLoggingState)
Init(Config{})
ctx := WithLogger(nil, New("svc", WithWriter(io.Discard)))
if ctx == nil {
t.Fatal("expected non-nil context")
}
}
func TestWithRequestIDTrimsWhitespace(t *testing.T) {
t.Cleanup(resetLoggingState)
Init(Config{})
ctx, id := WithRequestID(context.Background(), " ")
if id == "" {
t.Fatal("expected generated id for whitespace input")
}
if GetRequestID(ctx) != id {
t.Fatalf("expected context request id %s, got %s", id, GetRequestID(ctx))
}
}
func TestFromContextWithoutRequestID(t *testing.T) {
t.Cleanup(resetLoggingState)
Init(Config{
Format: "json",
Level: "info",
})
var buf bytes.Buffer
mu.Lock()
baseLogger = zerolog.New(&buf).With().Timestamp().Logger()
baseWriter = &buf
baseComponent = ""
log.Logger = baseLogger
mu.Unlock()
base := FromContext(context.Background())
base.Info().Msg("no-request")
event := readJSONLine(t, &buf)
if _, ok := event["request_id"]; ok {
t.Fatalf("did not expect request_id, got %v", event["request_id"])
}
}
func TestNewLoggerWithoutComponentOmitsField(t *testing.T) {
t.Cleanup(resetLoggingState)
Init(Config{
Format: "json",
Level: "info",
})
var buf bytes.Buffer
logger := New("", WithWriter(&buf))
logger.Info().Msg("no-component")
event := readJSONLine(t, &buf)
if _, exists := event["component"]; exists {
t.Fatalf("did not expect component field, got %v", event["component"])
}
}
func TestInitThreadSafety(t *testing.T) {
t.Cleanup(resetLoggingState)
var wg sync.WaitGroup
configs := []Config{
{Format: "json", Level: "debug", Component: "worker"},
{Format: "json", Level: "warn", Component: "api"},
}
for i := 0; i < 50; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
Init(configs[idx%len(configs)])
}(i)
}
wg.Wait()
mu.RLock()
defer mu.RUnlock()
// Ensure baseLogger is valid and global logger matches it.
if reflect.DeepEqual(baseLogger, zerolog.Logger{}) {
t.Fatal("expected initialized base logger")
}
if !reflect.DeepEqual(log.Logger, baseLogger) {
t.Fatal("expected global log.Logger to match baseLogger after concurrent init")
}
}
func TestInitFromConfigWithDefaults(t *testing.T) {
t.Cleanup(resetLoggingState)
logger, err := InitFromConfig(context.Background(), Config{
Component: "test",
})
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if reflect.DeepEqual(logger, zerolog.Logger{}) {
t.Fatal("expected initialized logger")
}
if zerolog.GlobalLevel() != zerolog.InfoLevel {
t.Fatalf("expected default info level, got %s", zerolog.GlobalLevel())
}
}
func TestInitFromConfigWithEnvOverrides(t *testing.T) {
t.Cleanup(resetLoggingState)
t.Cleanup(func() {
os.Unsetenv("LOG_LEVEL")
os.Unsetenv("LOG_FORMAT")
})
os.Setenv("LOG_LEVEL", "debug")
os.Setenv("LOG_FORMAT", "json")
logger, err := InitFromConfig(context.Background(), Config{
Level: "info",
Format: "console",
Component: "test",
})
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if reflect.DeepEqual(logger, zerolog.Logger{}) {
t.Fatal("expected initialized logger")
}
// Env override should set debug level
if zerolog.GlobalLevel() != zerolog.DebugLevel {
t.Fatalf("expected debug level from env override, got %s", zerolog.GlobalLevel())
}
// Format should be JSON (from env)
mu.RLock()
defer mu.RUnlock()
if _, ok := baseWriter.(zerolog.ConsoleWriter); ok {
t.Fatal("expected JSON writer from env override, got console writer")
}
}
func TestInitFromConfigInvalidLevel(t *testing.T) {
t.Cleanup(resetLoggingState)
_, err := InitFromConfig(context.Background(), Config{
Level: "invalid",
Format: "json",
})
if err == nil {
t.Fatal("expected error for invalid level")
}
if !strings.Contains(err.Error(), "invalid log level") {
t.Fatalf("expected invalid level error, got %v", err)
}
}
func TestInitFromConfigInvalidFormat(t *testing.T) {
t.Cleanup(resetLoggingState)
_, err := InitFromConfig(context.Background(), Config{
Level: "info",
Format: "invalid",
})
if err == nil {
t.Fatal("expected error for invalid format")
}
if !strings.Contains(err.Error(), "invalid log format") {
t.Fatalf("expected invalid format error, got %v", err)
}
}
func TestIsLevelEnabled(t *testing.T) {
t.Cleanup(resetLoggingState)
// Set global level to Info
zerolog.SetGlobalLevel(zerolog.InfoLevel)
if !IsLevelEnabled(zerolog.InfoLevel) {
t.Fatal("expected info level to be enabled")
}
if !IsLevelEnabled(zerolog.WarnLevel) {
t.Fatal("expected warn level to be enabled")
}
if !IsLevelEnabled(zerolog.ErrorLevel) {
t.Fatal("expected error level to be enabled")
}
if IsLevelEnabled(zerolog.DebugLevel) {
t.Fatal("expected debug level to be disabled")
}
// Change to debug level
zerolog.SetGlobalLevel(zerolog.DebugLevel)
if !IsLevelEnabled(zerolog.DebugLevel) {
t.Fatal("expected debug level to be enabled after setting global level")
}
}