gcli2api / internal /reqlog /reqlog_test.go
a3216's picture
chore: 同步到上游 1.12.0-panel + 凭证同步/独立启动器/保活
6d60378 verified
Raw History Blame Contribute Delete
8.79 kB
package reqlog
import (
"os"
"path/filepath"
"testing"
"time"
)
func TestRecorderMetricsAndRecent(t *testing.T) {
r := New(Config{})
r.Begin()
r.Record(Event{RequestID: "ok", Status: 200, OK: true, Outcome: OutcomeSuccess, DurationMs: 10})
r.Begin()
r.Record(Event{RequestID: "bad", Status: 429, OK: false, Outcome: OutcomeHTTPError, DurationMs: 30})
s := r.Snapshot()
if s.Completed != 2 || s.InFlight != 0 || s.Succeeded != 1 || s.Failed != 1 {
t.Fatalf("counts = %+v", s)
}
if s.SuccessRate != 50 || s.HTTPSuccessRate != 50 || s.AvgDurationMs != 20 {
t.Fatalf("rates = success:%v http:%v avg:%v", s.SuccessRate, s.HTTPSuccessRate, s.AvgDurationMs)
}
if len(s.Recent) != 2 || s.Recent[0].RequestID != "bad" || s.Recent[1].RequestID != "ok" {
t.Fatalf("recent = %+v, want newest first", s.Recent)
}
}
func TestArchiveRotationReadAndFilter(t *testing.T) {
dir := t.TempDir()
r := New(Config{Enabled: true, Dir: dir, FileMaxBytes: 120, MaxBytes: 1 << 20, RetentionDays: 7})
base := time.Now().Add(-time.Minute)
for i := 0; i < 12; i++ {
r.Record(Event{
Time: base.Add(time.Duration(i) * time.Second),
RequestID: "multi-" + string(rune('a'+i)),
Model: "glm-5.3",
Account: "账号(uid8)",
Status: 200,
OK: true,
Outcome: OutcomeSuccess,
DurationMs: int64(i + 1),
})
}
r.Record(Event{
Time: base.Add(20 * time.Second),
RequestID: "other",
Model: "other-model",
Status: 500,
Outcome: OutcomeHTTPError,
DurationMs: 99,
})
r.Close()
stats := r.Snapshot().Archive
if !stats.Enabled || stats.Files < 2 || stats.Bytes == 0 || stats.DroppedWrites != 0 {
t.Fatalf("archive stats = %+v", stats)
}
rows, err := r.ReadArchive(5, Filter{Model: "glm"})
if err != nil {
t.Fatal(err)
}
if len(rows) != 5 || rows[0].RequestID != "multi-l" || rows[4].RequestID != "multi-h" {
t.Fatalf("filtered rows = %+v", rows)
}
}
// ReadArchive 的契约是「按事件时间倒序返回最近记录」,与归档文件的 mtime 无关。
// 快速轮转(FileMaxBytes 很小)时同一秒内会写出多个文件,Linux 下这些文件的 mtime
// 可能完全相同;把归档目录整体拷贝 / 恢复备份也会打乱 mtime。此用例把所有归档文件的
// mtime 改成与事件时间相反的顺序,钉住「只能按事件时间排序」。
func TestReadArchiveOrdersByEventTimeNotFileMtime(t *testing.T) {
dir := t.TempDir()
r := New(Config{Enabled: true, Dir: dir, FileMaxBytes: 1, MaxBytes: 1 << 20, RetentionDays: 7})
base := time.Now().Add(-time.Hour)
const n = 5
for i := 0; i < n; i++ {
r.Record(Event{
Time: base.Add(time.Duration(i) * time.Second),
RequestID: "ev-" + string(rune('a'+i)),
Model: "glm-5.3",
Status: 200,
OK: true,
Outcome: OutcomeSuccess,
})
}
r.Close()
// FileMaxBytes=1 让每个事件独占一个归档文件:基准文件 requests-<day>.jsonl 装的是
// 最早的事件,字典序却排在 requests-<day>.N.jsonl 之后。按 os.ReadDir 顺序递增地
// 设置 mtime,最早事件所在文件就拿到最大 mtime、被放在最后读取。
entries, err := os.ReadDir(dir)
if err != nil {
t.Fatal(err)
}
if len(entries) != n {
t.Fatalf("归档文件数 = %d, want %d", len(entries), n)
}
stamp := time.Unix(1700000000, 0)
for i, e := range entries {
ts := stamp.Add(time.Duration(i) * time.Second)
if err := os.Chtimes(filepath.Join(dir, e.Name()), ts, ts); err != nil {
t.Fatal(err)
}
}
rows, err := r.ReadArchive(n, Filter{Model: "glm"})
if err != nil {
t.Fatal(err)
}
got := make([]string, 0, len(rows))
for _, e := range rows {
got = append(got, e.RequestID)
}
if len(rows) != n {
t.Fatalf("rows = %d, want %d", len(rows), n)
}
for i := range rows {
want := "ev-" + string(rune('a'+n-1-i))
if rows[i].RequestID != want {
t.Fatalf("rows[%d] = %s, want %s(须按事件时间倒序,与文件 mtime 无关)\n got %v", i, rows[i].RequestID, want, got)
}
}
}
// 来源字段随事件落盘并可被 client_ip / user_agent 过滤(包含匹配、大小写不敏感)。
func TestArchiveFilterByClientInfo(t *testing.T) {
dir := t.TempDir()
r := New(Config{Enabled: true, Dir: dir, MaxBytes: 1 << 20, RetentionDays: 7})
base := time.Now().Add(-time.Minute)
rows := []Event{
{RequestID: "a", Model: "glm-5.3", Status: 200, OK: true, Outcome: OutcomeSuccess,
ClientIP: "203.0.113.7", UserAgent: "python-requests/2.31.0"},
{RequestID: "b", Model: "glm-5.3", Status: 200, OK: true, Outcome: OutcomeSuccess,
ClientIP: "198.51.100.4", UserAgent: "Mozilla/5.0 Chrome/120.0.0.0"},
{RequestID: "c", Model: "glm-5.3", Status: 200, OK: true, Outcome: OutcomeSuccess},
}
for i, e := range rows {
e.Time = base.Add(time.Duration(i) * time.Second)
r.Record(e)
}
r.Close()
got, err := r.ReadArchive(10, Filter{ClientIP: "203.0.113."})
if err != nil {
t.Fatal(err)
}
if len(got) != 1 || got[0].RequestID != "a" {
t.Fatalf("client ip filter = %+v", got)
}
if got[0].UserAgent != "python-requests/2.31.0" {
t.Fatalf("user agent not persisted: %+v", got[0])
}
// UA 片段过滤大小写不敏感(面板搜索框直接传用户输入)。
got, err = r.ReadArchive(10, Filter{UserAgent: "chrome/120"})
if err != nil {
t.Fatal(err)
}
if len(got) != 1 || got[0].RequestID != "b" {
t.Fatalf("ua filter = %+v", got)
}
// 来源为空的旧事件不该被非空来源条件误命中。
got, err = r.ReadArchive(10, Filter{ClientIP: "10.0.0.1"})
if err != nil {
t.Fatal(err)
}
if len(got) != 0 {
t.Fatalf("unmatched ip filter = %+v", got)
}
}
// 时间区间过滤(「今天」/「自定义」):闭区间,任一侧为零值即该侧不设界。
func TestArchiveFilterByTimeRange(t *testing.T) {
dir := t.TempDir()
r := New(Config{Enabled: true, Dir: dir, MaxBytes: 1 << 20, RetentionDays: 30})
base := time.Now().Add(-72 * time.Hour).Truncate(time.Minute)
ids := []string{"d3", "d2", "d1", "now"}
for i, id := range ids {
r.Record(Event{
Time: base.Add(time.Duration(i) * 24 * time.Hour), RequestID: id,
Status: 200, OK: true, Outcome: OutcomeSuccess,
})
}
r.Close()
rows, err := r.ReadArchive(10, Filter{From: base.Add(24 * time.Hour), To: base.Add(48 * time.Hour)})
if err != nil {
t.Fatal(err)
}
got := make([]string, 0, len(rows))
for _, e := range rows {
got = append(got, e.RequestID)
}
// 倒序返回,闭区间命中 d2、d1。
if len(got) != 2 || got[0] != "d1" || got[1] != "d2" {
t.Fatalf("时间区间过滤 = %v, want [d1 d2]", got)
}
onlyFrom, err := r.ReadArchive(10, Filter{From: base.Add(48 * time.Hour)})
if err != nil {
t.Fatal(err)
}
if len(onlyFrom) != 2 || onlyFrom[0].RequestID != "now" {
t.Fatalf("仅 From 过滤 = %+v, want 2 条(d1、now)", onlyFrom)
}
onlyTo, err := r.ReadArchive(10, Filter{To: base})
if err != nil {
t.Fatal(err)
}
if len(onlyTo) != 1 || onlyTo[0].RequestID != "d3" {
t.Fatalf("仅 To 过滤 = %+v, want 仅 d3", onlyTo)
}
// 时间区间与其它条件是与关系。
combined, err := r.ReadArchive(10, Filter{
From: base.Add(24 * time.Hour), To: base.Add(48 * time.Hour), Outcome: OutcomeHTTPError,
})
if err != nil {
t.Fatal(err)
}
if len(combined) != 0 {
t.Fatalf("区间+结果组合应为空(这几条都是成功): %+v", combined)
}
}
func TestArchiveQueueDropCounter(t *testing.T) {
w := &archiveWriter{
cfg: Config{Enabled: true, Dir: t.TempDir()},
ch: make(chan Event, 1),
done: make(chan struct{}),
}
w.ch <- Event{RequestID: "occupied"}
w.enqueue(Event{RequestID: "dropped"})
if got := w.dropped.Load(); got != 1 {
t.Fatalf("dropped=%d want 1", got)
}
}
func TestArchivePruneHonorsSize(t *testing.T) {
dir := t.TempDir()
for i, name := range []string{"requests-2026-09-20.jsonl", "requests-2026-09-21.jsonl", "requests-2026-09-22.jsonl"} {
path := filepath.Join(dir, name)
if err := os.WriteFile(path, []byte("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"), 0o600); err != nil {
t.Fatal(err)
}
old := time.Now().AddDate(0, 0, -10+i)
if err := os.Chtimes(path, old, old); err != nil {
t.Fatal(err)
}
}
w := &archiveWriter{cfg: Config{Enabled: true, Dir: dir, RetentionDays: 30, MaxBytes: 50}, done: make(chan struct{})}
w.prune()
entries, err := os.ReadDir(dir)
if err != nil {
t.Fatal(err)
}
if len(entries) != 1 || entries[0].Name() != "requests-2026-09-22.jsonl" {
t.Fatalf("remaining = %+v, want newest file only", entries)
}
}