Files
loggerx/loggerx.go
T
2026-09-13 21:53:37 +08:00

311 lines
8.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 而不是只监听 ctxctx 为 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
}