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) } }