添加自动文件压缩的实现
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user