package loggerx import ( "errors" "fmt" "io" "os" "path/filepath" "time" ) // 文件操作 // 每个文件的写缓冲大小:越大越省系统调用,代价是崩溃时可能丢最后一批日志 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 { prefix := "" switch l.option.fileSplit { case FileSplitTimeA: // (年/月/日/时) prefix = time.Now().In(l.option.timeZone).Format("2006/01/02/15") case FileSplitTimeB: // (年/月/日) prefix = time.Now().In(l.option.timeZone).Format("2006/01/02") case FileSplitTimeC: // (年/月-日) prefix = time.Now().In(l.option.timeZone).Format("2006/01-02") case FileSplitTimeD: // (年-月-日-时) prefix = time.Now().In(l.option.timeZone).Format("2006-01-02-15") case FileSplitTimeE: // (年-月-日) prefix = time.Now().In(l.option.timeZone).Format("2006-01-02") } if prefix != "" { prefix = prefix + "_" } if l.channel != "" { prefix = l.channel + "/" + prefix } return l.option.dir + "/" + prefix + event + ".log" } // 新建文件 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) // 识别目录与文件,创建多层目录(已存在不报错) if dir := filepath.Dir(fileName); dir != "" { if err := os.MkdirAll(dir, 0755); err != nil { return nil, err } } 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 lf, nil } // 加载文件(读锁) func (l *Logger) loadFile(key fileKey) *logFile { l.mu.RLock() defer l.mu.RUnlock() return l.loadFileLocked(key) } // 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) }