package loggerx // author:黄新云 // lastTime:2024年4月3日21:18:05 // desc: 日志封装类 import ( "context" "fmt" "io" "log" "os" "sync" // "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 done chan struct{} // 通用关闭信号(delete / ctx 相关 goroutine 用) workerDone chan struct{} // 异步消费 goroutine 退出信号 closing chan struct{} // 异步投递关闭信号:只关一次 // 异步队列相关的整块状态都挂在指针后面:Logger 会被 Channel()/WriteAsync() // 浅拷贝,如果状态直接放在 Logger 里,拷贝出的实例就会各看各的 // (曾经因此让 Close 看不到真正的队列,白等一场、最后一条日志丢失) async *asyncCtl } // asyncCtl 异步队列的共享控制块:所有拷贝共享同一份 type asyncCtl struct { mu sync.Mutex ch chan cacheData closed bool // 只能读写于 mu 保护下 } // 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{}, done: make(chan struct{}), workerDone: make(chan struct{}), async: &asyncCtl{}, } 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() // 强制刷盘 // 用 done 而不是只监听 ctx:ctx 为 Background 时 Close 也要能把 goroutine 收回去 go func() { select { case <-ctx.Done(): l.MustSync() case <-l.done: } }() return l } // 关闭日志:关闭异步队列、落盘并关闭所有文件句柄 // 重复调用安全,调用后本实例的后台 goroutine 会全部退出 func (l *Logger) Close() error { var err error l.closeOnce.Do(func() { // 1. 停投递 + 等消费协程把队列里已入队的任务全部写完 l.drainAsync() // 2. 在写锁内完成最后一次刷盘与关闭 // 此后不会再有写入(投递已关闭,同步写入也要先抢到这把锁) l.writeMu.Lock() syncErr := l.MustSync() closeErr := l.close() l.writeMu.Unlock() err = joinErrors([]error{syncErr, closeErr}) // 3. 最后再发通用关闭信号,让 delete / ctx 相关 goroutine 退出 close(l.done) }) return err } // 强制刷盘:只 flush 缓存数据,文件继续可写 func (l *Logger) MustSync() 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 { 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 }