Files
loggerx/loggerx_prod_gaps_test.go
T

189 lines
5.1 KiB
Go
Raw Normal View History

2026-09-14 00:14:30 +08:00
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 名仍然应该把日志写进日志目录(不能丢日志)")
}
}