433 lines
10 KiB
Go
433 lines
10 KiB
Go
package starlog
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestWriteBufferFlushAfterSwitchingOff(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := NewStarlog(&buf)
|
|
logger.SetShowStd(false)
|
|
logger.SetShowColor(false)
|
|
|
|
logger.SetSwitching(true)
|
|
logger.Infoln("first")
|
|
logger.Infoln("second")
|
|
|
|
if got := buf.String(); strings.Contains(got, "first") || strings.Contains(got, "second") {
|
|
t.Fatalf("logs should stay buffered while switching=true, got: %q", got)
|
|
}
|
|
|
|
logger.SetSwitching(false)
|
|
got := buf.String()
|
|
if !strings.Contains(got, "first") || !strings.Contains(got, "second") {
|
|
t.Fatalf("buffered logs were not flushed after switching=false, got: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestAsyncQueuePushFailureFallsBackToSyncHandler(t *testing.T) {
|
|
resetAsyncMetricsForTest()
|
|
defer func() {
|
|
resetAsyncMetricsForTest()
|
|
stackMu.Lock()
|
|
if stacks != nil {
|
|
_ = stacks.Close()
|
|
}
|
|
stackStarted = false
|
|
stacks = nil
|
|
stackStopChan = nil
|
|
stackDoneChan = nil
|
|
stackMu.Unlock()
|
|
}()
|
|
|
|
logger := NewStarlog(nil)
|
|
logger.SetShowStd(false)
|
|
var handled uint64
|
|
logger.handlerFunc = func(data LogData) {
|
|
atomic.AddUint64(&handled, 1)
|
|
}
|
|
|
|
alertCalled := make(chan struct{}, 1)
|
|
SetAsyncErrorHandler(func(err error, data LogData) {
|
|
select {
|
|
case alertCalled <- struct{}{}:
|
|
default:
|
|
}
|
|
})
|
|
|
|
stackMu.Lock()
|
|
stackStarted = true
|
|
stacks = nil
|
|
stackStopChan = nil
|
|
stackDoneChan = nil
|
|
stackMu.Unlock()
|
|
|
|
logger.Infoln("trigger async fallback")
|
|
|
|
if atomic.LoadUint64(&handled) != 1 {
|
|
t.Fatalf("handler should be called once via sync fallback, got: %d", handled)
|
|
}
|
|
if GetAsyncDropCount() == 0 {
|
|
t.Fatalf("async drop counter should increase on push failure")
|
|
}
|
|
select {
|
|
case <-alertCalled:
|
|
default:
|
|
t.Fatalf("async alert handler should be invoked on push failure")
|
|
}
|
|
}
|
|
|
|
func TestStarChanStackPushClosedReturnsEOF(t *testing.T) {
|
|
stack := newStarChanStack(1)
|
|
if err := stack.Close(); err != nil {
|
|
t.Fatalf("Close failed: %v", err)
|
|
}
|
|
if err := stack.Push("x"); err != io.EOF {
|
|
t.Fatalf("Push on closed stack should return io.EOF, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestStarChanStackTryPushFullReturnsQueueFull(t *testing.T) {
|
|
stack := newStarChanStack(1)
|
|
if err := stack.Push("first"); err != nil {
|
|
t.Fatalf("Push failed: %v", err)
|
|
}
|
|
if err := stack.TryPush("second"); !errors.Is(err, errStackFull) {
|
|
t.Fatalf("TryPush on full queue should return errStackFull, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPendingWriteLimitDropOldest(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := NewStarlog(&buf)
|
|
logger.SetShowStd(false)
|
|
logger.SetShowColor(false)
|
|
logger.SetPendingWriteLimit(2)
|
|
logger.SetPendingDropPolicy(PendingDropOldest)
|
|
|
|
logger.SetSwitching(true)
|
|
logger.Infoln("one")
|
|
logger.Infoln("two")
|
|
logger.Infoln("three")
|
|
logger.SetSwitching(false)
|
|
|
|
got := buf.String()
|
|
if strings.Contains(got, "one") {
|
|
t.Fatalf("oldest pending log should be dropped, got %q", got)
|
|
}
|
|
if !strings.Contains(got, "two") || !strings.Contains(got, "three") {
|
|
t.Fatalf("newer pending logs should remain, got %q", got)
|
|
}
|
|
if logger.GetPendingDropCount() != 1 {
|
|
t.Fatalf("expected one dropped pending write, got %d", logger.GetPendingDropCount())
|
|
}
|
|
}
|
|
|
|
func TestPendingWriteLimitBlockPolicy(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := NewStarlog(&buf)
|
|
logger.SetShowStd(false)
|
|
logger.SetShowColor(false)
|
|
logger.SetPendingWriteLimit(1)
|
|
logger.SetPendingDropPolicy(PendingBlock)
|
|
logger.SetSwitching(true)
|
|
logger.Infoln("one")
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
logger.Infoln("two")
|
|
close(done)
|
|
}()
|
|
|
|
blocked := false
|
|
deadline := time.Now().Add(200 * time.Millisecond)
|
|
for time.Now().Before(deadline) {
|
|
select {
|
|
case <-done:
|
|
t.Fatalf("block policy should wait while queue is full and switching=true")
|
|
default:
|
|
}
|
|
if logger.GetPendingBlockCount() > 0 {
|
|
blocked = true
|
|
break
|
|
}
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
if !blocked {
|
|
t.Fatalf("expected pending block count to increase")
|
|
}
|
|
|
|
logger.SetSwitching(false)
|
|
|
|
select {
|
|
case <-done:
|
|
case <-time.After(300 * time.Millisecond):
|
|
t.Fatalf("blocked write should continue after switching=false")
|
|
}
|
|
|
|
got := buf.String()
|
|
if !strings.Contains(got, "one") || !strings.Contains(got, "two") {
|
|
t.Fatalf("both logs should be persisted, got %q", got)
|
|
}
|
|
if logger.GetPendingDropCount() != 0 {
|
|
t.Fatalf("block policy should avoid drops, got %d", logger.GetPendingDropCount())
|
|
}
|
|
stats := logger.GetPendingStats()
|
|
if stats.BlockCount == 0 {
|
|
t.Fatalf("pending stats should expose block count")
|
|
}
|
|
}
|
|
|
|
func TestPendingStatsSnapshot(t *testing.T) {
|
|
logger := NewStarlog(nil)
|
|
logger.SetShowStd(false)
|
|
logger.SetPendingWriteLimit(3)
|
|
logger.SetPendingDropPolicy(PendingDropNewest)
|
|
logger.SetSwitching(true)
|
|
logger.Infoln("one")
|
|
logger.Infoln("two")
|
|
|
|
stats := logger.GetPendingStats()
|
|
if stats.Limit != 3 {
|
|
t.Fatalf("unexpected limit: %d", stats.Limit)
|
|
}
|
|
if stats.Length != 2 {
|
|
t.Fatalf("unexpected pending length: %d", stats.Length)
|
|
}
|
|
if stats.Policy != PendingDropNewest {
|
|
t.Fatalf("unexpected policy: %v", stats.Policy)
|
|
}
|
|
if !stats.Switching {
|
|
t.Fatalf("expected switching flag true in stats snapshot")
|
|
}
|
|
if stats.PeakLength < 2 {
|
|
t.Fatalf("expected peak length >= 2, got %d", stats.PeakLength)
|
|
}
|
|
|
|
logger.SetSwitching(false)
|
|
}
|
|
|
|
type errWriter struct{}
|
|
|
|
func (w *errWriter) Write(p []byte) (int, error) {
|
|
return 0, errors.New("write failed")
|
|
}
|
|
|
|
func TestWriteErrorObservable(t *testing.T) {
|
|
resetAsyncMetricsForTest()
|
|
defer resetAsyncMetricsForTest()
|
|
|
|
logger := NewStarlog(&errWriter{})
|
|
logger.SetShowStd(false)
|
|
logger.SetShowColor(false)
|
|
|
|
observed := make(chan struct{}, 1)
|
|
SetWriteErrorHandler(func(err error, data LogData) {
|
|
if err != nil {
|
|
select {
|
|
case observed <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
})
|
|
|
|
logger.Infoln("write error check")
|
|
|
|
if GetWriteErrorCount() == 0 {
|
|
t.Fatalf("write error count should increase")
|
|
}
|
|
select {
|
|
case <-observed:
|
|
default:
|
|
t.Fatalf("write error handler should be invoked")
|
|
}
|
|
}
|
|
|
|
func TestAsyncHandlerPanicDoesNotCrash(t *testing.T) {
|
|
resetAsyncMetricsForTest()
|
|
defer func() {
|
|
resetAsyncMetricsForTest()
|
|
StopStacks()
|
|
}()
|
|
|
|
logger := NewStarlog(nil)
|
|
logger.SetShowStd(false)
|
|
logger.SetHandler(func(LogData) {
|
|
panic("boom")
|
|
})
|
|
|
|
logger.Infoln("panic safe")
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
if GetAsyncDropCount() == 0 {
|
|
t.Fatalf("panic in async handler should be reported as drop")
|
|
}
|
|
}
|
|
|
|
func TestEntryHandlerTimeoutFallback(t *testing.T) {
|
|
resetAsyncMetricsForTest()
|
|
defer func() {
|
|
resetAsyncMetricsForTest()
|
|
StopStacks()
|
|
}()
|
|
|
|
logger := NewStarlog(nil)
|
|
logger.SetShowStd(false)
|
|
logger.SetEntryHandler(HandlerFunc(func(context.Context, *Entry) error {
|
|
time.Sleep(80 * time.Millisecond)
|
|
return nil
|
|
}))
|
|
logger.SetEntryHandlerTimeout(10 * time.Millisecond)
|
|
|
|
begin := time.Now()
|
|
logger.Infoln("entry timeout")
|
|
cost := time.Since(begin)
|
|
|
|
if cost > 60*time.Millisecond {
|
|
t.Fatalf("entry handler timeout should protect main path, took %v", cost)
|
|
}
|
|
deadline := time.Now().Add(300 * time.Millisecond)
|
|
for time.Now().Before(deadline) {
|
|
if GetAsyncDropCount() > 0 {
|
|
return
|
|
}
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
t.Fatalf("entry handler timeout should be observable via async drop count")
|
|
}
|
|
|
|
func TestEntryHandlerQueueFullNoFallbackDoesNotBlock(t *testing.T) {
|
|
resetAsyncMetricsForTest()
|
|
defer func() {
|
|
resetAsyncMetricsForTest()
|
|
stackMu.Lock()
|
|
stackStarted = false
|
|
stacks = nil
|
|
stackStopChan = nil
|
|
stackDoneChan = nil
|
|
stackMu.Unlock()
|
|
}()
|
|
|
|
SetAsyncFallbackToSync(false)
|
|
|
|
logger := NewStarlog(nil)
|
|
logger.SetShowStd(false)
|
|
var handled uint64
|
|
logger.SetEntryHandler(HandlerFunc(func(context.Context, *Entry) error {
|
|
atomic.AddUint64(&handled, 1)
|
|
time.Sleep(80 * time.Millisecond)
|
|
return nil
|
|
}))
|
|
|
|
stackMu.Lock()
|
|
stackStarted = true
|
|
stacks = newStarChanStack(1)
|
|
stackStopChan = nil
|
|
stackDoneChan = nil
|
|
stackMu.Unlock()
|
|
|
|
if err := stacks.Push(logTransfer{
|
|
handlerFunc: func(LogData) {
|
|
time.Sleep(80 * time.Millisecond)
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("prepare full async queue failed: %v", err)
|
|
}
|
|
|
|
begin := time.Now()
|
|
logger.Infoln("entry queue full")
|
|
cost := time.Since(begin)
|
|
|
|
if cost > 60*time.Millisecond {
|
|
t.Fatalf("entry handler should not block when queue is full, took %v", cost)
|
|
}
|
|
if atomic.LoadUint64(&handled) != 0 {
|
|
t.Fatalf("entry handler should be dropped when queue is full and fallback disabled, got %d", handled)
|
|
}
|
|
if GetAsyncDropCount() == 0 {
|
|
t.Fatalf("queue-full drop should be observable")
|
|
}
|
|
}
|
|
|
|
func TestWriteMethodNameCompatibility(t *testing.T) {
|
|
logger := NewStarlog(nil)
|
|
logger.StopWrite()
|
|
if !logger.IsWriteStopped() || !logger.IsWriteStoed() {
|
|
t.Fatalf("both write-stop getters should report true")
|
|
}
|
|
logger.EnableWrite()
|
|
if logger.IsWriteStopped() || logger.IsWriteStoed() {
|
|
t.Fatalf("EnableWrite should resume writer for both getter names")
|
|
}
|
|
}
|
|
|
|
func TestLevelFilterAPI(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := NewStarlog(&buf)
|
|
logger.SetShowStd(false)
|
|
logger.SetShowColor(false)
|
|
logger.SetShowOriginFile(false)
|
|
logger.SetShowFuncName(false)
|
|
logger.SetShowFlag(false)
|
|
|
|
if !logger.IsLevelEnabled(LvDebug) {
|
|
t.Fatalf("debug level should be enabled by default")
|
|
}
|
|
logger.SetLevel(LvWarning)
|
|
if logger.GetLevel() != LvWarning {
|
|
t.Fatalf("unexpected level threshold: %d", logger.GetLevel())
|
|
}
|
|
if logger.IsLevelEnabled(LvInfo) {
|
|
t.Fatalf("info should be filtered by warning threshold")
|
|
}
|
|
if !logger.IsLevelEnabled(LvError) {
|
|
t.Fatalf("error should be enabled by warning threshold")
|
|
}
|
|
|
|
logger.Infoln("filtered")
|
|
logger.Warningln("visible")
|
|
logStr := buf.String()
|
|
if strings.Contains(logStr, "filtered") {
|
|
t.Fatalf("info log should be filtered, got %q", logStr)
|
|
}
|
|
if !strings.Contains(logStr, "visible") {
|
|
t.Fatalf("warning log should remain, got %q", logStr)
|
|
}
|
|
}
|
|
|
|
func TestParseLevel(t *testing.T) {
|
|
tests := map[string]int{
|
|
"debug": LvDebug,
|
|
"INFO": LvInfo,
|
|
"notice": LvNotice,
|
|
"warn": LvWarning,
|
|
"warning": LvWarning,
|
|
"err": LvError,
|
|
"critical": LvCritical,
|
|
"panic": LvPanic,
|
|
"fatal": LvFatal,
|
|
"7": 7,
|
|
}
|
|
for input, expected := range tests {
|
|
parsed, err := ParseLevel(input)
|
|
if err != nil {
|
|
t.Fatalf("ParseLevel(%q) returned error: %v", input, err)
|
|
}
|
|
if parsed != expected {
|
|
t.Fatalf("ParseLevel(%q)=%d, want %d", input, parsed, expected)
|
|
}
|
|
}
|
|
|
|
_, err := ParseLevel("unknown-level")
|
|
if !errors.Is(err, ErrInvalidLevel) {
|
|
t.Fatalf("ParseLevel invalid input should return ErrInvalidLevel, got %v", err)
|
|
}
|
|
}
|