添加自动文件压缩的实现
This commit is contained in:
+156
-12
@@ -6,11 +6,15 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 文件操作
|
||||
|
||||
// errClosed 日志实例已关闭,不再接受新的文件写入
|
||||
var errClosed = errors.New("loggerx: 日志已关闭")
|
||||
|
||||
// 每个文件的写缓冲大小:越大越省系统调用,代价是崩溃时可能丢最后一批日志
|
||||
const fileBufSize = 32 * 1024
|
||||
|
||||
@@ -18,10 +22,17 @@ const fileBufSize = 32 * 1024
|
||||
// 自己管理写缓冲(不用 bufio):bufio.Writer 在底层短写时会永久置位内部错误状态,
|
||||
// 之后所有写入和 Flush 都会失败,已攒下的一批日志会整批丢掉
|
||||
type logFile struct {
|
||||
file *os.File
|
||||
buf []byte
|
||||
pending int
|
||||
file *os.File
|
||||
buf []byte
|
||||
pending int
|
||||
// written 已写进文件的字节数(内存计数)
|
||||
// 按大小切割要判断是否该滚动:每条日志都 Stat 一次要花约 8µs,
|
||||
// 而写了多少字节自己最清楚,只在打开文件时 Stat 一次做基准即可
|
||||
written int64
|
||||
fileName string
|
||||
// baseName 是 fileNameIn 的结果(不带大小切割的 _N 序号),
|
||||
// 用于判断是否跨了时间切割边界,以及归档时取规范基名
|
||||
baseName string
|
||||
}
|
||||
|
||||
// Write 写入缓冲,满了就落盘一次
|
||||
@@ -41,6 +52,29 @@ func (f *logFile) Write(b []byte) (int, error) {
|
||||
return written, nil
|
||||
}
|
||||
|
||||
// sizeOnDisk 当前文件实际占用的字节数(内存计数,不是每次 Stat)
|
||||
func (f *logFile) sizeOnDisk() int64 {
|
||||
return f.written
|
||||
}
|
||||
|
||||
// initSize 打开文件后记录一次真实大小作为计数基准(追加打开时文件可能非空)
|
||||
func (f *logFile) initSize() {
|
||||
if f.file == nil {
|
||||
return
|
||||
}
|
||||
if st, err := f.file.Stat(); err == nil {
|
||||
f.written = st.Size()
|
||||
}
|
||||
}
|
||||
|
||||
// full 加上即将写入的 n 字节后是否会超过上限
|
||||
func (f *logFile) full(limit, n int) bool {
|
||||
if limit <= 0 || f.file == nil {
|
||||
return false
|
||||
}
|
||||
return f.written+int64(f.pending)+int64(n) > int64(limit)
|
||||
}
|
||||
|
||||
// writeFull 保证把整块数据写进文件(允许底层短写,循环补齐)
|
||||
func (f *logFile) writeFull(p []byte) error {
|
||||
for len(p) > 0 {
|
||||
@@ -65,10 +99,18 @@ func (f *logFile) Flush() error {
|
||||
// 保留未写成功的部分,下次继续
|
||||
return err
|
||||
}
|
||||
f.written += int64(f.pending)
|
||||
f.pending = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasPending 缓冲里还有没有未落盘的数据
|
||||
// 定时刷盘靠它跳过空闲文件:否则每次 tick 都会对所有句柄做一次 Sync(fsync),
|
||||
// 高频间隔下光 fsync 就能把吞吐拖垮
|
||||
func (f *logFile) hasPending() bool {
|
||||
return f != nil && f.pending > 0
|
||||
}
|
||||
|
||||
// Sync 把缓冲刷到文件并 fsync
|
||||
func (f *logFile) Sync() error {
|
||||
var errs []error
|
||||
@@ -104,6 +146,14 @@ func joinErrors(errs []error) error {
|
||||
|
||||
// 获取最新的文件名
|
||||
func (l *Logger) nowFileName(event string) string {
|
||||
return l.fileNameIn(l.channel, event)
|
||||
}
|
||||
|
||||
// fileNameIn 按指定 channel 计算文件名
|
||||
// channel 由调用方传入而不是取 l.channel:异步消费协程用的是「创建队列时的那个
|
||||
// logger」,它的 channel 是空的,必须按任务里记的 channel 落盘
|
||||
// (否则所有 channel 的日志都会混进根目录文件)
|
||||
func (l *Logger) fileNameIn(channel, event string) string {
|
||||
prefix := ""
|
||||
|
||||
switch l.option.fileSplit {
|
||||
@@ -128,15 +178,20 @@ func (l *Logger) nowFileName(event string) string {
|
||||
prefix = prefix + "_"
|
||||
}
|
||||
|
||||
if l.channel != "" {
|
||||
prefix = l.channel + "/" + prefix
|
||||
if channel != "" {
|
||||
prefix = channel + "/" + prefix
|
||||
}
|
||||
return l.option.dir + "/" + prefix + event + ".log"
|
||||
}
|
||||
|
||||
// 新建文件
|
||||
func (l *Logger) getFile(event string) (*logFile, error) {
|
||||
key := fileKey{channel: l.channel, event: event}
|
||||
return l.getFileTo(l.channel, event)
|
||||
}
|
||||
|
||||
// getFileTo 取指定 channel 的文件句柄
|
||||
func (l *Logger) getFileTo(channel, event string) (*logFile, error) {
|
||||
key := fileKey{channel: channel, event: event}
|
||||
|
||||
if f := l.loadFile(key); f != nil {
|
||||
return f, nil
|
||||
@@ -150,9 +205,47 @@ func (l *Logger) getFile(event string) (*logFile, error) {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
fileName := l.nowFileName(event)
|
||||
// 已经关闭:不再新开句柄,否则没人负责关它
|
||||
if l.closed.Load() {
|
||||
return nil, errClosed
|
||||
}
|
||||
|
||||
lf, err := l.newFile(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.filePath[key] = lf
|
||||
|
||||
return lf, nil
|
||||
}
|
||||
|
||||
// newFile 打开新文件;如果目标文件已经超过大小上限,就先把旧文件归档再开新序号
|
||||
func (l *Logger) newFile(key fileKey) (*logFile, error) {
|
||||
limit := l.option.sizeSplit
|
||||
|
||||
lf, err := l.openNewFile(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if limit <= 0 || lf.sizeOnDisk() <= int64(limit) {
|
||||
return lf, nil
|
||||
}
|
||||
|
||||
// 上一轮运行留下的文件已经写满(进程重启场景):归档它,再开一个新序号
|
||||
_ = lf.file.Close()
|
||||
path, aerr := l.archive(lf.baseName, lf.fileName)
|
||||
if aerr != nil {
|
||||
return nil, aerr
|
||||
}
|
||||
l.scheduleCompress(path)
|
||||
|
||||
return l.openNewFileIndexed(key)
|
||||
}
|
||||
|
||||
// openNewFile 打开不带序号的当日文件
|
||||
func (l *Logger) openNewFile(key fileKey) (*logFile, error) {
|
||||
fileName := l.fileNameIn(key.channel, key.event)
|
||||
|
||||
// 识别目录与文件,创建多层目录(已存在不报错)
|
||||
if dir := filepath.Dir(fileName); dir != "" {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return nil, err
|
||||
@@ -168,12 +261,62 @@ func (l *Logger) getFile(event string) (*logFile, error) {
|
||||
file: file,
|
||||
buf: make([]byte, fileBufSize),
|
||||
fileName: fileName,
|
||||
baseName: fileName,
|
||||
}
|
||||
l.filePath[key] = lf
|
||||
|
||||
lf.initSize()
|
||||
return lf, nil
|
||||
}
|
||||
|
||||
// openNewFileIndexed 开一个全新的序号文件
|
||||
// 序号永远递增且跳过已存在的文件:绝不复用别人的归档
|
||||
// (复用后再归档会得到 名字_1_2.log 这种链式文件名,旧内容也可能被追加写坏)
|
||||
func (l *Logger) openNewFileIndexed(key fileKey) (*logFile, error) {
|
||||
base := l.fileNameIn(key.channel, key.event)
|
||||
for try := 0; try < 1000; try++ {
|
||||
idx := l.nextIndex()
|
||||
if _, err := os.Stat(numberedName(base, idx)); err == nil {
|
||||
continue // 序号已被占用,换下一个
|
||||
}
|
||||
return l.openNumberedFile(key, idx)
|
||||
}
|
||||
return nil, fmt.Errorf("无法为 %s 找到可用的归档序号", key.event)
|
||||
}
|
||||
|
||||
// openFileWithIndex 打开 「基名_序号.log」
|
||||
func (l *Logger) openFileWithIndex(key fileKey, idx int) (*logFile, error) {
|
||||
base := l.fileNameIn(key.channel, key.event)
|
||||
fileName := numberedName(base, idx)
|
||||
|
||||
if dir := filepath.Dir(fileName); dir != "" {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
file, err := os.OpenFile(fileName, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("打开日志文件失败 %s: %w", fileName, err)
|
||||
}
|
||||
lf := &logFile{
|
||||
file: file,
|
||||
buf: make([]byte, fileBufSize),
|
||||
fileName: fileName,
|
||||
baseName: base,
|
||||
}
|
||||
lf.initSize()
|
||||
return lf, nil
|
||||
}
|
||||
|
||||
// numberedName 由基名和序号拼出归档文件名
|
||||
// 例:./log/2026-09-13_info.log + 3 -> ./log/2026-09-13_info_3.log
|
||||
func numberedName(base string, idx int) string {
|
||||
return fmt.Sprintf("%s_%d.log", strings.TrimSuffix(base, ".log"), idx)
|
||||
}
|
||||
|
||||
// openNumberedFile 打开指定序号文件,序号由调用方保证未被占用
|
||||
func (l *Logger) openNumberedFile(key fileKey, idx int) (*logFile, error) {
|
||||
return l.openFileWithIndex(key, idx)
|
||||
}
|
||||
|
||||
// 加载文件(读锁)
|
||||
func (l *Logger) loadFile(key fileKey) *logFile {
|
||||
l.mu.RLock()
|
||||
@@ -187,8 +330,9 @@ func (l *Logger) loadFileLocked(key fileKey) *logFile {
|
||||
if !ok || f == nil {
|
||||
return nil
|
||||
}
|
||||
// 时间切割导致文件名变化:关闭旧文件,让调用方按新名字重建
|
||||
if f.fileName != l.nowFileName(key.event) {
|
||||
// 时间切割导致文件名变化:关闭旧文件,让调用方按新名字重建。
|
||||
// 注意比的是不带序号的基名,否则按大小切割出的 _N 文件会被误判成跨天
|
||||
if f.baseName != l.fileNameIn(key.channel, key.event) {
|
||||
delete(l.filePath, key)
|
||||
f.Close()
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user