starlog/observer_test.go
2026-03-19 16:37:57 +08:00

175 lines
4.6 KiB
Go

package starlog
import (
"bytes"
"context"
"strings"
"sync/atomic"
"testing"
"time"
)
func waitObserverCount(t *testing.T, observer *Observer, want int, timeout time.Duration) {
t.Helper()
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
if observer != nil && observer.Count() >= want {
return
}
time.Sleep(5 * time.Millisecond)
}
got := 0
if observer != nil {
got = observer.Count()
}
t.Fatalf("observer count timeout, want >= %d got %d", want, got)
}
func waitObserverCondition(t *testing.T, timeout time.Duration, cond func() bool, reason string) {
t.Helper()
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
if cond() {
return
}
time.Sleep(5 * time.Millisecond)
}
t.Fatalf("observer condition timeout: %s", reason)
}
func TestObserverCollectsEntries(t *testing.T) {
defer StopStacks()
var buf bytes.Buffer
logger := newStructuredTestLogger(&buf)
observer := NewObserver()
logger.AppendEntryHandler(observer)
logger.WithField("user_id", 42).Info("login ok")
waitObserverCount(t, observer, 1, 300*time.Millisecond)
entries := observer.Entries()
if len(entries) == 0 {
t.Fatalf("observer should collect entries")
}
last := entries[len(entries)-1]
if last.Message != "login ok" {
t.Fatalf("unexpected observed message: %q", last.Message)
}
if got, ok := last.Fields["user_id"]; !ok || got != 42 {
t.Fatalf("unexpected observed fields: %+v", last.Fields)
}
}
func TestObserverLimitAndDropped(t *testing.T) {
defer StopStacks()
observer := NewObserverWithLimit(2)
logger := newStructuredTestLogger(&bytes.Buffer{})
logger.AppendEntryHandler(observer)
logger.Info("one")
logger.Info("two")
logger.Info("three")
waitObserverCondition(t, 400*time.Millisecond, func() bool {
if observer.Dropped() == 0 {
return false
}
entries := observer.Entries()
if len(entries) != 2 {
return false
}
return entries[0].Message == "two" && entries[1].Message == "three"
}, "observer limit should keep newest two entries")
entries := observer.Entries()
if len(entries) != 2 {
t.Fatalf("observer should keep only limited entries, got %d", len(entries))
}
if entries[0].Message != "two" || entries[1].Message != "three" {
t.Fatalf("observer should keep newest entries, got %q %q", entries[0].Message, entries[1].Message)
}
if observer.Dropped() == 0 {
t.Fatalf("observer dropped count should increase when over limit")
}
}
func TestObserverTakeAllAndReset(t *testing.T) {
defer StopStacks()
observer := NewObserver()
logger := newStructuredTestLogger(&bytes.Buffer{})
logger.AppendEntryHandler(observer)
logger.Info("a")
logger.Info("b")
waitObserverCount(t, observer, 2, 300*time.Millisecond)
all := observer.TakeAll()
if len(all) != 2 {
t.Fatalf("take all should return all collected entries, got %d", len(all))
}
if observer.Count() != 0 {
t.Fatalf("take all should clear observer entries")
}
logger.Info("c")
waitObserverCount(t, observer, 1, 300*time.Millisecond)
observer.Reset()
if observer.Count() != 0 || observer.Dropped() != 0 {
t.Fatalf("reset should clear observer state")
}
}
func TestTestHookAttachAndRestore(t *testing.T) {
defer StopStacks()
var buf bytes.Buffer
logger := newStructuredTestLogger(&buf)
var previousCount uint64
logger.SetEntryHandler(HandlerFunc(func(_ context.Context, _ *Entry) error {
atomic.AddUint64(&previousCount, 1)
return nil
}))
hook := NewTestHook(logger)
if hook == nil {
t.Fatalf("new test hook should not be nil")
}
logger.Info("hooked")
waitObserverCount(t, hook.Observer(), 1, 300*time.Millisecond)
if atomic.LoadUint64(&previousCount) == 0 {
t.Fatalf("test hook should keep previous handler in chain")
}
last, ok := hook.Last()
if !ok || last.Message != "hooked" {
t.Fatalf("unexpected hook last entry: ok=%v msg=%q", ok, last.Message)
}
if !hook.Close() {
t.Fatalf("hook close should restore previous handler when unchanged")
}
before := hook.Count()
logger.Info("after-close")
time.Sleep(30 * time.Millisecond)
if hook.Count() != before {
t.Fatalf("closed hook should stop collecting new entries")
}
if atomic.LoadUint64(&previousCount) < 2 {
t.Fatalf("previous handler should continue working after hook close")
}
if !strings.Contains(buf.String(), "after-close") {
t.Fatalf("logger output should still contain logs after hook close")
}
}
func TestTestHookCloseWhenHandlerReplaced(t *testing.T) {
defer StopStacks()
logger := newStructuredTestLogger(&bytes.Buffer{})
hook := NewTestHook(logger)
logger.SetEntryHandler(nil)
if hook.Close() {
t.Fatalf("hook close should fail when handler replaced externally")
}
}