package loggerx_test import ( "context" "encoding/json" "errors" "os" "path/filepath" "strings" "sync" "testing" "github.com/yuninks/loggerx" ) // 级别过滤:设为 Info 之后 Debug 必须一条都不落盘 func TestMinLevelFiltersDebug(t *testing.T) { dir := t.TempDir() l := loggerx.NewLogger(context.Background(), loggerx.SetDir(dir), loggerx.SetMinLevel(loggerx.LevelInfo), ) l.Debug(context.Background(), "NOISY-DEBUG") l.Info(context.Background(), "KEEP-INFO") if err := l.Close(); err != nil { t.Fatalf("Close: %v", err) } content := readAllLogs(t, dir) if strings.Contains(content, "NOISY-DEBUG") { t.Error("SetMinLevel(LevelInfo) 未生效:Debug 仍然落盘") } if !strings.Contains(content, "KEEP-INFO") { t.Error("Info 被错误过滤掉了") } } // LevelOff 之后什么都不写 func TestMinLevelOff(t *testing.T) { dir := t.TempDir() l := loggerx.NewLogger(context.Background(), loggerx.SetDir(dir), loggerx.SetMinLevel(loggerx.LevelOff), ) l.Error(context.Background(), "SHOULD-NOT-APPEAR") if err := l.Close(); err != nil { t.Fatal(err) } if content := readAllLogs(t, dir); strings.Contains(content, "SHOULD-NOT-APPEAR") { t.Errorf("LevelOff 之后仍有日志落盘: %s", content) } } // 落盘格式必须是合法 JSON 行,且带 level 字段(便于采集端解析) // 注意:不同级别分文件存放(info.log / error.log),所以按文件分别断言 func TestOutputIsValidJSONLines(t *testing.T) { dir := t.TempDir() l := loggerx.NewLogger(context.Background(), loggerx.SetDir(dir)) ctx := context.Background() l.Info(ctx, "info-msg") l.Warn(ctx, "warn-msg") l.Error(ctx, "err-msg") if err := l.Close(); err != nil { t.Fatal(err) } check := func(file, wantLevel string) { t.Helper() files, _ := filepath.Glob(filepath.Join(dir, "*_"+file)) if len(files) == 0 { t.Fatalf("没有找到 %s", file) } b, err := os.ReadFile(files[0]) if err != nil { t.Fatal(err) } lines := strings.Split(strings.TrimSpace(string(b)), "\n") if len(lines) != 1 { t.Fatalf("%s 期望 1 行,实际 %d 行: %q", file, len(lines), string(b)) } var v map[string]any if err := json.Unmarshal([]byte(lines[0]), &v); err != nil { t.Fatalf("%s 不是合法 JSON: %v\n%s", file, err, lines[0]) } if v["level"] != wantLevel { t.Errorf("%s 的 level=%v,期望 %v", file, v["level"], wantLevel) } if _, ok := v["time"]; !ok { t.Errorf("%s 缺少 time 字段", file) } if _, ok := v["content"]; !ok { t.Errorf("%s 缺少 content 字段", file) } } check("info.log", "info") check("warn.log", "warn") check("error.log", "error") } // readAllLogsRecursive 递归读取目录下所有日志(含 channel 子目录) func readAllLogsRecursive(t *testing.T, dir string) string { t.Helper() var sb strings.Builder _ = filepath.Walk(dir, func(p string, info os.FileInfo, err error) error { if err != nil || info.IsDir() { return nil } b, rerr := os.ReadFile(p) if rerr != nil { t.Fatalf("读取 %s: %v", p, rerr) } sb.Write(b) return nil }) return sb.String() } // SetErrorHandler 必须能拿到内部错误 func TestErrorHandlerReceivesErrors(t *testing.T) { boom := errors.New("driver boom") var mu sync.Mutex var got []error l := loggerx.NewLogger(context.Background(), loggerx.SetDir(t.TempDir()), loggerx.SetPrintFile(false), loggerx.SetErrorHandler(func(err error) { mu.Lock() got = append(got, err) mu.Unlock() }), loggerx.SetExtraDriver(writerFunc(func(p []byte) (int, error) { return 0, boom })), ) l.Info(context.Background(), "trigger") if err := l.Close(); err != nil { t.Fatalf("Close: %v", err) } mu.Lock() defer mu.Unlock() if len(got) == 0 { t.Fatal("驱动写失败没有通过 SetErrorHandler 上报,运维完全看不到") } if !errors.Is(got[0], boom) { t.Errorf("错误内容不符合预期: %v", got[0]) } } // 关闭后再写应能被 errors.Is(err, ErrClosed) 判断出来 func TestErrClosedIsDiscoverable(t *testing.T) { l := loggerx.NewLogger(context.Background(), loggerx.SetDir(t.TempDir())) if err := l.Close(); err != nil { t.Fatal(err) } _, err := l.Write([]byte("x")) if !errors.Is(err, loggerx.ErrClosed) { t.Errorf("期望 errors.Is(err, ErrClosed),实际: %v", err) } } // channel 名来自外部输入时不能逃出日志目录 func TestChannelNameCannotEscapeDir(t *testing.T) { base := t.TempDir() dir := filepath.Join(base, "logs") l := loggerx.NewLogger(context.Background(), loggerx.SetDir(dir)) // 试图用 ../ 写到日志目录外面 l.Channel("../../evil").Info(context.Background(), "ESCAPE-ATTEMPT") if err := l.Close(); err != nil { t.Fatal(err) } // 日志目录之外不应出现任何我们写的文件 entries, err := os.ReadDir(base) if err != nil { t.Fatal(err) } for _, e := range entries { if e.Name() != "logs" { t.Errorf("channel 名逃出了日志目录,产生了 %s", e.Name()) } } if !strings.Contains(readAllLogsRecursive(t, dir), "ESCAPE-ATTEMPT") { t.Error("清洗后的 channel 名仍然应该把日志写进日志目录(不能丢日志)") } }