This commit is contained in:
yun
2026-09-14 00:14:30 +08:00
parent 316c6420a7
commit 7e1b474823
20 changed files with 1885 additions and 166 deletions
+166 -31
View File
@@ -25,6 +25,7 @@ type Logger struct {
filePath map[fileKey]*logFile // 每个 (channel,event) 一个句柄
mu *sync.RWMutex // 保护 filePath
writeMu *sync.Mutex // 串行化文件写入(bufio 不是并发安全的)
driverMu *sync.Mutex // 串行化额外驱动:用户 driver 往往不是并发安全的
option loggerOption
channel string
writeType writeType // 是否异步落盘,这里作用范围是本条,优先判断这里
@@ -33,22 +34,120 @@ type Logger struct {
closed *atomic.Bool // 是否已经关闭:关闭后再写不再新开句柄
done chan struct{} // 通用关闭信号(delete / ctx 相关 goroutine 用)
workerDone chan struct{} // 异步消费 goroutine 退出信号
closing chan struct{} // 异步投递关闭信号:只关一次
// 异步队列相关的整块状态都挂在指针后面:Logger 会被 Channel()/WriteAsync()
// 浅拷贝,如果状态直接放在 Logger 里,拷贝出的实例就会各看各的
// (曾经因此让 Close 看不到真正的队列,白等一场、最后一条日志丢失)
async *asyncCtl
// basePath 缓存(见 format.go basePath),避免每条日志都算一次 filepath.Abs
basePathOnce *sync.Once
basePathVal string
// basePath 缓存:值是共享指针,拷贝出的实例才能看到同一个基路径
basePathVal *string
// 归档压缩相关状态(同样挂在指针后面供拷贝共享)
idxOnce *sync.Once // 归档序号只从目录里初始化一次
idxMu *sync.Mutex // 保护 idx
idx int // 归档序号
compWg *sync.WaitGroup // 后台压缩协程计数Close 需要等它们收尾
// 归档序号状态:必须整块共享,否则 Channel()/WriteAsync() 拷贝出来的实例
// 会各自从 0 开始数,选出已被占用的序号,把已有归档覆盖掉
idxState *idxCtl
// 后台压缩协程计数Close 需要等它们收尾,否则读归档会读到半截 .gz
compWg *sync.WaitGroup
}
// idxCtl 归档序号的共享状态
type idxCtl struct {
once *sync.Once
mu *sync.Mutex
n int
}
// nextIndex 返回下一个归档序号(首次调用时从目录里扫描续号)
func (l *Logger) nextIndex() int {
l.idxState.once.Do(func() {
l.idxState.n = l.maxIndexOnDisk()
})
l.idxState.mu.Lock()
defer l.idxState.mu.Unlock()
l.idxState.n++
return l.idxState.n
}
// 全局 stdlib log / gin 写手的接管登记信息
var (
globalLogOwner atomic.Pointer[Logger] // 当前接管全局 log 的实例
origLogWriter atomic.Value // 进程最初(首次接管前)的 writer,只写一次
globalLogMu sync.Mutex
ginOwner atomic.Pointer[Logger]
origGinOut atomic.Value // 首次接管前的 gin.DefaultWriter
origGinErr atomic.Value // 首次接管前的 gin.DefaultErrorWriter
globalGinMu sync.Mutex
)
// logWriterAdapter 让 io.Writer 能被安全地存进 atomic.Value
type logWriterAdapter struct{ w io.Writer }
// takeOverGlobalLog 接管全局 stdlib log,只记录一次「最初」的 writer
func (l *Logger) takeOverGlobalLog() {
globalLogMu.Lock()
defer globalLogMu.Unlock()
// 无论中间被接管几次,origLogWriter 永远是进程最初那个 writer。
// 旧实现记的是「上一个持有者」,于是 L1->L2->Close(L2) 会把全局 log
// 还原成 L1,而 L1 也可能已关闭 —— 那正是这套机制要避免的丢日志场景
if t := origLogWriter.Load(); t == nil {
origLogWriter.Store(logWriterAdapter{w: log.Writer()})
}
log.SetOutput(l)
log.SetFlags(log.LstdFlags | log.Llongfile | log.Lmicroseconds)
globalLogOwner.Store(l)
}
// releaseGlobalLog 如果全局 log 还指向自己,就还原成最初的 writer
//
// Close 时必须做:否则 Close 之后的 stdlib log 会写进已关闭实例,
// 被 ErrClosed 拒绝后静默丢失,运维完全看不到
func (l *Logger) releaseGlobalLog() {
globalLogMu.Lock()
defer globalLogMu.Unlock()
if cur := globalLogOwner.Load(); cur == nil || cur != l {
return
}
if v := origLogWriter.Load(); v != nil {
log.SetOutput(v.(logWriterAdapter).w)
}
globalLogOwner.Store(nil)
}
// takeOverGinWriters 接管 gin 的默认写手
// 把 os.Stdout 放前面:MultiWriter 遇到第一个错误就短路,
// 若先写 l 且 l 出错(磁盘满 / 已关闭),gin 连控制台输出和 Recovery 堆栈都会一起消失
func (l *Logger) takeOverGinWriters() {
globalGinMu.Lock()
defer globalGinMu.Unlock()
if v := origGinOut.Load(); v == nil {
origGinOut.Store(logWriterAdapter{w: gin.DefaultWriter})
origGinErr.Store(logWriterAdapter{w: gin.DefaultErrorWriter})
}
gin.DefaultWriter = io.MultiWriter(os.Stdout, l)
gin.DefaultErrorWriter = io.MultiWriter(os.Stdout, l)
ginOwner.Store(l)
}
// releaseGinWriters 还原 gin 的默认写手
func (l *Logger) releaseGinWriters() {
globalGinMu.Lock()
defer globalGinMu.Unlock()
if cur := ginOwner.Load(); cur == nil || cur != l {
return
}
if v := origGinOut.Load(); v != nil {
gin.DefaultWriter = v.(logWriterAdapter).w
}
if v := origGinErr.Load(); v != nil {
gin.DefaultErrorWriter = v.(logWriterAdapter).w
}
ginOwner.Store(nil)
}
// asyncCtl 异步队列的共享控制块:所有拷贝共享同一份
@@ -88,36 +187,47 @@ func NewLogger(ctx context.Context, opts ...Option) *Logger {
panic("文件夹权限不足")
}
l := &Logger{
ctx: ctx,
filePath: make(map[fileKey]*logFile),
mu: &sync.RWMutex{},
writeMu: &sync.Mutex{},
option: opt,
writeType: writeTypeDefault,
closeOnce: &sync.Once{},
closed: &atomic.Bool{},
done: make(chan struct{}),
workerDone: make(chan struct{}),
async: &asyncCtl{},
basePathOnce: &sync.Once{},
idxOnce: &sync.Once{},
idxMu: &sync.Mutex{},
compWg: &sync.WaitGroup{},
// 构造函数允许 ctx 为 nil(调用方常把可选 ctx 直接传进来),
// 但后台 goroutine 会调 ctx.Done()nil 接口会 panic,这里统一兜底。
// 另外提醒:这个 ctx 是「实例生命周期」句柄,应当传进程级 ctx,
// 不要传请求级 ctx —— 请求结束会让清理协程提前退出
if ctx == nil {
ctx = context.Background()
}
log.SetOutput(l)
log.SetFlags(log.LstdFlags | log.Llongfile | log.Lmicroseconds) // log.Lshortfile | log.LUTC
l := &Logger{
ctx: ctx,
filePath: make(map[fileKey]*logFile),
mu: &sync.RWMutex{},
writeMu: &sync.Mutex{},
driverMu: &sync.Mutex{},
option: opt,
writeType: writeTypeDefault,
closeOnce: &sync.Once{},
closed: &atomic.Bool{},
done: make(chan struct{}),
workerDone: make(chan struct{}),
async: &asyncCtl{},
basePathVal: new(string),
idxState: &idxCtl{once: &sync.Once{}, mu: &sync.Mutex{}},
compWg: &sync.WaitGroup{},
}
*l.basePathVal = resolveBasePath()
// 把全局 stdlib log 的输出接管到本实例(可选,默认不接管)
// 生产建议:日志库不应该在 import 或 NewLogger 时静默劫持宿主的全局 log,
// 多实例场景下后建的会把先建的顶掉,Close 之后全局 log 还会指向已关闭实例
if opt.asGlobalLog {
l.takeOverGlobalLog()
}
// 保存Gin日志写入到文件+控制台
if opt.isGinLog {
gin.DefaultWriter = io.MultiWriter(l, os.Stdout)
gin.DefaultErrorWriter = io.MultiWriter(l, os.Stdout)
l.takeOverGinWriters()
}
// 日志删除
go l.delete()
// 定时刷盘
if opt.flushEvery > 0 {
go l.flushLoop()
@@ -191,12 +301,37 @@ func (l *Logger) Close() error {
// 4. 等后台压缩协程收尾:不等的话 Close 返回后去读归档会读到半截 .gz
l.compWg.Wait()
// 5. 最后再发通用关闭信号,让 delete / ctx 相关 goroutine 退出
// 5. 如果全局 log / gin 写手还指向自己,还原成最初的那个
// 否则 Close 之后 stdlib log 会写进已关闭实例并被静默丢弃
l.releaseGlobalLog()
l.releaseGinWriters()
// 6. 最后再发通用关闭信号,让 delete / ctx 相关 goroutine 退出
close(l.done)
})
return err
}
// reportError 上报日志库自身的内部错误
//
// 这是运维唯一能看到「磁盘满 / 句柄失效 / 归档失败 / 刷盘超时」的通道:
// 注册了 SetErrorHandler 就交给它,否则退回 stderr,至少不会完全静默。
// 回调自身 panic 会被吞掉,绝不能让错误处理把调用方带崩。
func (l *Logger) reportError(err error) {
if err == nil {
return
}
fn := l.option.onError
if fn == nil {
fmt.Fprintln(os.Stderr, err)
return
}
func() {
defer func() { _ = recover() }()
fn(err)
}()
}
// 强制刷盘:只 flush 缓存数据,文件继续可写
// 与写入并发调用是安全的(内部会先拿到写锁)
func (l *Logger) MustSync() error {