Files
loggerx/loggerx.go
T

224 lines
5.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_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 而不是只监听 ctxctx 为 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
}