Files
loggerx/loggerx_features_test.go
T
2026-09-13 21:53:37 +08:00

302 lines
8.1 KiB
Go

package loggerx_test
import (
"compress/gzip"
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/yuninks/loggerx"
)
// ---------- 功能 1:定时刷盘 ----------
// 开启定时刷盘后,未 Close 也能在磁盘上看到日志
func TestFlushIntervalPersistsWithoutClose(t *testing.T) {
dir := t.TempDir()
l := loggerx.NewLogger(context.Background(),
loggerx.SetDir(dir),
loggerx.SetFlushInterval(30*time.Millisecond),
)
defer l.Close()
l.Info(context.Background(), "FLUSHED-BY-TIMER")
// 等若干次刷盘周期;不调用 MustSync,也不 Close
deadline := time.Now().Add(3 * time.Second)
for time.Now().Before(deadline) {
if strings.Contains(readAllLogs(t, dir), "FLUSHED-BY-TIMER") {
return // 成功:定时器把它刷下去了
}
time.Sleep(20 * time.Millisecond)
}
t.Fatal("定时刷盘没生效:未 Close 时磁盘上看不到日志")
}
// 不开定时刷盘时,日志应该仍留在内存缓冲里(证明上面的测试不是假通过)
func TestNoFlushIntervalKeepsInBuffer(t *testing.T) {
dir := t.TempDir()
l := loggerx.NewLogger(context.Background(), loggerx.SetDir(dir))
defer l.Close()
l.Info(context.Background(), "STILL-BUFFERED")
time.Sleep(200 * time.Millisecond)
if strings.Contains(readAllLogs(t, dir), "STILL-BUFFERED") {
t.Skip("本次写入正好触发了缓冲落盘,跳过该对照")
}
}
// 定时刷盘不能丢日志、也不能让 Close 之后 goroutine 残留
func TestFlushIntervalNoLossAndStops(t *testing.T) {
dir := t.TempDir()
l := loggerx.NewLogger(context.Background(),
loggerx.SetDir(dir),
loggerx.SetFlushInterval(10*time.Millisecond),
)
const n = 3000
for i := 0; i < n; i++ {
l.Infof(context.Background(), "FL-%d", i)
}
if err := l.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
content := readAllLogs(t, dir)
missing := 0
for i := 0; i < n; i++ {
if !strings.Contains(content, fmt.Sprintf(`FL-%d"`, i)) {
missing++
}
}
if missing > 0 {
t.Errorf("定时刷盘场景丢了 %d 条日志", missing)
}
}
// ---------- 功能 2:按大小切割 + 压缩 ----------
// 超过大小上限后应该滚动出带序号的归档文件,并压缩成 .gz
func TestSizeSplitRollsAndCompresses(t *testing.T) {
dir := t.TempDir()
l := loggerx.NewLogger(context.Background(),
loggerx.SetDir(dir),
loggerx.SetSizeSplit(8*1024), // 8KB 一个文件
)
const n = 800 // 每条约 130 字节 => 约 104KB,应滚出十来个文件
for i := 0; i < n; i++ {
l.Infof(context.Background(), "SPLIT-%d-%s", i, strings.Repeat("x", 60))
}
if err := l.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
files, _ := filepath.Glob(filepath.Join(dir, "*.log"))
gzs, _ := filepath.Glob(filepath.Join(dir, "*.log.gz"))
t.Logf("滚动结果: %d 个 .log, %d 个 .gz", len(files), len(gzs))
if len(gzs) == 0 {
t.Fatal("没有产生任何压缩归档 .gz")
}
// 每个未压缩文件都不应明显超过上限(留一点余量给缓冲/单条超长)
for _, f := range files {
st, err := os.Stat(f)
if err != nil {
t.Fatalf("stat %s: %v", f, err)
}
if st.Size() > 8*1024+2048 {
t.Errorf("%s 大小 %d 超过上限过多", filepath.Base(f), st.Size())
}
}
// 归档文件名应带序号
for _, g := range gzs {
if !strings.Contains(filepath.Base(g), "_") {
t.Errorf("归档文件名缺少序号: %s", filepath.Base(g))
}
}
}
// 压缩归档要能被解开,且内容是完整合法的日志(不能丢条)
func TestCompressedArchiveIsReadable(t *testing.T) {
dir := t.TempDir()
l := loggerx.NewLogger(context.Background(),
loggerx.SetDir(dir),
loggerx.SetSizeSplit(4*1024),
)
const n = 400
for i := 0; i < n; i++ {
l.Infof(context.Background(), "GZ-%d", i)
}
if err := l.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
gzs, _ := filepath.Glob(filepath.Join(dir, "*.log.gz"))
if len(gzs) == 0 {
t.Fatal("没有产生 .gz 归档")
}
// 压缩归档 + 当前未压缩文件一起统计,必须一条不漏
content := readAllGz(t, dir) + readAllLogs(t, dir)
if !strings.Contains(content, "[info]{") {
t.Errorf("内容不像日志: %.120q", content)
}
missing := 0
for i := 0; i < n; i++ {
if !strings.Contains(content, fmt.Sprintf(`GZ-%d"`, i)) {
missing++
}
}
if missing > 0 {
t.Errorf("压缩归档丢失 %d / %d 条日志", missing, n)
}
t.Logf("共 %d 个 .gz 归档,解压后日志完整(%d 条)", len(gzs), n)
}
// 关闭压缩时,归档应保持未压缩的 .log 形态
func TestSizeSplitWithoutCompress(t *testing.T) {
dir := t.TempDir()
l := loggerx.NewLogger(context.Background(),
loggerx.SetDir(dir),
loggerx.SetSizeSplit(4*1024),
loggerx.SetCompress(false),
)
for i := 0; i < 300; i++ {
l.Infof(context.Background(), "RAW-%d", i)
}
if err := l.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
gzs, _ := filepath.Glob(filepath.Join(dir, "*.log.gz"))
if len(gzs) != 0 {
t.Errorf("SetCompress(false) 仍然产生了 %d 个 .gz", len(gzs))
}
files, _ := filepath.Glob(filepath.Join(dir, "*.log"))
if len(files) < 2 {
t.Errorf("期望滚出多个归档文件,实际 %d 个", len(files))
}
}
// 滚动过程中不能丢日志:所有序号都应在 .log 或 .gz 里找得到
func TestSizeSplitNoLoss(t *testing.T) {
for round := 0; round < 3; round++ {
dir := t.TempDir()
l := loggerx.NewLogger(context.Background(),
loggerx.SetDir(dir),
loggerx.SetSizeSplit(8*1024),
)
const n = 600
for i := 0; i < n; i++ {
l.Infof(context.Background(), "NOLOSS-%d", i)
}
if err := l.Close(); err != nil {
t.Fatalf("第 %d 轮 Close: %v", round, err)
}
content := readAllLogs(t, dir) + readAllGz(t, dir)
missing := 0
for i := 0; i < n; i++ {
if !strings.Contains(content, fmt.Sprintf(`NOLOSS-%d"`, i)) {
missing++
}
}
if missing > 0 {
t.Errorf("第 %d 轮:按大小切割丢了 %d 条日志", round, missing)
}
}
}
// 单条日志就超过上限时不能死循环
func TestSizeSplitHugeSingleEntry(t *testing.T) {
dir := t.TempDir()
l := loggerx.NewLogger(context.Background(),
loggerx.SetDir(dir),
loggerx.SetSizeSplit(1024), // 上限比单条还小
)
done := make(chan struct{})
go func() {
defer close(done)
l.Info(context.Background(), strings.Repeat("H", 8192))
l.Info(context.Background(), "after-huge")
}()
select {
case <-done:
case <-time.After(10 * time.Second):
t.Fatal("单条超长日志导致卡死(可能是滚动死循环)")
}
if err := l.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
if !strings.Contains(readAllLogs(t, dir)+readAllGz(t, dir), "after-huge") {
t.Error("超长日志之后的那条日志丢失了")
}
}
// 重启场景:文件已经满了,新进程开起来应该归档旧文件而不是永远写同一个
func TestSizeSplitOnRestart(t *testing.T) {
dir := t.TempDir()
opts := []loggerx.Option{loggerx.SetDir(dir), loggerx.SetSizeSplit(4 * 1024)}
l1 := loggerx.NewLogger(context.Background(), opts...)
for i := 0; i < 200; i++ {
l1.Infof(context.Background(), "RUN1-%d", i)
}
if err := l1.Close(); err != nil {
t.Fatalf("l1.Close: %v", err)
}
l2 := loggerx.NewLogger(context.Background(), opts...)
for i := 0; i < 200; i++ {
l2.Infof(context.Background(), "RUN2-%d", i)
}
if err := l2.Close(); err != nil {
t.Fatalf("l2.Close: %v", err)
}
content := readAllLogs(t, dir) + readAllGz(t, dir)
for i := 0; i < 200; i++ {
if !strings.Contains(content, fmt.Sprintf(`RUN2-%d"`, i)) {
t.Fatalf("重启后 RUN2-%d 丢失", i)
}
}
// 归档序号不应互相覆盖:RUN1 与 RUN2 都应该在
if !strings.Contains(content, `RUN1-0"`) {
t.Error("第一轮运行的日志被覆盖了")
}
}
// readAllGz 解开目录下所有 .gz 归档
func readAllGz(t *testing.T, dir string) string {
t.Helper()
var sb strings.Builder
gzs, _ := filepath.Glob(filepath.Join(dir, "*.gz"))
for _, g := range gzs {
f, err := os.Open(g)
if err != nil {
t.Fatalf("打开 %s: %v", g, err)
}
zr, err := gzip.NewReader(f)
if err != nil {
_ = f.Close()
t.Fatalf("%s 不是合法 gzip: %v", g, err)
}
b, err := io.ReadAll(zr)
if err != nil {
t.Fatalf("解压 %s: %v", g, err)
}
_ = zr.Close()
_ = f.Close()
sb.Write(b)
}
return sb.String()
}