package loggerx // author:黄新云 // lastTime:2024年4月3日21:18:05 // desc: 日志封装类 import ( "context" "fmt" "io" "log" "os" "sync" "sync/atomic" "time" // "sync_log/global" "github.com/gin-gonic/gin" ) // 需要实现io.Writer接口 type Logger struct { ctx context.Context filePath map[fileKey]*logFile // 每个 (channel,event) 一个句柄 mu *sync.RWMutex // 保护 filePath writeMu *sync.Mutex // 串行化文件写入(bufio 不是并发安全的) option loggerOption channel string writeType writeType // 是否异步落盘,这里作用范围是本条,优先判断这里 closeOnce *sync.Once 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 // 归档压缩相关状态(同样挂在指针后面供拷贝共享) idxOnce *sync.Once // 归档序号只从目录里初始化一次 idxMu *sync.Mutex // 保护 idx idx int // 归档序号 compWg *sync.WaitGroup // 后台压缩协程计数,Close 需要等它们收尾 } // asyncCtl 异步队列的共享控制块:所有拷贝共享同一份 type asyncCtl struct { mu sync.Mutex ch chan cacheData closed bool // 只能读写于 mu 保护下 wg sync.WaitGroup } // pendingAsync 记录「已通过投递检查、正在往队列里放」的写入数量。 // 调用方必须持有 mu,Done 必须紧跟在 mu 解锁之后(见 toAsync) func (c *asyncCtl) begin() { c.wg.Add(1) } func (c *asyncCtl) end() { c.wg.Done() } // fileKey 文件缓存的键 // 必须同时包含 channel 与 event:文件名由两者共同决定,只按 channel 缓存会 // 导致 info/error 互相顶掉句柄,每次写入都 close+open 一次文件 type fileKey struct { channel string event string } func NewLogger(ctx context.Context, opts ...Option) *Logger { opt := defaultOptions() for _, apply := range opts { apply(&opt) } // 验证文件夹权限 if !checkDir(opt.dir) { 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{}, } log.SetOutput(l) log.SetFlags(log.LstdFlags | log.Llongfile | log.Lmicroseconds) // log.Lshortfile | log.LUTC // 保存Gin日志写入到文件+控制台 if opt.isGinLog { gin.DefaultWriter = io.MultiWriter(l, os.Stdout) gin.DefaultErrorWriter = io.MultiWriter(l, os.Stdout) } // 日志删除 go l.delete() // 定时刷盘 if opt.flushEvery > 0 { go l.flushLoop() } // 强制刷盘 // 用 done 而不是只监听 ctx:ctx 为 Background 时 Close 也要能把 goroutine 收回去 go func() { select { case <-ctx.Done(): l.MustSync() case <-l.done: } }() return l } // flushTick 定时刷盘的检查频率 // 实际刷盘间隔由 SetFlushInterval 决定;这里用固定的小 tick 做检查, // 这样即使调用方设了很小的间隔,也不会因为「每个 tick 都抢全局写锁」而拖垮吞吐。 // 写锁是全局的,高频抢锁会直接顶住所有写入方 const flushTick = 50 * time.Millisecond // flushLoop 定时把内存缓冲刷到磁盘 // 作用是把「进程崩溃时可能丢的数据量」从「最多一整个 32KB 缓冲」 // 缩小到「一个刷盘间隔内产生的日志量」 func (l *Logger) flushLoop() { tick := time.NewTicker(flushTick) defer tick.Stop() last := time.Now() for { select { case <-tick.C: interval := l.option.flushEvery if interval <= 0 { continue } if now := time.Now(); now.Sub(last) >= interval { last = now // 单个文件刷盘失败不应该让这个循环退出 if err := l.MustSync(); err != nil { log.Println("loggerx: 定时刷盘失败:", err) } } case <-l.done: return } } } // 关闭日志:关闭异步队列、落盘并关闭所有文件句柄 // 重复调用安全,调用后本实例的后台 goroutine 会全部退出 func (l *Logger) Close() error { var err error l.closeOnce.Do(func() { // 1. 先停投递 + 等消费协程把队列里已入队的任务全部写完。 // 这一步必须在「封盘」之前:队列里可能还有别的 channel / event 的任务, // 它们需要新开自己的文件句柄。如果提前封盘,这些任务会直接写失败被丢掉 l.drainAsync() // 2. 封盘:此后不再新开句柄,避免 Close 扫完句柄表之后又冒出没人负责关的句柄 l.closed.Store(true) // 3. 在写锁内完成最后一次刷盘并关闭所有句柄。此时无锁等待,不会和谁抢锁 l.writeMu.Lock() err = joinErrors([]error{l.mustSyncLocked(), l.close()}) l.writeMu.Unlock() // 4. 等后台压缩协程收尾:不等的话 Close 返回后去读归档会读到半截 .gz l.compWg.Wait() // 5. 最后再发通用关闭信号,让 delete / ctx 相关 goroutine 退出 close(l.done) }) return err } // 强制刷盘:只 flush 缓存数据,文件继续可写 // 与写入并发调用是安全的(内部会先拿到写锁) func (l *Logger) MustSync() error { l.writeMu.Lock() defer l.writeMu.Unlock() return l.mustSyncLocked() } // mustSyncLocked 调用方必须持有 writeMu // 缓冲的 pending 与 buf 只有在写锁内访问才安全,否则会和 store 并发改动同一块内存 func (l *Logger) mustSyncLocked() error { l.mu.RLock() files := make([]*logFile, 0, len(l.filePath)) for _, f := range l.filePath { files = append(files, f) } l.mu.RUnlock() var errs []error for _, f := range files { // 没有待落盘数据的文件直接跳过:省掉一次 fsync。 // Close 仍然安全:logFile.Close 自己会再 Flush 一次并关句柄 if !f.hasPending() { continue } errs = append(errs, f.Sync()) } return joinErrors(errs) } func (l *Logger) Channel(ch string) (r *Logger) { rr := *l rr.channel = ch return &rr } func (l *Logger) WriteAsync() (r *Logger) { rr := *l rr.writeType = writeTypeAsync return &rr } func (l *Logger) WriteSync() (r *Logger) { rr := *l rr.writeType = writeTypeSync return &rr } // 获取TraceField的字段 func (l *Logger) GetTraceField() string { return l.option.traceField } // 实现io.Writer接口 func (l *Logger) Write(b []byte) (n int, err error) { return l.write("info", b) } func (l *Logger) Info(ctx context.Context, v ...any) { l.logger(ctx, "info", v...) } func (l *Logger) Infof(ctx context.Context, format string, v ...any) { s := fmt.Sprintf(format, v...) l.logger(ctx, "info", s) } func (l *Logger) Error(ctx context.Context, v ...any) { l.logger(ctx, "error", v...) } func (l *Logger) Errorf(ctx context.Context, format string, v ...any) { s := fmt.Sprintf(format, v...) l.logger(ctx, "error", s) } func (l *Logger) Debug(ctx context.Context, v ...any) { l.logger(ctx, "debug", v...) } func (l *Logger) Debugf(ctx context.Context, format string, v ...any) { s := fmt.Sprintf(format, v...) l.logger(ctx, "debug", s) } func (l *Logger) Warn(ctx context.Context, v ...any) { l.logger(ctx, "warn", v...) } func (l *Logger) Warnf(ctx context.Context, format string, v ...any) { s := fmt.Sprintf(format, v...) l.logger(ctx, "warn", s) } // 验证文件夹权限 // 根文件夹如果不存在则创建 func checkDir(dir string) bool { if _, err := os.Stat(dir); err != nil { if os.IsNotExist(err) { if err := os.MkdirAll(dir, 0755); err != nil { log.Println("创建文件夹失败", err) return false } return true } // 目标存在但不可访问(权限不足等),此时返回 false 才是这个函数的本意 log.Println("日志文件夹不可用", dir, err) return false } return true }