starlog/multi_sink_test.go

222 lines
6.3 KiB
Go
Raw Permalink Normal View History

2026-03-19 16:37:57 +08:00
package starlog
import (
"bytes"
"errors"
"sync/atomic"
"testing"
)
type sinkAlwaysFail struct{}
func (sink *sinkAlwaysFail) Write(data []byte) error {
_ = data
return errors.New("sink failed")
}
func (sink *sinkAlwaysFail) Close() error {
return nil
}
type sinkToggleFail struct {
fail uint32
}
func (sink *sinkToggleFail) SetFail(fail bool) {
if fail {
atomic.StoreUint32(&sink.fail, 1)
return
}
atomic.StoreUint32(&sink.fail, 0)
}
func (sink *sinkToggleFail) Write(data []byte) error {
_ = data
if atomic.LoadUint32(&sink.fail) == 1 {
return errors.New("toggle sink write failed")
}
return nil
}
func (sink *sinkToggleFail) Close() error {
if atomic.LoadUint32(&sink.fail) == 1 {
return errors.New("toggle sink close failed")
}
return nil
}
func TestMultiSinkWritesAll(t *testing.T) {
var a bytes.Buffer
var b bytes.Buffer
multi := NewMultiSink(NewWriterSink(&a), NewWriterSink(&b))
if err := multi.Write([]byte("hello")); err != nil {
t.Fatalf("multi sink write should succeed, got %v", err)
}
if a.String() != "hello" || b.String() != "hello" {
t.Fatalf("all sinks should receive data, got a=%q b=%q", a.String(), b.String())
}
}
func TestMultiSinkContinueOnError(t *testing.T) {
var out bytes.Buffer
multi := NewMultiSink(&sinkAlwaysFail{}, NewWriterSink(&out))
if err := multi.Write([]byte("x")); err == nil {
t.Fatalf("write should return error when one sink fails")
}
if out.String() != "x" {
t.Fatalf("healthy sink should still receive data when continueOnError=true, got %q", out.String())
}
}
func TestMultiSinkStopOnError(t *testing.T) {
var out bytes.Buffer
multi := NewMultiSink(&sinkAlwaysFail{}, NewWriterSink(&out))
multi.SetContinueOnError(false)
if err := multi.Write([]byte("x")); err == nil {
t.Fatalf("write should return error in stop-on-error mode")
}
if out.String() != "" {
t.Fatalf("later sinks should not run when continueOnError=false, got %q", out.String())
}
}
func TestLoggerSetSinks(t *testing.T) {
var a bytes.Buffer
var b bytes.Buffer
logger := NewStarlog(nil)
logger.SetShowStd(false)
logger.SetShowColor(false)
logger.SetShowOriginFile(false)
logger.SetShowFuncName(false)
logger.SetShowFlag(false)
logger.SetSinks(NewWriterSink(&a), NewWriterSink(&b))
logger.Info("fanout")
if !bytes.Contains(a.Bytes(), []byte("fanout")) || !bytes.Contains(b.Bytes(), []byte("fanout")) {
t.Fatalf("logger should write to all configured sinks")
}
}
func TestMultiSinkStatsPerSink(t *testing.T) {
var out bytes.Buffer
toggle := &sinkToggleFail{}
multi := NewMultiSink(toggle, NewWriterSink(&out))
if err := multi.Write([]byte("a")); err != nil {
t.Fatalf("first write should succeed, got %v", err)
}
stats := multi.GetStats()
if len(stats.Sinks) != 2 {
t.Fatalf("expected 2 sink stats, got %d", len(stats.Sinks))
}
first := stats.Sinks[0]
second := stats.Sinks[1]
if first.Writes != 1 || first.WriteErrors != 0 || first.State != SinkStateHealthy {
t.Fatalf("unexpected first sink stats after success: %+v", first)
}
if second.Writes != 1 || second.WriteErrors != 0 || second.State != SinkStateHealthy {
t.Fatalf("unexpected second sink stats after success: %+v", second)
}
toggle.SetFail(true)
if err := multi.Write([]byte("b")); err == nil {
t.Fatalf("write should fail when toggle sink is failing")
}
stats = multi.GetStats()
first = stats.Sinks[0]
second = stats.Sinks[1]
if first.Writes != 2 || first.WriteErrors != 1 {
t.Fatalf("unexpected first sink counters after failure: %+v", first)
}
if first.ConsecutiveWriteErrors == 0 || first.State != SinkStateDegraded {
t.Fatalf("first sink should become degraded on failure: %+v", first)
}
if first.LastWriteError == "" {
t.Fatalf("first sink should record last write error")
}
if second.Writes != 2 || second.WriteErrors != 0 || second.State != SinkStateHealthy {
t.Fatalf("healthy sink should continue receiving writes: %+v", second)
}
}
func TestMultiSinkStatsRecoveryAndReset(t *testing.T) {
toggle := &sinkToggleFail{}
multi := NewMultiSink(toggle)
toggle.SetFail(true)
if err := multi.Write([]byte("x")); err == nil {
t.Fatalf("write should fail when sink is failing")
}
toggle.SetFail(false)
if err := multi.Write([]byte("y")); err != nil {
t.Fatalf("write should recover when sink becomes healthy, got %v", err)
}
stats := multi.GetStats()
if len(stats.Sinks) != 1 {
t.Fatalf("expected 1 sink stats, got %d", len(stats.Sinks))
}
first := stats.Sinks[0]
if first.Writes != 2 || first.WriteErrors != 1 {
t.Fatalf("unexpected sink counters after recovery: %+v", first)
}
if first.ConsecutiveWriteErrors != 0 {
t.Fatalf("consecutive write errors should reset after recovery: %+v", first)
}
if first.State != SinkStateRecovered {
t.Fatalf("sink should be recovered after success following failures: %+v", first)
}
multi.ResetStats()
stats = multi.GetStats()
first = stats.Sinks[0]
if first.Writes != 0 || first.WriteErrors != 0 || first.Closes != 0 || first.CloseErrors != 0 {
t.Fatalf("reset should clear sink counters: %+v", first)
}
if first.LastWriteError != "" || first.LastCloseError != "" {
t.Fatalf("reset should clear last errors: %+v", first)
}
if first.State != SinkStateHealthy {
t.Fatalf("reset should set healthy state: %+v", first)
}
}
func TestMultiSinkCloseStats(t *testing.T) {
toggle := &sinkToggleFail{}
multi := NewMultiSink(toggle)
toggle.SetFail(true)
if err := multi.Close(); err == nil {
t.Fatalf("close should fail when sink close fails")
}
stats := multi.GetStats()
if len(stats.Sinks) != 1 {
t.Fatalf("expected 1 sink stats, got %d", len(stats.Sinks))
}
first := stats.Sinks[0]
if first.Closes != 1 || first.CloseErrors != 1 {
t.Fatalf("unexpected close counters after failure: %+v", first)
}
if first.ConsecutiveCloseErrors == 0 || first.State != SinkStateDegraded {
t.Fatalf("sink should become degraded on close failure: %+v", first)
}
toggle.SetFail(false)
if err := multi.Close(); err != nil {
t.Fatalf("close should recover when sink becomes healthy, got %v", err)
}
stats = multi.GetStats()
first = stats.Sinks[0]
if first.Closes != 2 || first.CloseErrors != 1 {
t.Fatalf("unexpected close counters after recovery: %+v", first)
}
if first.ConsecutiveCloseErrors != 0 || first.State != SinkStateRecovered {
t.Fatalf("close error state should recover after success: %+v", first)
}
}