- internal/logger/ring.go: Entry{time,level,source,msg} + RingBuffer(2000,满覆盖最旧,不阻塞)+ringHandler(slog旁路,Enabled委托内层,sourceOf提取source=再回落Scoped); Options.Ring非nil才包
- gateway/observability_logs.go: GET /admin/observability/logs?level=&limit= (writeAuth owner/admin) → {data:[{time(RFC3339 UTC),level,source,msg}]} 最新在前; level阈值过滤(warn=warn+error,已标注取舍); limit缺省200上限1000; 未知level400/未注入501
- routes.go注册 + observability.go SetLogBuffer(atomic.Pointer) + cmd/cyrene main.go/wireGatewayLogBuffer接线(logger先于gateway)
- 控制台/文件输出零变化; 记录只源自已产出日志,不额外打印敏感
- Docs/api-client v0.35; 测试(ring 10 + observability_logs 8 + cmd端到端2); build/vet/test绿(56)+win/darwin交叉编译
185 lines
4.7 KiB
Go
185 lines
4.7 KiB
Go
package logger
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"log/slog"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestRingBuffer_BoundedDropsOldest(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ring := NewRingBuffer(3)
|
|
for i := 0; i < 5; i++ {
|
|
ring.Append(Entry{Time: time.Unix(int64(i), 0), Level: "info", Msg: string(rune('a' + i))})
|
|
}
|
|
|
|
if got := ring.Len(); got != 3 {
|
|
t.Fatalf("Len() = %d, want 3 (bounded)", got)
|
|
}
|
|
got := ring.Snapshot(0)
|
|
if len(got) != 3 {
|
|
t.Fatalf("Snapshot len = %d, want 3", len(got))
|
|
}
|
|
want := []string{"e", "d", "c"} // newest first, a/b evicted
|
|
for i, msg := range want {
|
|
if got[i].Msg != msg {
|
|
t.Fatalf("Snapshot[%d].Msg = %q, want %q (got %v)", i, got[i].Msg, msg, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRingBuffer_SnapshotLimit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ring := NewRingBuffer(5)
|
|
for i := 0; i < 5; i++ {
|
|
ring.Append(Entry{Level: "info", Msg: string(rune('a' + i))})
|
|
}
|
|
|
|
if got := ring.Snapshot(2); len(got) != 2 || got[0].Msg != "e" || got[1].Msg != "d" {
|
|
t.Fatalf("Snapshot(2) = %v, want newest two [e d]", got)
|
|
}
|
|
if got := ring.Snapshot(-1); len(got) != 5 {
|
|
t.Fatalf("Snapshot(-1) len = %d, want all 5", len(got))
|
|
}
|
|
}
|
|
|
|
func TestRingBuffer_DefaultCapacityOnNonPositive(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ring := NewRingBuffer(0)
|
|
if got := cap(ring.buf); got != DefaultRingCapacity {
|
|
t.Fatalf("capacity = %d, want default %d", got, DefaultRingCapacity)
|
|
}
|
|
}
|
|
|
|
func TestNew_RingCapturesSourceLevelAndMsg(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var out bytes.Buffer
|
|
ring := NewRingBuffer(8)
|
|
log := New(Options{Level: "debug", Format: "text", Out: &out, Ring: ring})
|
|
WithSource(log, "runtime/loop").Warn("agent stalled", "step", 3)
|
|
|
|
entries := ring.Snapshot(0)
|
|
if len(entries) != 1 {
|
|
t.Fatalf("captured %d entries, want 1", len(entries))
|
|
}
|
|
e := entries[0]
|
|
if e.Level != "warn" {
|
|
t.Fatalf("level = %q, want warn", e.Level)
|
|
}
|
|
if e.Source != "runtime/loop" {
|
|
t.Fatalf("source = %q, want runtime/loop", e.Source)
|
|
}
|
|
if e.Msg != "agent stalled" {
|
|
t.Fatalf("msg = %q, want %q", e.Msg, "agent stalled")
|
|
}
|
|
if e.Time.IsZero() {
|
|
t.Fatal("time is zero, want the record timestamp")
|
|
}
|
|
// The wrapped output is unchanged: the ring is a pure side channel.
|
|
if !strings.Contains(out.String(), "level=WARN") || !strings.Contains(out.String(), "msg=\"agent stalled\"") {
|
|
t.Fatalf("inner output changed, got %q", out.String())
|
|
}
|
|
}
|
|
|
|
func TestNew_RingExtractsScopedSource(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ring := NewRingBuffer(8)
|
|
log := New(Options{Level: "debug", Format: "text", Out: io.Discard, Ring: ring})
|
|
Scoped(log, "channel", "adapter").Info("connected")
|
|
|
|
entries := ring.Snapshot(0)
|
|
if len(entries) != 1 || entries[0].Source != "channel/adapter" {
|
|
t.Fatalf("entries = %v, want one with source channel/adapter", entries)
|
|
}
|
|
}
|
|
|
|
func TestNew_RingCapturesLevelsNewestFirst(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ring := NewRingBuffer(8)
|
|
log := New(Options{Level: "debug", Format: "text", Out: io.Discard, Ring: ring})
|
|
log.Debug("d")
|
|
log.Info("i")
|
|
log.Warn("w")
|
|
log.Error("e")
|
|
|
|
entries := ring.Snapshot(0)
|
|
if len(entries) != 4 {
|
|
t.Fatalf("captured %d entries, want 4", len(entries))
|
|
}
|
|
wantLevels := []string{"error", "warn", "info", "debug"}
|
|
wantMsgs := []string{"e", "w", "i", "d"}
|
|
for i := range wantLevels {
|
|
if entries[i].Level != wantLevels[i] || entries[i].Msg != wantMsgs[i] {
|
|
t.Fatalf("entries[%d] = %+v, want level=%s msg=%s", i, entries[i], wantLevels[i], wantMsgs[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNew_RingHonorsConfiguredLevel(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ring := NewRingBuffer(8)
|
|
log := New(Options{Level: "warn", Format: "text", Out: io.Discard, Ring: ring})
|
|
log.Info("dropped")
|
|
log.Warn("kept")
|
|
|
|
entries := ring.Snapshot(0)
|
|
if len(entries) != 1 || entries[0].Msg != "kept" {
|
|
t.Fatalf("entries = %v, want only the warn record", entries)
|
|
}
|
|
}
|
|
|
|
func TestNew_NilRingIsTransparent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var out bytes.Buffer
|
|
log := New(Options{Level: "debug", Format: "text", Out: &out})
|
|
log.Info("hello")
|
|
if !strings.Contains(out.String(), "msg=hello") {
|
|
t.Fatalf("logger without a ring stopped emitting, got %q", out.String())
|
|
}
|
|
}
|
|
|
|
func TestRingBuffer_NilReceiverIsSafe(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var ring *RingBuffer
|
|
ring.Append(Entry{Msg: "x"})
|
|
if got := ring.Snapshot(0); got != nil {
|
|
t.Fatalf("Snapshot on nil = %v, want nil", got)
|
|
}
|
|
if got := ring.Len(); got != 0 {
|
|
t.Fatalf("Len on nil = %d, want 0", got)
|
|
}
|
|
}
|
|
|
|
func TestLevelName(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
in slog.Level
|
|
want string
|
|
}{
|
|
{slog.LevelDebug, "debug"},
|
|
{slog.LevelInfo, "info"},
|
|
{slog.LevelWarn, "warn"},
|
|
{slog.LevelError, "error"},
|
|
{slog.LevelError + 4, "error"},
|
|
{slog.LevelDebug - 4, "debug"},
|
|
}
|
|
for _, tt := range tests {
|
|
if got := levelName(tt.in); got != tt.want {
|
|
t.Fatalf("levelName(%v) = %q, want %q", tt.in, got, tt.want)
|
|
}
|
|
}
|
|
}
|