更新
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
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 名仍然应该把日志写进日志目录(不能丢日志)")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user