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 } // NewLoggerNoCaller 创建不带自动 Caller 字段的日志实例(用于访问日志等 caller 无意义的场景) func NewLoggerNoCaller(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, } 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(). 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 } // --- 调用者智能过滤 --- func callerMarshalFunc(_ uintptr, file string, line int) string { return findCaller(file, line) } func findCaller(origFile string, origLine int) string { var lastInfraFile string var lastInfraLine int var primaryCaller string var appLayerCaller string for i := 1; i < 20; i++ { _, file, line, ok := runtime.Caller(i) if !ok { break } shortFile := shortenPath(file) if isInfrastructureFile(shortFile) { lastInfraFile = shortFile lastInfraLine = line continue } if primaryCaller == "" { primaryCaller = fmt.Sprintf("%s:%d", shortFile, line) } if appLayerCaller == "" && !strings.Contains(strings.ToLower(file), "hotime") { appLayerCaller = fmt.Sprintf("%s:%d", shortFile, line) } } result := "" if primaryCaller != "" { result = primaryCaller } else if lastInfraFile != "" { result = fmt.Sprintf("%s:%d", lastInfraFile, lastInfraLine) } else { result = fmt.Sprintf("%s:%d", shortenPath(origFile), origLine) } if appLayerCaller != "" && result != appLayerCaller { result += " <- " + appLayerCaller } return result } 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 } // isInfrastructureFile 判断文件是否为基础设施(日志/DB/缓存/通用工具)管线, // 这些文件在调用栈中会被跳过,以显示真正的业务调用者。 // context.go 是 Display/View 的纯中间层,也作为基础设施跳过。 // application.go、code/makecode.go 等保留,会作为有意义的调用者返回。 func isInfrastructureFile(file string) bool { if strings.HasPrefix(file, "zerolog/") || strings.HasPrefix(file, "zerolog@") { return true } if strings.HasPrefix(file, "logrus/") || strings.HasPrefix(file, "logrus@") { return true } if strings.HasPrefix(file, "runtime/") { return true } if strings.HasPrefix(file, "log/") { return true } infraPrefixes := []string{"db/", "cache/", "common/", "dri/"} for _, prefix := range infraPrefixes { if strings.HasPrefix(file, prefix) { return true } } lowerFile := strings.ToLower(file) if strings.Contains(lowerFile, "hotime") { infraDirs := []string{"db/", "cache/", "common/", "log/", "dri/"} for _, dir := range infraDirs { if strings.Contains(file, dir) { return true } } if strings.HasSuffix(file, "/context.go") || strings.HasSuffix(file, "\\context.go") { return true } } return false }