2024-01-22 18:05:51 +08:00
|
|
|
|
package loggerx
|
|
|
|
|
|
|
|
|
|
|
|
// author:黄新云
|
2024-04-03 21:18:44 +08:00
|
|
|
|
// lastTime:2024年4月3日21:18:05
|
2024-01-22 18:05:51 +08:00
|
|
|
|
// desc: 日志封装类
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"io"
|
|
|
|
|
|
"log"
|
|
|
|
|
|
"os"
|
|
|
|
|
|
"sync"
|
2026-09-13 21:53:37 +08:00
|
|
|
|
"sync/atomic"
|
|
|
|
|
|
"time"
|
2024-01-22 18:05:51 +08:00
|
|
|
|
|
|
|
|
|
|
// "sync_log/global"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 需要实现io.Writer接口
|
|
|
|
|
|
type Logger struct {
|
2024-11-22 20:08:25 +08:00
|
|
|
|
ctx context.Context
|
2026-09-13 20:41:27 +08:00
|
|
|
|
filePath map[fileKey]*logFile // 每个 (channel,event) 一个句柄
|
|
|
|
|
|
mu *sync.RWMutex // 保护 filePath
|
|
|
|
|
|
writeMu *sync.Mutex // 串行化文件写入(bufio 不是并发安全的)
|
2026-09-14 00:14:30 +08:00
|
|
|
|
driverMu *sync.Mutex // 串行化额外驱动:用户 driver 往往不是并发安全的
|
2024-11-22 20:08:25 +08:00
|
|
|
|
option loggerOption
|
|
|
|
|
|
channel string
|
|
|
|
|
|
writeType writeType // 是否异步落盘,这里作用范围是本条,优先判断这里
|
2026-09-13 20:41:27 +08:00
|
|
|
|
|
|
|
|
|
|
closeOnce *sync.Once
|
2026-09-13 21:53:37 +08:00
|
|
|
|
closed *atomic.Bool // 是否已经关闭:关闭后再写不再新开句柄
|
2026-09-13 20:41:27 +08:00
|
|
|
|
done chan struct{} // 通用关闭信号(delete / ctx 相关 goroutine 用)
|
|
|
|
|
|
workerDone chan struct{} // 异步消费 goroutine 退出信号
|
|
|
|
|
|
|
|
|
|
|
|
// 异步队列相关的整块状态都挂在指针后面:Logger 会被 Channel()/WriteAsync()
|
|
|
|
|
|
// 浅拷贝,如果状态直接放在 Logger 里,拷贝出的实例就会各看各的
|
|
|
|
|
|
// (曾经因此让 Close 看不到真正的队列,白等一场、最后一条日志丢失)
|
|
|
|
|
|
async *asyncCtl
|
2026-09-13 21:53:37 +08:00
|
|
|
|
|
2026-09-14 00:14:30 +08:00
|
|
|
|
// basePath 缓存:值是共享指针,拷贝出的实例才能看到同一个基路径
|
|
|
|
|
|
basePathVal *string
|
2026-09-13 21:53:37 +08:00
|
|
|
|
|
2026-09-14 00:14:30 +08:00
|
|
|
|
// 归档序号状态:必须整块共享,否则 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)
|
2024-01-23 00:12:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-13 20:41:27 +08:00
|
|
|
|
// asyncCtl 异步队列的共享控制块:所有拷贝共享同一份
|
|
|
|
|
|
type asyncCtl struct {
|
|
|
|
|
|
mu sync.Mutex
|
|
|
|
|
|
ch chan cacheData
|
|
|
|
|
|
closed bool // 只能读写于 mu 保护下
|
2026-09-13 21:53:37 +08:00
|
|
|
|
wg sync.WaitGroup
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// pendingAsync 记录「已通过投递检查、正在往队列里放」的写入数量。
|
|
|
|
|
|
// 调用方必须持有 mu,Done 必须紧跟在 mu 解锁之后(见 toAsync)
|
|
|
|
|
|
func (c *asyncCtl) begin() {
|
|
|
|
|
|
c.wg.Add(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (c *asyncCtl) end() {
|
|
|
|
|
|
c.wg.Done()
|
2026-09-13 20:41:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// fileKey 文件缓存的键
|
|
|
|
|
|
// 必须同时包含 channel 与 event:文件名由两者共同决定,只按 channel 缓存会
|
|
|
|
|
|
// 导致 info/error 互相顶掉句柄,每次写入都 close+open 一次文件
|
|
|
|
|
|
type fileKey struct {
|
|
|
|
|
|
channel string
|
|
|
|
|
|
event string
|
2024-01-22 18:05:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-04-15 23:40:55 +08:00
|
|
|
|
func NewLogger(ctx context.Context, opts ...Option) *Logger {
|
2024-01-22 18:05:51 +08:00
|
|
|
|
opt := defaultOptions()
|
|
|
|
|
|
for _, apply := range opts {
|
|
|
|
|
|
apply(&opt)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-04-03 21:18:44 +08:00
|
|
|
|
// 验证文件夹权限
|
|
|
|
|
|
if !checkDir(opt.dir) {
|
|
|
|
|
|
panic("文件夹权限不足")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-14 00:14:30 +08:00
|
|
|
|
// 构造函数允许 ctx 为 nil(调用方常把可选 ctx 直接传进来),
|
|
|
|
|
|
// 但后台 goroutine 会调 ctx.Done(),nil 接口会 panic,这里统一兜底。
|
|
|
|
|
|
// 另外提醒:这个 ctx 是「实例生命周期」句柄,应当传进程级 ctx,
|
|
|
|
|
|
// 不要传请求级 ctx —— 请求结束会让清理协程提前退出
|
|
|
|
|
|
if ctx == nil {
|
|
|
|
|
|
ctx = context.Background()
|
2024-01-22 18:05:51 +08:00
|
|
|
|
}
|
2024-01-23 00:12:08 +08:00
|
|
|
|
|
2026-09-14 00:14:30 +08:00
|
|
|
|
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()
|
|
|
|
|
|
}
|
2024-01-22 18:05:51 +08:00
|
|
|
|
|
|
|
|
|
|
// 保存Gin日志写入到文件+控制台
|
2024-01-23 00:12:08 +08:00
|
|
|
|
if opt.isGinLog {
|
2026-09-14 00:14:30 +08:00
|
|
|
|
l.takeOverGinWriters()
|
2024-01-22 18:05:51 +08:00
|
|
|
|
}
|
2024-01-23 13:37:51 +08:00
|
|
|
|
|
|
|
|
|
|
// 日志删除
|
|
|
|
|
|
go l.delete()
|
2026-09-13 21:53:37 +08:00
|
|
|
|
// 定时刷盘
|
|
|
|
|
|
if opt.flushEvery > 0 {
|
|
|
|
|
|
go l.flushLoop()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-04-15 23:40:55 +08:00
|
|
|
|
// 强制刷盘
|
2026-09-13 20:41:27 +08:00
|
|
|
|
// 用 done 而不是只监听 ctx:ctx 为 Background 时 Close 也要能把 goroutine 收回去
|
2024-04-15 23:40:55 +08:00
|
|
|
|
go func() {
|
2026-09-13 20:41:27 +08:00
|
|
|
|
select {
|
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
|
l.MustSync()
|
|
|
|
|
|
case <-l.done:
|
|
|
|
|
|
}
|
2024-04-15 23:40:55 +08:00
|
|
|
|
}()
|
|
|
|
|
|
|
2024-01-22 18:05:51 +08:00
|
|
|
|
return l
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-13 21:53:37 +08:00
|
|
|
|
// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-13 20:41:27 +08:00
|
|
|
|
// 关闭日志:关闭异步队列、落盘并关闭所有文件句柄
|
|
|
|
|
|
// 重复调用安全,调用后本实例的后台 goroutine 会全部退出
|
|
|
|
|
|
func (l *Logger) Close() error {
|
|
|
|
|
|
var err error
|
|
|
|
|
|
l.closeOnce.Do(func() {
|
2026-09-13 21:53:37 +08:00
|
|
|
|
// 1. 先停投递 + 等消费协程把队列里已入队的任务全部写完。
|
|
|
|
|
|
// 这一步必须在「封盘」之前:队列里可能还有别的 channel / event 的任务,
|
|
|
|
|
|
// 它们需要新开自己的文件句柄。如果提前封盘,这些任务会直接写失败被丢掉
|
2026-09-13 20:41:27 +08:00
|
|
|
|
l.drainAsync()
|
|
|
|
|
|
|
2026-09-13 21:53:37 +08:00
|
|
|
|
// 2. 封盘:此后不再新开句柄,避免 Close 扫完句柄表之后又冒出没人负责关的句柄
|
|
|
|
|
|
l.closed.Store(true)
|
2026-09-13 20:41:27 +08:00
|
|
|
|
|
2026-09-13 21:53:37 +08:00
|
|
|
|
// 3. 在写锁内完成最后一次刷盘并关闭所有句柄。此时无锁等待,不会和谁抢锁
|
|
|
|
|
|
l.writeMu.Lock()
|
|
|
|
|
|
err = joinErrors([]error{l.mustSyncLocked(), l.close()})
|
|
|
|
|
|
l.writeMu.Unlock()
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 等后台压缩协程收尾:不等的话 Close 返回后去读归档会读到半截 .gz
|
|
|
|
|
|
l.compWg.Wait()
|
|
|
|
|
|
|
2026-09-14 00:14:30 +08:00
|
|
|
|
// 5. 如果全局 log / gin 写手还指向自己,还原成最初的那个
|
|
|
|
|
|
// 否则 Close 之后 stdlib log 会写进已关闭实例并被静默丢弃
|
|
|
|
|
|
l.releaseGlobalLog()
|
|
|
|
|
|
l.releaseGinWriters()
|
|
|
|
|
|
|
|
|
|
|
|
// 6. 最后再发通用关闭信号,让 delete / ctx 相关 goroutine 退出
|
2026-09-13 20:41:27 +08:00
|
|
|
|
close(l.done)
|
2024-01-23 00:12:08 +08:00
|
|
|
|
})
|
2026-09-13 20:41:27 +08:00
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-14 00:14:30 +08:00
|
|
|
|
// 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)
|
|
|
|
|
|
}()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-13 20:41:27 +08:00
|
|
|
|
// 强制刷盘:只 flush 缓存数据,文件继续可写
|
2026-09-13 21:53:37 +08:00
|
|
|
|
// 与写入并发调用是安全的(内部会先拿到写锁)
|
2026-09-13 20:41:27 +08:00
|
|
|
|
func (l *Logger) MustSync() error {
|
2026-09-13 21:53:37 +08:00
|
|
|
|
l.writeMu.Lock()
|
|
|
|
|
|
defer l.writeMu.Unlock()
|
|
|
|
|
|
return l.mustSyncLocked()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// mustSyncLocked 调用方必须持有 writeMu
|
|
|
|
|
|
// 缓冲的 pending 与 buf 只有在写锁内访问才安全,否则会和 store 并发改动同一块内存
|
|
|
|
|
|
func (l *Logger) mustSyncLocked() error {
|
2026-09-13 20:41:27 +08:00
|
|
|
|
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 {
|
2026-09-13 21:53:37 +08:00
|
|
|
|
// 没有待落盘数据的文件直接跳过:省掉一次 fsync。
|
|
|
|
|
|
// Close 仍然安全:logFile.Close 自己会再 Flush 一次并关句柄
|
|
|
|
|
|
if !f.hasPending() {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
2026-09-13 20:41:27 +08:00
|
|
|
|
errs = append(errs, f.Sync())
|
|
|
|
|
|
}
|
|
|
|
|
|
return joinErrors(errs)
|
2024-01-23 00:12:08 +08:00
|
|
|
|
}
|
2024-01-22 18:05:51 +08:00
|
|
|
|
|
2024-01-23 00:12:08 +08:00
|
|
|
|
func (l *Logger) Channel(ch string) (r *Logger) {
|
|
|
|
|
|
rr := *l
|
|
|
|
|
|
rr.channel = ch
|
|
|
|
|
|
return &rr
|
|
|
|
|
|
}
|
2024-01-22 18:05:51 +08:00
|
|
|
|
|
2024-11-22 20:08:25 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-07-03 16:58:14 +08:00
|
|
|
|
// 获取TraceField的字段
|
|
|
|
|
|
func (l *Logger) GetTraceField() string {
|
|
|
|
|
|
return l.option.traceField
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-01-23 00:12:08 +08:00
|
|
|
|
// 实现io.Writer接口
|
2024-01-22 18:05:51 +08:00
|
|
|
|
func (l *Logger) Write(b []byte) (n int, err error) {
|
2024-01-23 00:12:08 +08:00
|
|
|
|
return l.write("info", b)
|
2024-01-22 18:05:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-23 19:17:40 +08:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-04-03 21:18:44 +08:00
|
|
|
|
// 验证文件夹权限
|
|
|
|
|
|
// 根文件夹如果不存在则创建
|
|
|
|
|
|
func checkDir(dir string) bool {
|
|
|
|
|
|
if _, err := os.Stat(dir); err != nil {
|
|
|
|
|
|
if os.IsNotExist(err) {
|
2026-09-13 20:41:27 +08:00
|
|
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
2024-04-03 21:18:44 +08:00
|
|
|
|
log.Println("创建文件夹失败", err)
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
2026-09-13 20:41:27 +08:00
|
|
|
|
return true
|
2024-04-03 21:18:44 +08:00
|
|
|
|
}
|
2026-09-13 20:41:27 +08:00
|
|
|
|
// 目标存在但不可访问(权限不足等),此时返回 false 才是这个函数的本意
|
|
|
|
|
|
log.Println("日志文件夹不可用", dir, err)
|
|
|
|
|
|
return false
|
2024-04-03 21:18:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|