Files
Logging-System/logsys/logger_test.go
T
weichenandCursor bed0dbf076
ci / vet-and-test (push) Successful in 4s
ci / docker-checkout-hostgateway (push) Successful in 2s
Add a structured logger and collector so runs can be traced across processes.
Sync and async sinks, redaction, and a queryable HTTP service keep concurrent logs tied to a trace id.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-29 13:51:16 +00:00

135 lines
3.4 KiB
Go

package logsys
import (
"context"
"errors"
"strings"
"sync"
"testing"
"time"
)
type memSink struct {
mu sync.Mutex
entries []Entry
}
func (m *memSink) Write(e Entry) error {
m.mu.Lock()
defer m.mu.Unlock()
m.entries = append(m.entries, e)
return nil
}
func (m *memSink) Sync() error { return nil }
func (m *memSink) Close() error { return nil }
func (m *memSink) list() []Entry {
m.mu.Lock()
defer m.mu.Unlock()
out := make([]Entry, len(m.entries))
copy(out, m.entries)
return out
}
type stubErr struct{}
func (stubErr) Error() string { return "dial failed" }
func (stubErr) ErrorCode() string { return "agent.provider_unavailable" }
func (stubErr) ErrorFields() map[string]any {
return map[string]any{"model": "glm-4", "reason": "模型不可达"}
}
func TestLevelFilterModuleAndTrace(t *testing.T) {
mem := &memSink{}
fixed := time.Date(2026, 9, 29, 12, 0, 0, 0, time.UTC)
log := New(Options{
Service: "agent-squad",
Level: LevelInfo,
ModuleLevels: map[string]Level{
"engine": LevelDebug,
"server": LevelWarn,
},
Sinks: []Sink{mem},
Now: func() time.Time { return fixed },
})
log.Debug("hidden")
log.Info("visible")
log.Module("engine").Debug("engine-debug")
log.Module("server").Info("server-info")
log.Module("server").Warn("server-warn")
ctx := WithTrace(context.Background(), "trace-1")
log.WithContext(ctx).With("step", "plan").Info("traced")
got := mem.list()
if len(got) != 4 {
t.Fatalf("len %d %+v", len(got), got)
}
if got[0].Message != "visible" || got[0].Time != fixed || got[0].Service != "agent-squad" {
t.Fatalf("first %+v", got[0])
}
if got[1].Module != "engine" || got[1].Message != "engine-debug" {
t.Fatalf("engine %+v", got[1])
}
if got[2].Message != "server-warn" {
t.Fatalf("server %+v", got[2])
}
if got[3].TraceID != "trace-1" || got[3].Fields["step"] != "plan" {
t.Fatalf("trace %+v", got[3])
}
}
func TestErrorDuckTypingAndRedact(t *testing.T) {
mem := &memSink{}
log := New(Options{
Level: LevelInfo,
Sinks: []Sink{mem},
Redact: func(s string) string { return strings.ReplaceAll(s, "glm-4", "***") },
})
log.Error("provider down", stubErr{}, "attempt", 2)
got := mem.list()
if len(got) != 1 {
t.Fatal(len(got))
}
e := got[0]
if e.Code != "agent.provider_unavailable" || e.Level != "error" {
t.Fatalf("%+v", e)
}
if e.Fields["model"] != "***" || e.Fields["reason"] != "模型不可达" || e.Fields["attempt"] != 2 {
t.Fatalf("fields %+v", e.Fields)
}
if !strings.Contains(e.Error, "dial failed") {
t.Fatal(e.Error)
}
plain := errors.New("boom")
log.Error("plain", plain)
if mem.list()[1].Code != "" || mem.list()[1].Error != "boom" {
t.Fatalf("%+v", mem.list()[1])
}
}
func TestDefaultLogger(t *testing.T) {
prev := Default()
t.Cleanup(func() { SetDefault(prev) })
mem := &memSink{}
SetDefault(New(Options{Level: LevelInfo, Sinks: []Sink{mem}}))
Default().Info("from-default")
if err := Sync(); err != nil {
t.Fatal(err)
}
if len(mem.list()) != 1 {
t.Fatal(mem.list())
}
}
func TestHumanAndJSON(t *testing.T) {
e := Entry{Time: time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC), Level: "info", Service: "svc", Message: "hello", Fields: map[string]any{"b": 1, "a": "x"}}
line := FormatHuman(e)
if !strings.Contains(line, "INFO svc hello") || !strings.Contains(line, "a=x") {
t.Fatal(line)
}
raw, err := FormatJSON(e)
if err != nil || !strings.Contains(string(raw), `"msg":"hello"`) {
t.Fatalf("%s %v", raw, err)
}
}