优化一些问题&修复一些BUG
This commit is contained in:
@@ -8,11 +8,14 @@ import (
|
||||
"github.com/yuninks/loggerx/middleware"
|
||||
)
|
||||
|
||||
// 运行: go run ./example/gin_server
|
||||
// curl --location '127.0.0.1:8080/ping'
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
log := loggerx.NewLogger(ctx, loggerx.SetToConsole())
|
||||
// 进程退出前落盘并释放文件句柄
|
||||
defer log.Close()
|
||||
|
||||
g := gin.Default()
|
||||
|
||||
@@ -25,5 +28,4 @@ func main() {
|
||||
})
|
||||
|
||||
g.Run(":8080")
|
||||
|
||||
}
|
||||
@@ -17,6 +17,7 @@ func main() {
|
||||
loggerx.SetEscapeHTML(false),
|
||||
// loggerx.SetExpandData("ddd", "dddd"),
|
||||
)
|
||||
defer log.Close()
|
||||
log.WriteAsync().Info(ctx, "{ \"a\": 1, \"b\": 2 }")
|
||||
log.Info(ctx, "哈哈哈2")
|
||||
log.Info(ctx, "哈哈哈2")
|
||||
|
||||
+152
-49
@@ -1,7 +1,9 @@
|
||||
package loggerx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
@@ -9,99 +11,200 @@ import (
|
||||
|
||||
// 文件操作
|
||||
|
||||
// 每个文件的写缓冲大小:越大越省系统调用,代价是崩溃时可能丢最后一批日志
|
||||
const fileBufSize = 32 * 1024
|
||||
|
||||
// logFile 一个已打开的日志文件句柄
|
||||
// 自己管理写缓冲(不用 bufio):bufio.Writer 在底层短写时会永久置位内部错误状态,
|
||||
// 之后所有写入和 Flush 都会失败,已攒下的一批日志会整批丢掉
|
||||
type logFile struct {
|
||||
file *os.File
|
||||
buf []byte
|
||||
pending int
|
||||
fileName string
|
||||
}
|
||||
|
||||
// Write 写入缓冲,满了就落盘一次
|
||||
func (f *logFile) Write(b []byte) (int, error) {
|
||||
written := 0
|
||||
for len(b) > 0 {
|
||||
if f.pending == len(f.buf) {
|
||||
if err := f.Flush(); err != nil {
|
||||
return written, err
|
||||
}
|
||||
}
|
||||
n := copy(f.buf[f.pending:], b)
|
||||
f.pending += n
|
||||
written += n
|
||||
b = b[n:]
|
||||
}
|
||||
return written, nil
|
||||
}
|
||||
|
||||
// writeFull 保证把整块数据写进文件(允许底层短写,循环补齐)
|
||||
func (f *logFile) writeFull(p []byte) error {
|
||||
for len(p) > 0 {
|
||||
n, err := f.file.Write(p)
|
||||
p = p[n:]
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Flush 把缓冲写入文件(不做 fsync)
|
||||
func (f *logFile) Flush() error {
|
||||
if f.pending == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := f.writeFull(f.buf[:f.pending]); err != nil {
|
||||
// 保留未写成功的部分,下次继续
|
||||
return err
|
||||
}
|
||||
f.pending = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sync 把缓冲刷到文件并 fsync
|
||||
func (f *logFile) Sync() error {
|
||||
var errs []error
|
||||
if err := f.Flush(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if f.file != nil {
|
||||
if err := f.file.Sync(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return joinErrors(errs)
|
||||
}
|
||||
|
||||
// Close 落盘并关闭文件
|
||||
func (f *logFile) Close() error {
|
||||
return joinErrors([]error{f.Sync(), f.file.Close()})
|
||||
}
|
||||
|
||||
// joinErrors 汇总错误,Go 1.20+ 用 errors.Join
|
||||
func joinErrors(errs []error) error {
|
||||
var real []error
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
real = append(real, err)
|
||||
}
|
||||
}
|
||||
if len(real) == 0 {
|
||||
return nil
|
||||
}
|
||||
return errors.Join(real...)
|
||||
}
|
||||
|
||||
// 获取最新的文件名
|
||||
func (l *Logger) nowFileName(event string) string {
|
||||
// ioc, _ := time.LoadLocation("Asia/Shanghai")
|
||||
// timeDir := fmt.Sprint(time.Now().In(ioc).Format("2006/01/02/15")) // 2006-01-02 15:04:05
|
||||
|
||||
prefix := ""
|
||||
|
||||
switch l.option.fileSplit {
|
||||
case FileSplitTimeA:
|
||||
// (年/月/日/时)
|
||||
prefix = fmt.Sprint(time.Now().In(l.option.timeZone).Format("2006/01/02/15")) // 2006-01-02 15:04:05
|
||||
prefix = time.Now().In(l.option.timeZone).Format("2006/01/02/15")
|
||||
case FileSplitTimeB:
|
||||
// (年/月/日)
|
||||
prefix = fmt.Sprint(time.Now().In(l.option.timeZone).Format("2006/01/02")) // 2006-01-02 15:04:05
|
||||
prefix = time.Now().In(l.option.timeZone).Format("2006/01/02")
|
||||
case FileSplitTimeC:
|
||||
// (年/月-日)
|
||||
prefix = fmt.Sprint(time.Now().In(l.option.timeZone).Format("2006/01-02")) // 2006-01-02 15:04:05
|
||||
prefix = time.Now().In(l.option.timeZone).Format("2006/01-02")
|
||||
case FileSplitTimeD:
|
||||
// (年-月-日-时)
|
||||
prefix = fmt.Sprint(time.Now().In(l.option.timeZone).Format("2006-01-02-15")) // 2006-01-02 15:04:05
|
||||
prefix = time.Now().In(l.option.timeZone).Format("2006-01-02-15")
|
||||
case FileSplitTimeE:
|
||||
// (年-月-日)
|
||||
prefix = fmt.Sprint(time.Now().In(l.option.timeZone).Format("2006-01-02")) // 2006-01-02 15:04:05
|
||||
prefix = time.Now().In(l.option.timeZone).Format("2006-01-02")
|
||||
}
|
||||
|
||||
if prefix != "" {
|
||||
prefix = prefix + "_"
|
||||
}
|
||||
|
||||
// timeDir := fmt.Sprint(time.Now().Local().Format("2006/01/02")) // 2006-01-02 15:04:05
|
||||
if l.channel != "" {
|
||||
prefix = l.channel + "/" + prefix
|
||||
}
|
||||
path := l.option.dir + "/" + prefix + event + ".log"
|
||||
// fmt.Println(filepath.Abs(path))
|
||||
return path
|
||||
return l.option.dir + "/" + prefix + event + ".log"
|
||||
}
|
||||
|
||||
// 新建文件
|
||||
func (l *Logger) getFile(event string, isRefresh bool) (*os.File, error) {
|
||||
f := l.loadFile(event)
|
||||
if f != nil && !isRefresh {
|
||||
func (l *Logger) getFile(event string) (*logFile, error) {
|
||||
key := fileKey{channel: l.channel, event: event}
|
||||
|
||||
if f := l.loadFile(key); f != nil {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
// 双检:可能在等锁期间已经被别的 goroutine 建好了
|
||||
if f := l.loadFileLocked(key); f != nil {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
fileName := l.nowFileName(event)
|
||||
|
||||
dir, _ := filepath.Split(fileName) // 识别目录与文件
|
||||
os.MkdirAll(dir, os.ModePerm) // 创建多层目录,如果存在不会报错
|
||||
|
||||
// 打开该文件,如果不存在则创建
|
||||
file, err := os.OpenFile(fileName, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
// 打开失败,尝试创建
|
||||
fmt.Println("打开日志文件失败")
|
||||
// 识别目录与文件,创建多层目录(已存在不报错)
|
||||
if dir := filepath.Dir(fileName); dir != "" {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 关闭原来的
|
||||
if f != nil {
|
||||
closeFile(f)
|
||||
}
|
||||
|
||||
l.filePath.Store(l.channel, &filePath{
|
||||
file, err := os.OpenFile(fileName, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("打开日志文件失败 %s: %w", fileName, err)
|
||||
}
|
||||
|
||||
lf := &logFile{
|
||||
file: file,
|
||||
buf: make([]byte, fileBufSize),
|
||||
fileName: fileName,
|
||||
})
|
||||
}
|
||||
l.filePath[key] = lf
|
||||
|
||||
return file, nil
|
||||
return lf, nil
|
||||
}
|
||||
|
||||
// 加载文件
|
||||
func (l *Logger) loadFile(event string) *os.File {
|
||||
val, ok := l.filePath.Load(l.channel)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
f := val.(*filePath)
|
||||
if f == nil {
|
||||
return nil
|
||||
}
|
||||
if f.fileName != l.nowFileName(event) {
|
||||
// 原来的文件需关闭
|
||||
closeFile(f.file)
|
||||
return nil
|
||||
}
|
||||
return f.file
|
||||
// 加载文件(读锁)
|
||||
func (l *Logger) loadFile(key fileKey) *logFile {
|
||||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
return l.loadFileLocked(key)
|
||||
}
|
||||
|
||||
// 关闭文件
|
||||
func closeFile(f *os.File) error {
|
||||
f.Sync()
|
||||
return f.Close()
|
||||
// loadFileLocked 调用方必须已持有锁
|
||||
func (l *Logger) loadFileLocked(key fileKey) *logFile {
|
||||
f, ok := l.filePath[key]
|
||||
if !ok || f == nil {
|
||||
return nil
|
||||
}
|
||||
// 时间切割导致文件名变化:关闭旧文件,让调用方按新名字重建
|
||||
if f.fileName != l.nowFileName(key.event) {
|
||||
delete(l.filePath, key)
|
||||
f.Close()
|
||||
return nil
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// close 关闭所有文件句柄(Close 专用)
|
||||
func (l *Logger) close() error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
var errs []error
|
||||
for key, f := range l.filePath {
|
||||
errs = append(errs, f.Close())
|
||||
delete(l.filePath, key)
|
||||
}
|
||||
return joinErrors(errs)
|
||||
}
|
||||
|
||||
@@ -7,75 +7,133 @@ import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (l *Logger) logger(ctx context.Context, event string, v ...any) {
|
||||
pc, file, line, _ := runtime.Caller(2)
|
||||
// fmt.Println("runtime.Caller", pc, file, line, ok)
|
||||
// 调用方可能是 log 包(Logger.Write -> logger),所以这里取第 2 层
|
||||
pc, file, line, ok := runtime.Caller(2)
|
||||
|
||||
basePath, _ := filepath.Abs("./")
|
||||
var funcName string
|
||||
if ok && pc != 0 {
|
||||
if fn := runtime.FuncForPC(pc); fn != nil {
|
||||
funcName = strings.TrimPrefix(filepath.Ext(fn.Name()), ".")
|
||||
}
|
||||
if basePath, err := filepath.Abs("./"); err == nil {
|
||||
basePath = strings.ReplaceAll(basePath, "\\", "/")
|
||||
// fmt.Println("basePath", basePath)
|
||||
|
||||
file = strings.TrimPrefix(file, basePath)
|
||||
|
||||
funcName := runtime.FuncForPC(pc).Name()
|
||||
funcName = filepath.Ext(funcName)
|
||||
funcName = strings.TrimPrefix(funcName, ".")
|
||||
|
||||
// by, _ := json.Marshal(v)
|
||||
file = strings.TrimPrefix(strings.ReplaceAll(file, "\\", "/"), basePath)
|
||||
}
|
||||
}
|
||||
|
||||
nowTime := time.Now().In(l.option.timeZone).Format("2006-01-02 15:04:05.000000")
|
||||
|
||||
traceId, _ := ctx.Value(l.option.traceField).(string)
|
||||
|
||||
// writeStr := "[" + event + "]" + nowTime + " " + file + ":" + fmt.Sprintf("%d", line) + " " + funcName + " gid:" + getGID() + " " + traceId + " @data@: " + string(by) + "\n\n"
|
||||
// ctx 允许为 nil,不能直接调 Value
|
||||
var traceId string
|
||||
if ctx != nil {
|
||||
traceId, _ = ctx.Value(l.option.traceField).(string)
|
||||
}
|
||||
|
||||
// error 转成带堆栈的字符串(%+v 会带上 pkg/errors 的调用栈)
|
||||
for idx, val := range v {
|
||||
if _, ok := val.(error); ok {
|
||||
if _, isErr := val.(error); isErr {
|
||||
v[idx] = fmt.Sprintf("%+v", val)
|
||||
}
|
||||
}
|
||||
|
||||
var gid string
|
||||
if l.option.isGid {
|
||||
gid = getGID()
|
||||
}
|
||||
|
||||
fd := FormatData{
|
||||
Time: nowTime,
|
||||
File: file + ":" + fmt.Sprintf("%d", line),
|
||||
File: file + ":" + strconv.Itoa(line),
|
||||
Func: funcName,
|
||||
Gid: getGID(),
|
||||
Gid: gid,
|
||||
Content: v,
|
||||
TraceId: traceId,
|
||||
Expand: l.option.expandData,
|
||||
}
|
||||
|
||||
if event == "error" {
|
||||
// fd.Stack = string(debug.Stack())
|
||||
fdb := marshalLog(fd, nowTime, file, line, l.option.escapeHTML, gid, traceId)
|
||||
|
||||
fdb = append([]byte("\n["+event+"]"), fdb...)
|
||||
|
||||
_, _ = l.write(event, fdb)
|
||||
|
||||
if l.option.errorToInfo && event == "error" {
|
||||
_, _ = l.write("info", fdb)
|
||||
}
|
||||
}
|
||||
|
||||
var fdb []byte
|
||||
if l.option.escapeHTML {
|
||||
fdb, _ = json.Marshal(fd)
|
||||
// marshalLog 序列化日志内容
|
||||
// json 序列化失败时(chan/func 等不可序列化类型,或循环引用)不能静默丢弃,
|
||||
// 否则只会留下一行没有内容的 "["+event+"]",需要降级成可读的文本
|
||||
func marshalLog(fd FormatData, nowTime, file string, line int, escapeHTML bool, gid, traceId string) []byte {
|
||||
var (
|
||||
b []byte
|
||||
err error
|
||||
)
|
||||
if escapeHTML {
|
||||
b, err = json.Marshal(fd)
|
||||
} else {
|
||||
// 非转义
|
||||
var buf bytes.Buffer
|
||||
encoder := json.NewEncoder(&buf)
|
||||
encoder.SetEscapeHTML(false)
|
||||
encoder.Encode(fd)
|
||||
fdb = buf.Bytes()
|
||||
err = encoder.Encode(fd)
|
||||
b = bytes.TrimRight(buf.Bytes(), "\n")
|
||||
}
|
||||
fdb = bytes.TrimRight(fdb, "\n")
|
||||
|
||||
ff := []byte("\n[" + event + "]")
|
||||
fdb = append(ff, fdb...)
|
||||
|
||||
l.write(event, fdb)
|
||||
|
||||
if l.option.errorToInfo && event == "error" {
|
||||
l.write("info", fdb)
|
||||
if err == nil {
|
||||
return b
|
||||
}
|
||||
|
||||
// log.Println("" + string(by))
|
||||
// 降级:逐字段尝试,失败的内容用 %+v 兜底
|
||||
fallback := FormatData{
|
||||
Time: nowTime,
|
||||
File: file + ":" + strconv.Itoa(line),
|
||||
Func: fd.Func,
|
||||
Gid: gid,
|
||||
TraceId: traceId,
|
||||
Content: stringifyValues(fd.Content),
|
||||
Expand: fd.Expand,
|
||||
}
|
||||
if fb, ferr := json.Marshal(fallback); ferr == nil {
|
||||
return fb
|
||||
}
|
||||
|
||||
return []byte(fmt.Sprintf(`{"time":%q,"content":%q,"marshal_error":%q}`,
|
||||
nowTime, fmt.Sprintf("%+v", fd.Content), err.Error()))
|
||||
}
|
||||
|
||||
// stringifyValues 把不可序列化的值转成字符串
|
||||
func stringifyValues(v any) []string {
|
||||
switch val := v.(type) {
|
||||
case nil:
|
||||
return nil
|
||||
case []any:
|
||||
out := make([]string, 0, len(val))
|
||||
for _, item := range val {
|
||||
out = append(out, fmt.Sprintf("%+v", item))
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return []string{fmt.Sprintf("%+v", val)}
|
||||
}
|
||||
}
|
||||
|
||||
// getGID 获取当前 goroutine 的 ID
|
||||
func getGID() string {
|
||||
b := make([]byte, 64)
|
||||
b = b[:runtime.Stack(b, false)]
|
||||
b = bytes.TrimPrefix(b, []byte("goroutine "))
|
||||
idx := bytes.IndexByte(b, ' ')
|
||||
if idx < 0 {
|
||||
// 解析失败时不要 panic
|
||||
return "0"
|
||||
}
|
||||
return string(b[:idx])
|
||||
}
|
||||
|
||||
type FormatData struct {
|
||||
|
||||
+78
-33
@@ -5,13 +5,11 @@ package loggerx
|
||||
// desc: 日志封装类
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
// "sync_log/global"
|
||||
@@ -22,16 +20,37 @@ import (
|
||||
// 需要实现io.Writer接口
|
||||
type Logger struct {
|
||||
ctx context.Context
|
||||
filePath *sync.Map // filePath
|
||||
mu *sync.Mutex
|
||||
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
|
||||
}
|
||||
|
||||
type filePath struct {
|
||||
file *os.File
|
||||
fileName string
|
||||
// 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 {
|
||||
@@ -47,10 +66,15 @@ func NewLogger(ctx context.Context, opts ...Option) *Logger {
|
||||
|
||||
l := &Logger{
|
||||
ctx: ctx,
|
||||
filePath: &sync.Map{},
|
||||
mu: &sync.Mutex{},
|
||||
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)
|
||||
@@ -66,21 +90,54 @@ func NewLogger(ctx context.Context, opts ...Option) *Logger {
|
||||
go l.delete()
|
||||
|
||||
// 强制刷盘
|
||||
// 用 done 而不是只监听 ctx:ctx 为 Background 时 Close 也要能把 goroutine 收回去
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
l.MustSync()
|
||||
case <-l.done:
|
||||
}
|
||||
}()
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// 强制刷盘
|
||||
func (l *Logger) MustSync() {
|
||||
l.filePath.Range(func(key, value any) bool {
|
||||
f := value.(*filePath)
|
||||
f.file.Sync()
|
||||
return true
|
||||
// 关闭日志:关闭异步队列、落盘并关闭所有文件句柄
|
||||
// 重复调用安全,调用后本实例的后台 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) {
|
||||
@@ -147,32 +204,20 @@ func (l *Logger) Warnf(ctx context.Context, format string, v ...any) {
|
||||
l.logger(ctx, "warn", s)
|
||||
}
|
||||
|
||||
// 添加固定的内容
|
||||
// func (l *Logger) ContextWithFields(ctx context.Context, v ...any) {
|
||||
// l.logger(ctx, "add", v...)
|
||||
// }
|
||||
// func (l *Logger) Field(key,val string) {
|
||||
// l.logger(nil, "add", key,val)
|
||||
// }
|
||||
|
||||
func getGID() string {
|
||||
b := make([]byte, 64)
|
||||
b = b[:runtime.Stack(b, false)]
|
||||
b = bytes.TrimPrefix(b, []byte("goroutine "))
|
||||
b = b[:bytes.IndexByte(b, ' ')]
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// 验证文件夹权限
|
||||
// 根文件夹如果不存在则创建
|
||||
func checkDir(dir string) bool {
|
||||
if _, err := os.Stat(dir); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
log.Println("创建文件夹失败", err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
// 目标存在但不可访问(权限不足等),此时返回 false 才是这个函数的本意
|
||||
log.Println("日志文件夹不可用", dir, err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,380 @@
|
||||
package loggerx_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/yuninks/loggerx"
|
||||
)
|
||||
|
||||
// newTestLogger 统一构造测试实例,测试结束关闭句柄
|
||||
func newTestLogger(t *testing.T, opts ...loggerx.Option) (*loggerx.Logger, string) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
opts = append([]loggerx.Option{loggerx.SetDir(dir)}, opts...)
|
||||
l := loggerx.NewLogger(context.Background(), opts...)
|
||||
t.Cleanup(func() {
|
||||
if err := l.Close(); err != nil {
|
||||
t.Errorf("Close 返回错误: %v", err)
|
||||
}
|
||||
})
|
||||
return l, dir
|
||||
}
|
||||
|
||||
// syncAndRead 先刷盘再读取(写入走 bufio,落盘后才能看到)
|
||||
func syncAndRead(t *testing.T, l *loggerx.Logger, dir, event string) string {
|
||||
t.Helper()
|
||||
if err := l.MustSync(); err != nil {
|
||||
t.Fatalf("MustSync: %v", err)
|
||||
}
|
||||
return readLog(t, dir, event)
|
||||
}
|
||||
|
||||
// tailOf 取字符串末尾 n 个字节,用于失败时打印现场
|
||||
func tailOf(s string, n int) string {
|
||||
if len(s) <= n {
|
||||
return s
|
||||
}
|
||||
return s[len(s)-n:]
|
||||
}
|
||||
|
||||
// readLog 读取切割后的日志文件(文件名可能带日期前缀)
|
||||
func readLog(t *testing.T, dir, event string) string {
|
||||
t.Helper()
|
||||
files, err := filepath.Glob(filepath.Join(dir, "*_"+event+".log"))
|
||||
if err != nil {
|
||||
t.Fatalf("glob %s: %v", event, err)
|
||||
}
|
||||
if len(files) == 0 {
|
||||
files, _ = filepath.Glob(filepath.Join(dir, event+".log"))
|
||||
}
|
||||
var sb strings.Builder
|
||||
for _, f := range files {
|
||||
b, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("读取 %s: %v", f, err)
|
||||
}
|
||||
sb.Write(b)
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// ---------------- 修复1:文件句柄缓存键必须包含 event ----------------
|
||||
|
||||
// info/error 交替写入后,各自文件的内容必须完整且不串台
|
||||
func TestEventHandlerIsolation(t *testing.T) {
|
||||
l, dir := newTestLogger(t)
|
||||
|
||||
const n = 50
|
||||
for i := 0; i < n; i++ {
|
||||
l.Infof(context.Background(), "INFO-%d", i)
|
||||
l.Errorf(context.Background(), "ERR-%d", i)
|
||||
}
|
||||
|
||||
info, errl := syncAndRead(t, l, dir, "info"), syncAndRead(t, l, dir, "error")
|
||||
if got := strings.Count(info, "INFO-"); got != n {
|
||||
t.Errorf("info.log 条数 = %d, 期望 %d", got, n)
|
||||
}
|
||||
if got := strings.Count(errl, "ERR-"); got != n {
|
||||
t.Errorf("error.log 条数 = %d, 期望 %d", got, n)
|
||||
}
|
||||
if strings.Contains(info, "ERR-") {
|
||||
t.Error("info.log 中混入了 error 日志")
|
||||
}
|
||||
if strings.Contains(errl, "INFO-") {
|
||||
t.Error("error.log 中混入了 info 日志")
|
||||
}
|
||||
}
|
||||
|
||||
// 不同 channel 也必须各自独立
|
||||
func TestChannelIsolation(t *testing.T) {
|
||||
l, dir := newTestLogger(t)
|
||||
l.Channel("c1").Info(context.Background(), "IN-C1")
|
||||
l.Channel("c2").Info(context.Background(), "IN-C2")
|
||||
|
||||
if s := syncAndRead(t, l, dir, "info"); strings.Contains(s, "IN-C2") {
|
||||
t.Error("根 channel 的日志里混入了 c2 的内容")
|
||||
}
|
||||
// channel 的日志在子目录里,同样带时间前缀
|
||||
c1, err := filepath.Glob(filepath.Join(dir, "c1", "*_info.log"))
|
||||
if err != nil || len(c1) == 0 {
|
||||
t.Fatalf("没有找到 c1 的日志文件: %v", err)
|
||||
}
|
||||
b, err := os.ReadFile(c1[0])
|
||||
if err != nil {
|
||||
t.Fatalf("读取 c1 日志: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(b), "IN-C1") {
|
||||
t.Errorf("c1 日志内容不正确: %s", string(b))
|
||||
}
|
||||
}
|
||||
|
||||
// 事件交替写入的性能:不能因为切换事件而反复重开文件
|
||||
func BenchmarkInfoOnly(b *testing.B) {
|
||||
l := loggerx.NewLogger(context.Background(), loggerx.SetDir(b.TempDir()))
|
||||
defer l.Close()
|
||||
ctx := context.Background()
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
l.Infof(ctx, "hello %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkInfoErrorInterleaved(b *testing.B) {
|
||||
l := loggerx.NewLogger(context.Background(), loggerx.SetDir(b.TempDir()))
|
||||
defer l.Close()
|
||||
ctx := context.Background()
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
l.Infof(ctx, "hello %d", i)
|
||||
l.Errorf(ctx, "hello %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- 修复2:io.Writer 契约 ----------------
|
||||
|
||||
func TestWriteSatisfiesIOWriterContract(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
opts []loggerx.Option
|
||||
}{
|
||||
{"默认(写文件)", nil},
|
||||
{"只写驱动(不写文件)", []loggerx.Option{loggerx.SetPrintFile(false), loggerx.SetToConsole()}},
|
||||
{"文件+附加驱动", []loggerx.Option{loggerx.SetExtraDriver(io.Discard)}},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
l, _ := newTestLogger(t, c.opts...)
|
||||
b := []byte("hello io.Writer\n")
|
||||
n, err := l.Write(b)
|
||||
if err != nil {
|
||||
t.Fatalf("Write 返回错误: %v", err)
|
||||
}
|
||||
if n != len(b) {
|
||||
t.Fatalf("Write 返回 n=%d, 期望 len(b)=%d(违反 io.Writer 契约)", n, len(b))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 通过标准库 log 写入也必须成功:log 在短写时会报 io.ErrShortWrite
|
||||
func TestStdLogWriteNoShortWriteError(t *testing.T) {
|
||||
l, dir := newTestLogger(t)
|
||||
|
||||
// log.SetOutput 是全局状态,恢复现场
|
||||
prev := log.Writer()
|
||||
defer log.SetOutput(prev)
|
||||
log.SetOutput(l)
|
||||
|
||||
if err := log.Output(2, "via standard log"); err != nil {
|
||||
t.Fatalf("标准 log 写入失败: %v", err)
|
||||
}
|
||||
if s := syncAndRead(t, l, dir, "info"); !strings.Contains(s, "via standard log") {
|
||||
t.Errorf("日志内容未落盘: %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- 修复3:句柄释放与刷盘 ----------------
|
||||
|
||||
// Close 之后文件必须能被删除(Windows 上句柄没关是删不掉的)
|
||||
func TestCloseReleasesFileHandles(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
l := loggerx.NewLogger(context.Background(), loggerx.SetDir(dir))
|
||||
l.Info(context.Background(), "before close")
|
||||
|
||||
if err := l.Close(); err != nil {
|
||||
t.Fatalf("Close: %v", err)
|
||||
}
|
||||
|
||||
files, _ := filepath.Glob(filepath.Join(dir, "*.log"))
|
||||
if len(files) == 0 {
|
||||
t.Fatal("没有生成日志文件")
|
||||
}
|
||||
for _, f := range files {
|
||||
if err := os.Remove(f); err != nil {
|
||||
t.Errorf("Close 后仍无法删除 %s: %v", filepath.Base(f), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close 必须先把缓存里的内容刷到磁盘
|
||||
func TestCloseFlushesBufferedData(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
l := loggerx.NewLogger(context.Background(), loggerx.SetDir(dir))
|
||||
l.Info(context.Background(), "buffered-content")
|
||||
|
||||
if err := l.Close(); err != nil {
|
||||
t.Fatalf("Close: %v", err)
|
||||
}
|
||||
files, _ := filepath.Glob(filepath.Join(dir, "*.log"))
|
||||
if len(files) != 1 {
|
||||
t.Fatalf("期望 1 个日志文件, 实际 %d", len(files))
|
||||
}
|
||||
b, err := os.ReadFile(files[0])
|
||||
if err != nil {
|
||||
t.Fatalf("读取: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(b), "buffered-content") {
|
||||
t.Errorf("Close 后内容丢失: %q", string(b))
|
||||
}
|
||||
}
|
||||
|
||||
// 重复 Close 必须安全
|
||||
func TestCloseIsIdempotent(t *testing.T) {
|
||||
l := loggerx.NewLogger(context.Background(), loggerx.SetDir(t.TempDir()))
|
||||
if err := l.Close(); err != nil {
|
||||
t.Fatalf("首次 Close: %v", err)
|
||||
}
|
||||
if err := l.Close(); err != nil {
|
||||
t.Fatalf("重复 Close: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 异步写入 + Close:队列里的内容不能丢
|
||||
func TestCloseDrainsAsyncQueue(t *testing.T) {
|
||||
for _, n := range []int{10, 100, 197, 198, 199, 200, 201, 250} {
|
||||
t.Run(fmt.Sprintf("n=%d", n), func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
l := loggerx.NewLogger(context.Background(), loggerx.SetDir(dir), loggerx.SetWriteAsync())
|
||||
|
||||
al := l.WriteAsync()
|
||||
for i := 0; i < n; i++ {
|
||||
al.Infof(context.Background(), "ASYNC-%d", i)
|
||||
}
|
||||
if err := l.Close(); err != nil {
|
||||
t.Fatalf("Close: %v", err)
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
files, _ := filepath.Glob(filepath.Join(dir, "*.log"))
|
||||
for _, f := range files {
|
||||
b, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("读取 %s: %v", f, err)
|
||||
}
|
||||
sb.Write(b)
|
||||
}
|
||||
content := sb.String()
|
||||
|
||||
missing := make([]string, 0)
|
||||
for i := 0; i < n; i++ {
|
||||
if !strings.Contains(content, fmt.Sprintf(`ASYNC-%d"`, i)) {
|
||||
missing = append(missing, fmt.Sprint(i))
|
||||
}
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
cnt := strings.Count(content, "[info]")
|
||||
t.Errorf("丢 %d 条: %s | 实际日志行数=%d n=%d 结尾=%q", len(missing), strings.Join(missing, ","), cnt, n, tailOf(content, 160))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 用 context.Background 创建实例,Close 后后台 goroutine 必须退出
|
||||
func TestCloseStopsGoroutines(t *testing.T) {
|
||||
before := runtime.NumGoroutine()
|
||||
|
||||
loggers := make([]*loggerx.Logger, 0, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
loggers = append(loggers, loggerx.NewLogger(context.Background(), loggerx.SetDir(t.TempDir())))
|
||||
}
|
||||
for _, l := range loggers {
|
||||
if err := l.Close(); err != nil {
|
||||
t.Fatalf("Close: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(3 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
if runtime.NumGoroutine() <= before {
|
||||
return
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
t.Errorf("Close 后 goroutine 未回收: before=%d after=%d", before, runtime.NumGoroutine())
|
||||
}
|
||||
|
||||
// ---------------- 修复4:nil ctx / 序列化失败 ----------------
|
||||
|
||||
func TestNilContextDoesNotPanic(t *testing.T) {
|
||||
l, dir := newTestLogger(t)
|
||||
// nolint:staticcheck // 故意传 nil,验证不 panic
|
||||
l.Info(nil, "nil ctx")
|
||||
if s := syncAndRead(t, l, dir, "info"); !strings.Contains(s, "nil ctx") {
|
||||
t.Errorf("nil ctx 时日志未写入: %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
// 不可序列化的内容不能产出空行 / 坏 JSON
|
||||
func TestUnmarshalableContentProducesValidJSON(t *testing.T) {
|
||||
l, dir := newTestLogger(t)
|
||||
l.Info(context.Background(), make(chan int), func() {}, map[string]any{"ok": 1})
|
||||
l.Infof(context.Background(), "后面这条必须还在")
|
||||
|
||||
content := syncAndRead(t, l, dir, "info")
|
||||
lines := strings.Split(strings.TrimSpace(content), "\n")
|
||||
if len(lines) != 2 {
|
||||
t.Fatalf("期望 2 行日志, 实际 %d 行: %q", len(lines), content)
|
||||
}
|
||||
for i, line := range lines {
|
||||
body := strings.TrimPrefix(line, "[info]")
|
||||
if body == "" {
|
||||
t.Fatalf("第 %d 行是空的 [info] 裸行(序列化失败被静默丢弃)", i+1)
|
||||
}
|
||||
var v map[string]any
|
||||
if err := json.Unmarshal([]byte(body), &v); err != nil {
|
||||
t.Fatalf("第 %d 行不是合法 JSON: %v\n内容: %s", i+1, err, body)
|
||||
}
|
||||
if i == 0 && v["content"] == nil {
|
||||
t.Error("序列化失败时 content 丢失,无法定位问题")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 并发写入:数据不能串、不能丢
|
||||
func TestConcurrentWriteIntegrity(t *testing.T) {
|
||||
l, dir := newTestLogger(t)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
const perLevel = 300
|
||||
for i := 0; i < perLevel; i++ {
|
||||
wg.Add(3)
|
||||
go func(i int) { defer wg.Done(); l.Infof(context.Background(), "INFO-%d", i) }(i)
|
||||
go func(i int) { defer wg.Done(); l.Errorf(context.Background(), "ERR-%d", i) }(i)
|
||||
go func(i int) { defer wg.Done(); l.Channel("cc").Infof(context.Background(), "CC-%d", i) }(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
if got := strings.Count(syncAndRead(t, l, dir, "info"), "INFO-"); got != perLevel {
|
||||
t.Errorf("info 条数 = %d, 期望 %d", got, perLevel)
|
||||
}
|
||||
if got := strings.Count(syncAndRead(t, l, dir, "error"), "ERR-"); got != perLevel {
|
||||
t.Errorf("error 条数 = %d, 期望 %d", got, perLevel)
|
||||
}
|
||||
// cc 是子 channel,日志落在 cc/ 子目录
|
||||
ccFiles, _ := filepath.Glob(filepath.Join(dir, "cc", "*_info.log"))
|
||||
if len(ccFiles) == 0 {
|
||||
t.Fatal("没有找到 cc 的日志文件")
|
||||
}
|
||||
var ccb strings.Builder
|
||||
for _, f := range ccFiles {
|
||||
b, _ := os.ReadFile(f)
|
||||
ccb.Write(b)
|
||||
}
|
||||
if got := strings.Count(ccb.String(), "CC-"); got != perLevel {
|
||||
t.Errorf("cc 条数 = %d, 期望 %d", got, perLevel)
|
||||
}
|
||||
}
|
||||
@@ -10,35 +10,48 @@ import (
|
||||
// 监听dir文件夹的所有文件,循环查找是否有超过days天的文件,删除掉
|
||||
|
||||
func (l *Logger) delete() {
|
||||
|
||||
tick := time.NewTicker(time.Hour)
|
||||
defer tick.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-tick.C:
|
||||
err := l.walkAndDel()
|
||||
if err != nil {
|
||||
// 后台清理不能把整个进程带崩
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println("清理日志文件异常:", r)
|
||||
}
|
||||
}()
|
||||
if err := l.walkAndDel(); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}()
|
||||
case <-l.ctx.Done():
|
||||
return
|
||||
case <-l.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) walkAndDel() error {
|
||||
err := filepath.Walk(l.option.dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
// 没设置删除天数
|
||||
// 没设置删除天数(<=0 表示不删除)
|
||||
if l.option.days <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 判断最后修改时间是否大于3天
|
||||
if info.ModTime().After(time.Now().AddDate(0, 0, -l.option.days)) {
|
||||
deadline := time.Now().AddDate(0, 0, -l.option.days)
|
||||
|
||||
err := filepath.Walk(l.option.dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
// 单个文件出错不应该中断整个遍历
|
||||
fmt.Println(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 最后修改时间在保留期内,跳过
|
||||
if info.ModTime().After(deadline) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -46,14 +59,18 @@ func (l *Logger) walkAndDel() error {
|
||||
if !isEmptyDir(path) {
|
||||
return nil
|
||||
}
|
||||
} else if filepath.Ext(path) != ".log" {
|
||||
return nil
|
||||
}
|
||||
ext := filepath.Ext(path)
|
||||
if ext != ".log" {
|
||||
|
||||
// 删除文件 / 空目录
|
||||
if rerr := os.Remove(path); rerr != nil {
|
||||
// 正被写入的文件在 Windows 上删不掉,等下一轮再试,不要终止遍历
|
||||
fmt.Println("删除文件失败", path, rerr)
|
||||
return nil
|
||||
}
|
||||
// 删除文件
|
||||
fmt.Println("删除文件", path)
|
||||
return os.Remove(path)
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
|
||||
+113
-26
@@ -2,71 +2,158 @@ package loggerx
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 异步队列容量,跟随实例
|
||||
const asyncQueueSize = 1000
|
||||
|
||||
// Close 等待异步日志落盘的最长时间
|
||||
const closeDrainTimeout = 5 * time.Second
|
||||
|
||||
// 写入,需要判断同步还是异步
|
||||
func (l *Logger) write(event string, b []byte) (n int, err error) {
|
||||
if l.toAsync(event, b) {
|
||||
// fmt.Println("异步写入")
|
||||
return len(b), nil
|
||||
}
|
||||
return l.store(event, b)
|
||||
}
|
||||
|
||||
// 实际的存储
|
||||
// 必须满足 io.Writer 契约:成功时 n == len(b),否则调用方(log / io.MultiWriter)
|
||||
// 会认为发生了短写并把日志吞掉
|
||||
func (l *Logger) store(event string, b []byte) (n int, err error) {
|
||||
|
||||
if l.option.isPrintFile {
|
||||
f, err := l.getFile(event, false)
|
||||
// 串行化写入:句柄缓冲区不是并发安全的,
|
||||
// 异步消费协程与同步调用可能同时写同一个句柄
|
||||
l.writeMu.Lock()
|
||||
n, err = l.storeFile(event, b)
|
||||
l.writeMu.Unlock()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
// 驱动(控制台等)的写入失败不改变对调用方的契约
|
||||
_, _ = l.writeDrivers(b)
|
||||
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
// storeFile 写入日志文件
|
||||
func (l *Logger) storeFile(event string, b []byte) (int, error) {
|
||||
f, err := l.getFile(event)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
n, err = f.Write(b)
|
||||
n, err := f.Write(b)
|
||||
if err == nil && n < len(b) {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
if err != nil {
|
||||
// 强制更新 & 再次写入
|
||||
f, err := l.getFile(event, true)
|
||||
if err == nil {
|
||||
f.Write(b)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// 写入失败:丢弃这个句柄,落到磁盘后按最新文件名重开一次再写
|
||||
// 只重试一次,避免原实现在短写时反复重开文件
|
||||
l.discardFile(event, f)
|
||||
if nf, nerr := l.getFile(event); nerr == nil {
|
||||
if n2, err2 := nf.Write(b); err2 == nil && n2 == len(b) {
|
||||
return n2, nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(l.option.drivers) > 0 {
|
||||
io.MultiWriter(l.option.drivers...).Write(b)
|
||||
}
|
||||
return n, err
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var chanStore = make(chan cacheData, 1000)
|
||||
var chanOnce = sync.Once{}
|
||||
// 写入额外的驱动(控制台 / 自定义 writer)
|
||||
func (l *Logger) writeDrivers(b []byte) (int, error) {
|
||||
if len(l.option.drivers) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return io.MultiWriter(l.option.drivers...).Write(b)
|
||||
}
|
||||
|
||||
// discardFile 关闭文件并从缓存中移除,使下次写入重新打开
|
||||
func (l *Logger) discardFile(event string, f *logFile) {
|
||||
_ = f.Close()
|
||||
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
key := fileKey{channel: l.channel, event: event}
|
||||
if cur, ok := l.filePath[key]; ok && cur == f {
|
||||
delete(l.filePath, key)
|
||||
}
|
||||
}
|
||||
|
||||
// 异步队列的任务
|
||||
type cacheData struct {
|
||||
logger *Logger
|
||||
Event string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// toAsync 尝试异步写入
|
||||
// 返回 true 表示已经交给异步队列,返回 false 表示需要同步写入
|
||||
func (l *Logger) toAsync(event string, b []byte) bool {
|
||||
chanOnce.Do(func() {
|
||||
go func() {
|
||||
for val := range chanStore {
|
||||
val.logger.store(val.Event, val.Data)
|
||||
}
|
||||
}()
|
||||
})
|
||||
|
||||
if l.writeType == writeTypeSync || // 指定同步模式
|
||||
(l.writeType == writeTypeDefault && l.option.writeType != writeTypeAsync) { // 默认同步模式
|
||||
return false
|
||||
}
|
||||
|
||||
// 为了避免丢失,还是要阻塞等待
|
||||
chanStore <- cacheData{l, event, b}
|
||||
// 整段「检查开关 + 入队」都在同一把锁内:Close 也拿这把锁来停止投递,
|
||||
// 这样 Close 拿到锁时就能确定「要么这条还没入队、要么已经完整入队」。
|
||||
// 否则消费协程可能先看到空队列就退出,把还在路上的这条日志整条丢掉。
|
||||
// 队列满时这里会阻塞,但消费者是独立 goroutine 且不需要这把锁,不会死锁
|
||||
l.async.mu.Lock()
|
||||
defer l.async.mu.Unlock()
|
||||
|
||||
if l.async.closed {
|
||||
// 已开始关闭:退化为同步写入
|
||||
return false
|
||||
}
|
||||
if l.async.ch == nil {
|
||||
l.async.ch = make(chan cacheData, asyncQueueSize)
|
||||
go l.asyncWorker(l.async.ch)
|
||||
}
|
||||
l.async.ch <- cacheData{Event: event, Data: b}
|
||||
return true
|
||||
}
|
||||
|
||||
// asyncWorker 消费异步队列,直到队列被关闭
|
||||
func (l *Logger) asyncWorker(q chan cacheData) {
|
||||
defer close(l.workerDone)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Println("loggerx: 异步写入协程异常:", r)
|
||||
}
|
||||
}()
|
||||
for val := range q {
|
||||
_, _ = l.store(val.Event, val.Data)
|
||||
}
|
||||
}
|
||||
|
||||
// drainAsync 停止投递、关闭队列,并等消费协程把剩余任务全部写完
|
||||
func (l *Logger) drainAsync() {
|
||||
// 持锁关闭投递:此后 toAsync 一律退化为同步写入
|
||||
l.async.mu.Lock()
|
||||
l.async.closed = true
|
||||
q := l.async.ch
|
||||
l.async.mu.Unlock()
|
||||
|
||||
if q == nil {
|
||||
// 从未启用过异步写入,没有后台 goroutine 需要等
|
||||
return
|
||||
}
|
||||
|
||||
// 关闭队列并等消费者把缓冲里的任务全部处理完。
|
||||
// 这一步必须真的等到 workerDone:否则 Close 会在消费协程还在写缓冲时
|
||||
// 就刷盘并关闭文件,最后几条日志会连着句柄一起丢掉
|
||||
close(q)
|
||||
select {
|
||||
case <-l.workerDone:
|
||||
case <-time.After(closeDrainTimeout):
|
||||
log.Println("loggerx: 等待异步日志落盘超时")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user