refactor(logging): 迁移日志记录到 Zerolog 并优化错误处理

- 将日志记录库从 Logrus 替换为 Zerolog,提升性能和灵活性
- 更新各个模块的日志记录方式,确保一致性
- 优化错误处理逻辑,确保在发生错误时能够正确记录并传递错误信息
- 移除不再使用的错误处理字段,简化代码结构
- 更新相关文档以反映新的日志记录和错误处理机制
This commit is contained in:
2026-04-13 00:38:50 +08:00
parent 86daa3153e
commit 9a9b9c83ff
445 changed files with 59349 additions and 13297 deletions
+429
View File
@@ -0,0 +1,429 @@
package log
import (
"bufio"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"github.com/rs/zerolog"
)
// ErrorRecord 错误历史记录条目
type ErrorRecord struct {
Err error
Msg string
Time time.Time
Caller string
}
// Logger 日志核心结构体,封装 zerolog
type Logger struct {
zl zerolog.Logger
logLevel int
errors []ErrorRecord
errIdx int // 环形缓冲写入位置
errCount int // 实际写入总数(用于判断缓冲是否满)
errorsMu sync.RWMutex
maxErrors int
}
// NewLogger 创建日志实例
// logLevel: 0=仅 error>=1=全部
// logFile: 文件路径模板(如 "logs/20060102.txt"),空则不写文件
// maxErrors: 错误历史最大条数,0 则不记录
func NewLogger(logLevel int, logFile string, maxErrors int) *Logger {
zerolog.CallerMarshalFunc = callerMarshalFunc
zerolog.TimeFieldFormat = "2006-01-02 15:04:05"
var level zerolog.Level
if logLevel == 0 {
level = zerolog.ErrorLevel
} else {
level = zerolog.DebugLevel
}
consoleWriter := zerolog.ConsoleWriter{
Out: os.Stderr,
TimeFormat: "2006-01-02 15:04:05",
NoColor: false,
FormatLevel: formatLevelColor,
FormatCaller: func(i interface{}) string {
if i == nil {
return ""
}
return fmt.Sprintf("[%s]", i)
},
}
var writers []io.Writer
writers = append(writers, consoleWriter)
if logFile != "" {
fw := &TemplateFileWriter{pathTemplate: logFile}
writers = append(writers, fw)
}
multi := zerolog.MultiLevelWriter(writers...)
zl := zerolog.New(multi).
Level(level).
With().
Timestamp().
Caller().
Logger()
l := &Logger{
zl: zl,
logLevel: logLevel,
maxErrors: maxErrors,
}
if maxErrors > 0 {
l.errors = make([]ErrorRecord, maxErrors)
}
return l
}
// NewTestLogger 创建用于测试的静默 Logger(输出到 io.Discard
func NewTestLogger() *Logger {
zl := zerolog.New(io.Discard).Level(zerolog.Disabled)
return &Logger{zl: zl, logLevel: 0}
}
// SetOutput 设置日志输出(用于测试等场景)
func (l *Logger) SetOutput(w io.Writer) {
if w == io.Discard {
l.zl = zerolog.New(w).Level(zerolog.Disabled)
return
}
consoleWriter := zerolog.ConsoleWriter{
Out: w,
TimeFormat: "2006-01-02 15:04:05",
NoColor: true,
FormatLevel: formatLevelPlain,
FormatCaller: func(i interface{}) string {
if i == nil {
return ""
}
return fmt.Sprintf("[%s]", i)
},
}
l.zl = zerolog.New(consoleWriter).
With().
Timestamp().
Caller().
Logger()
}
// GetLevel 获取当前日志等级
func (l *Logger) GetLevel() int {
return l.logLevel
}
// --- 链式调用 API ---
// 以下方法兼容两种调用风格:
// 无参数:返回 *zerolog.Event 用于链式调用,如 l.Error().Str("k","v").Msg("...")
// 有参数:直接拼接并打印日志(logrus 兼容),返回 no-op Event
func (l *Logger) Debug(args ...interface{}) *zerolog.Event {
if len(args) > 0 {
l.zl.Debug().Msg(fmt.Sprint(args...))
nop := zerolog.Nop()
return nop.Debug()
}
return l.zl.Debug()
}
func (l *Logger) Info(args ...interface{}) *zerolog.Event {
if len(args) > 0 {
l.zl.Info().Msg(fmt.Sprint(args...))
nop := zerolog.Nop()
return nop.Info()
}
return l.zl.Info()
}
func (l *Logger) Warn(args ...interface{}) *zerolog.Event {
if len(args) > 0 {
l.zl.Warn().Msg(fmt.Sprint(args...))
nop := zerolog.Nop()
return nop.Warn()
}
return l.zl.Warn()
}
func (l *Logger) Error(args ...interface{}) *zerolog.Event {
if len(args) > 0 {
msg := fmt.Sprint(args...)
if l.maxErrors > 0 {
l.recordError(msg, nil)
}
l.zl.Error().Msg(msg)
nop := zerolog.Nop()
return nop.Error()
}
if l.maxErrors > 0 {
l.recordError("", nil)
}
return l.zl.Error()
}
// RecordError 手动记录一条错误到历史(用于需要指定详情的场景)
func (l *Logger) RecordError(err error, msg string, caller string) {
if l.maxErrors <= 0 {
return
}
l.errorsMu.Lock()
defer l.errorsMu.Unlock()
l.errors[l.errIdx] = ErrorRecord{
Err: err,
Msg: msg,
Time: time.Now(),
Caller: caller,
}
l.errIdx = (l.errIdx + 1) % l.maxErrors
l.errCount++
}
func (l *Logger) recordError(msg string, err error) {
_, file, line, ok := runtime.Caller(2)
caller := ""
if ok {
caller = formatCaller(file, line)
}
l.RecordError(err, msg, caller)
}
// GetRecentErrors 获取最近 N 条错误(不传则返回全部已存储的)
func (l *Logger) GetRecentErrors(n ...int) []ErrorRecord {
if l.maxErrors <= 0 {
return nil
}
l.errorsMu.RLock()
defer l.errorsMu.RUnlock()
total := l.errCount
if total > l.maxErrors {
total = l.maxErrors
}
if total == 0 {
return nil
}
want := total
if len(n) > 0 && n[0] > 0 && n[0] < want {
want = n[0]
}
result := make([]ErrorRecord, 0, want)
// 从最新的往前读
for i := 0; i < want; i++ {
idx := (l.errIdx - 1 - i + l.maxErrors) % l.maxErrors
if l.errors[idx].Time.IsZero() {
break
}
result = append(result, l.errors[idx])
}
return result
}
// --- 格式化快捷方法 ---
func (l *Logger) Debugf(format string, v ...interface{}) {
l.zl.Debug().Msgf(format, v...)
}
func (l *Logger) Infof(format string, v ...interface{}) {
l.zl.Info().Msgf(format, v...)
}
func (l *Logger) Warnf(format string, v ...interface{}) {
l.zl.Warn().Msgf(format, v...)
}
func (l *Logger) Errorf(format string, v ...interface{}) {
if l.maxErrors > 0 {
l.recordError(fmt.Sprintf(format, v...), nil)
}
l.zl.Error().Msgf(format, v...)
}
// --- 日志级别颜色 ---
func formatLevelColor(i interface{}) string {
level := strings.ToUpper(fmt.Sprintf("%s", i))
switch level {
case "DEBUG":
return fmt.Sprintf("\x1b[36m|%s|\x1b[0m", level) // cyan
case "INFO":
return fmt.Sprintf("\x1b[32m|%s|\x1b[0m", level) // green
case "WARN":
return fmt.Sprintf("\x1b[33m|%s|\x1b[0m", level) // yellow
case "ERROR":
return fmt.Sprintf("\x1b[31m|%s|\x1b[0m", level) // red
case "FATAL":
return fmt.Sprintf("\x1b[35m|%s|\x1b[0m", level) // magenta
default:
return fmt.Sprintf("|%s|", level)
}
}
func formatLevelPlain(i interface{}) string {
return fmt.Sprintf("|%s|", strings.ToUpper(fmt.Sprintf("%s", i)))
}
// --- TemplateFileWriter ---
// TemplateFileWriter 按时间模板切换文件路径的 Writer
type TemplateFileWriter struct {
pathTemplate string
mu sync.Mutex
currentPath string
file *os.File
writer *bufio.Writer
}
func (w *TemplateFileWriter) Write(p []byte) (n int, err error) {
w.mu.Lock()
defer w.mu.Unlock()
path := time.Now().Format(w.pathTemplate)
if path != w.currentPath {
if w.writer != nil {
_ = w.writer.Flush()
}
if w.file != nil {
_ = w.file.Close()
}
_ = os.MkdirAll(filepath.Dir(path), 0755)
w.file, err = os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return 0, err
}
w.writer = bufio.NewWriterSize(w.file, 4096)
w.currentPath = path
}
n, err = w.writer.Write(p)
_ = w.writer.Flush()
return
}
// --- 调用者智能过滤 ---
const maxFrameworkDepth = 10
func callerMarshalFunc(_ uintptr, file string, line int) string {
return findCaller(file, line)
}
func findCaller(origFile string, origLine int) string {
frameworkCount := 0
var lastFrameworkFile string
var lastFrameworkLine int
var applicationFile string
var applicationLine int
for i := 1; i < 20; i++ {
_, file, line, ok := runtime.Caller(i)
if !ok {
break
}
shortFile := shortenPath(file)
if isHoTimeFrameworkFile(shortFile) {
frameworkCount++
lastFrameworkFile = shortFile
lastFrameworkLine = line
if strings.Contains(shortFile, "application.go") {
applicationFile = shortFile
applicationLine = line
}
if frameworkCount >= maxFrameworkDepth {
return fmt.Sprintf("%s:%d", shortFile, line)
}
continue
}
return fmt.Sprintf("%s:%d", shortFile, line)
}
if applicationFile != "" {
return fmt.Sprintf("%s:%d", applicationFile, applicationLine)
}
if lastFrameworkFile != "" {
return fmt.Sprintf("%s:%d", lastFrameworkFile, lastFrameworkLine)
}
return fmt.Sprintf("%s:%d", shortenPath(origFile), origLine)
}
func formatCaller(file string, line int) string {
return fmt.Sprintf("%s:%d", shortenPath(file), line)
}
func shortenPath(file string) string {
n := 0
for i := len(file) - 1; i > 0; i-- {
if file[i] == '/' || file[i] == '\\' {
n++
if n >= 2 {
return file[i+1:]
}
}
}
return file
}
func isHoTimeFrameworkFile(file string) bool {
// zerolog 内部文件
if strings.HasPrefix(file, "zerolog/") || strings.HasPrefix(file, "zerolog@") {
return true
}
// logrus 遗留(vendor 中可能存在)
if strings.HasPrefix(file, "logrus/") || strings.HasPrefix(file, "logrus@") {
return true
}
if strings.HasPrefix(file, "runtime/") {
return true
}
lowerFile := strings.ToLower(file)
if strings.Contains(lowerFile, "hotime") {
frameworkDirs := []string{"db/", "common/", "code/", "cache/", "log/", "dri/"}
for _, dir := range frameworkDirs {
if strings.Contains(file, dir) {
return true
}
}
if strings.HasSuffix(file, "application.go") ||
strings.HasSuffix(file, "context.go") ||
strings.HasSuffix(file, "session.go") ||
strings.HasSuffix(file, "const.go") ||
strings.HasSuffix(file, "type.go") ||
strings.HasSuffix(file, "var.go") ||
strings.HasSuffix(file, "mime.go") {
return true
}
}
frameworkCoreDirs := []string{"db/", "common/", "code/", "cache/"}
for _, dir := range frameworkCoreDirs {
if strings.HasPrefix(file, dir) {
frameworkFiles := []string{
"query.go", "crud.go", "where.go", "builder.go", "db.go",
"dialect.go", "aggregate.go", "transaction.go", "identifier.go",
"error.go", "func.go", "map.go", "obj.go", "slice.go",
"makecode.go", "template.go", "config.go",
"cache.go", "cache_db.go", "cache_memory.go", "cache_redis.go",
}
for _, f := range frameworkFiles {
if strings.HasSuffix(file, f) {
return true
}
}
}
}
return false
}