restructured logger from budigt

This commit is contained in:
juancwu 2026-04-25 20:35:03 +00:00
commit b2fd12b1c8
11 changed files with 1227 additions and 1 deletions

219
logger_test.go Normal file
View file

@ -0,0 +1,219 @@
package splinter
import (
"bytes"
"context"
"encoding/json"
"errors"
"sync"
"testing"
)
type memoryStream struct {
mu sync.Mutex
records []Record
level Level
}
func newMemoryStream(level Level) *memoryStream {
return &memoryStream{level: level}
}
func (m *memoryStream) Name() string { return "memory" }
func (m *memoryStream) Write(_ context.Context, rec Record) error {
m.mu.Lock()
defer m.mu.Unlock()
m.records = append(m.records, rec)
return nil
}
func (m *memoryStream) Enabled(level Level) bool { return level >= m.level }
func (m *memoryStream) Close() error { return nil }
func (m *memoryStream) len() int {
m.mu.Lock()
defer m.mu.Unlock()
return len(m.records)
}
func (m *memoryStream) last() Record {
m.mu.Lock()
defer m.mu.Unlock()
return m.records[len(m.records)-1]
}
type failingStream struct{}
func (f *failingStream) Name() string { return "failing" }
func (f *failingStream) Write(_ context.Context, _ Record) error { return errors.New("always fails") }
func (f *failingStream) Enabled(_ Level) bool { return true }
func (f *failingStream) Close() error { return nil }
func TestLogger_LevelFiltering(t *testing.T) {
mem := newMemoryStream(LevelWarn)
logger := New(WithStream(mem))
logger.Debug("d")
logger.Info("i")
logger.Warn("w")
logger.Error("e")
if mem.len() != 2 {
t.Fatalf("expected 2 records (Warn+Error), got %d", mem.len())
}
}
func TestLogger_BaseAttrs(t *testing.T) {
mem := newMemoryStream(LevelDebug)
logger := New(WithStream(mem), WithAttrs(map[string]any{"service": "api"}))
logger.Info("request")
if mem.last().Attrs["service"] != "api" {
t.Errorf("expected service=api, got %v", mem.last().Attrs["service"])
}
}
func TestLogger_With(t *testing.T) {
mem := newMemoryStream(LevelDebug)
logger := New(WithStream(mem))
child := logger.With(map[string]any{"request_id": "abc"})
child.Info("handled")
if mem.last().Attrs["request_id"] != "abc" {
t.Errorf("expected request_id=abc, got %v", mem.last().Attrs["request_id"])
}
}
func TestLogger_InlineArgsOverrideBase(t *testing.T) {
mem := newMemoryStream(LevelDebug)
logger := New(WithStream(mem), WithAttrs(map[string]any{"env": "prod"}))
logger.Info("override", "env", "staging")
if mem.last().Attrs["env"] != "staging" {
t.Errorf("expected staging, got %v", mem.last().Attrs["env"])
}
}
func TestLogger_FanOut(t *testing.T) {
a := newMemoryStream(LevelDebug)
b := newMemoryStream(LevelDebug)
logger := New(WithStream(a), WithStream(b))
logger.Info("fanout")
if a.len() != 1 || b.len() != 1 {
t.Errorf("expected both streams to receive 1 record, got %d and %d", a.len(), b.len())
}
}
func TestLogger_ErrorHandler(t *testing.T) {
var captured string
logger := New(
WithStream(&failingStream{}),
WithErrorHandler(func(stream string, _ Record, err error) {
captured = stream + ": " + err.Error()
}),
)
logger.Info("trigger")
if captured != "failing: always fails" {
t.Errorf("expected handler to fire, got %q", captured)
}
}
func TestLogger_DefaultsToConsoleStream(t *testing.T) {
logger := New()
if len(logger.streams) != 1 {
t.Fatalf("expected 1 default stream, got %d", len(logger.streams))
}
if _, ok := logger.streams[0].(*ConsoleStream); !ok {
t.Errorf("expected default to be *ConsoleStream, got %T", logger.streams[0])
}
}
func TestPackageFuncs_RouteThroughDefault(t *testing.T) {
mem := newMemoryStream(LevelDebug)
prev := SetDefault(New(WithStream(mem)))
defer SetDefault(prev)
Debug("d")
Info("i")
Warn("w")
Error("e")
if mem.len() != 4 {
t.Fatalf("expected 4 records via package funcs, got %d", mem.len())
}
}
func TestSetDefault_ReturnsPrevious(t *testing.T) {
memA := newMemoryStream(LevelDebug)
memB := newMemoryStream(LevelDebug)
loggerA := New(WithStream(memA))
loggerB := New(WithStream(memB))
original := SetDefault(loggerA)
t.Cleanup(func() { SetDefault(original) })
prev := SetDefault(loggerB)
if prev != loggerA {
t.Errorf("expected SetDefault to return loggerA, got %p", prev)
}
Info("via B")
if memA.len() != 0 {
t.Errorf("expected memA to be empty after swap, got %d", memA.len())
}
if memB.len() != 1 {
t.Errorf("expected memB to have 1 record, got %d", memB.len())
}
}
func TestLevelFromString(t *testing.T) {
tests := []struct {
in string
want Level
}{
{"debug", LevelDebug},
{"DEBUG", LevelDebug},
{"info", LevelInfo},
{"warn", LevelWarn},
{"error", LevelError},
{"bogus", LevelInfo},
}
for _, tt := range tests {
if got := LevelFromString(tt.in); got != tt.want {
t.Errorf("LevelFromString(%q) = %v, want %v", tt.in, got, tt.want)
}
}
}
func TestRecord_LevelLabel(t *testing.T) {
tests := []struct {
level Level
want string
}{
{LevelDebug, "DEBUG"},
{LevelInfo, "INFO"},
{LevelWarn, "WARN"},
{LevelError, "ERROR"},
}
for _, tt := range tests {
r := Record{Level: tt.level}
if got := r.LevelLabel(); got != tt.want {
t.Errorf("LevelLabel(%v) = %q, want %q", tt.level, got, tt.want)
}
}
}
// Sanity: serialising a Record through encoding/json works for callers who
// build their own streams from scratch.
func TestRecord_JSONShape(t *testing.T) {
r := Record{Message: "hi", Attrs: map[string]any{"k": 1}}
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(r); err != nil {
t.Fatalf("encode: %v", err)
}
if !bytes.Contains(buf.Bytes(), []byte(`"Message":"hi"`)) {
t.Errorf("missing Message in JSON: %s", buf.String())
}
}