Files
hotime/log/logger.go
T
hoteas 831a7f017a feat(log): Seq 会话追踪、控制台降噪与推送质量增强
请求日志自动带 sid/request_id,支持 LogBind;挂 Seq 后控制台仅 Warn+,Seq 仍全量;并修毫秒时间戳、@m、失败重试与停机 flush。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 05:26:44 +08:00

887 lines
21 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package log
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/rs/zerolog"
)
const maxCaptureLine = 10 << 20 // 10MB,支持 GetReqMap 等大 body 单行日志
var (
origStderr io.Writer
origStderrOnce sync.Once
)
func saveOrigStderr() {
origStderrOnce.Do(func() {
origStderr = os.Stderr
})
}
func seqWriteStderr(format string, args ...interface{}) {
if origStderr != nil {
fmt.Fprintf(origStderr, format, args...)
} else {
fmt.Fprintf(os.Stderr, format, args...)
}
}
// ErrorRecord 错误历史记录条目
type ErrorRecord struct {
Err error
Msg string
Time time.Time
Caller string
}
// errorStore 错误历史环形缓冲(指针共享,供 WithFields 派生子 Logger 共用)
type errorStore struct {
mu sync.RWMutex
errors []ErrorRecord
errIdx int
errCount int
maxErrors int
}
// Logger 日志核心结构体,封装 zerolog
type Logger struct {
zl zerolog.Logger
mw *multiWriter // 动态多输出,支持运行时追加 SeqWriter 等
console *levelFilterWriter // 控制台出口,挂 Seq 后可降到 Warn+
logLevel int
store *errorStore
seqs []*SeqWriter // 本 Logger 挂载过的 SeqWriter,供 CloseSeq 冲刷
seqsMu sync.Mutex
}
// levelFilterWriter 按级别过滤的 Writer(实现 zerolog.LevelWriter
type levelFilterWriter struct {
inner io.Writer
minLevel atomic.Int32 // zerolog.Level 存为 int32
}
func newLevelFilterWriter(inner io.Writer, min zerolog.Level) *levelFilterWriter {
w := &levelFilterWriter{inner: inner}
w.minLevel.Store(int32(min))
return w
}
func (w *levelFilterWriter) setMinLevel(min zerolog.Level) {
if w == nil {
return
}
w.minLevel.Store(int32(min))
}
func (w *levelFilterWriter) Write(p []byte) (int, error) {
// 无级别信息时按 Info 处理:低于门槛则丢弃
if zerolog.Level(w.minLevel.Load()) > zerolog.InfoLevel {
return len(p), nil
}
return w.inner.Write(p)
}
func (w *levelFilterWriter) WriteLevel(l zerolog.Level, p []byte) (int, error) {
if l < zerolog.Level(w.minLevel.Load()) {
return len(p), nil
}
if lw, ok := w.inner.(zerolog.LevelWriter); ok {
return lw.WriteLevel(l, p)
}
return w.inner.Write(p)
}
const timeFormatMS = "2006-01-02 15:04:05.000"
// NewLogger 创建日志实例
// logLevel: 0=仅 error>=1=全部
// logFile: 文件路径模板(如 "logs/20060102.txt"),空则不写文件
// maxErrors: 错误历史最大条数,0 则不记录
func NewLogger(logLevel int, logFile string, maxErrors int) *Logger {
saveOrigStderr()
zerolog.CallerMarshalFunc = callerMarshalFunc
zerolog.TimeFieldFormat = timeFormatMS
var level zerolog.Level
if logLevel == 0 {
level = zerolog.ErrorLevel
} else {
level = zerolog.DebugLevel
}
consoleWriter := zerolog.ConsoleWriter{
Out: os.Stderr,
TimeFormat: timeFormatMS,
NoColor: false,
FormatLevel: formatLevelColor,
FormatCaller: func(i interface{}) string {
if i == nil {
return ""
}
return fmt.Sprintf("[%s]", i)
},
}
console := newLevelFilterWriter(consoleWriter, zerolog.DebugLevel)
var writers []io.Writer
writers = append(writers, console)
if logFile != "" {
fw := &TemplateFileWriter{pathTemplate: logFile}
writers = append(writers, fw)
}
mw := &multiWriter{writers: writers}
zl := zerolog.New(mw).
Level(level).
With().
Timestamp().
Caller().
Logger()
l := &Logger{
zl: zl,
mw: mw,
console: console,
logLevel: logLevel,
store: &errorStore{maxErrors: maxErrors},
}
if maxErrors > 0 {
l.store.errors = make([]ErrorRecord, maxErrors)
}
return l
}
// NewLoggerNoCaller 创建不带自动 Caller 字段的日志实例(用于访问日志等 caller 无意义的场景)
func NewLoggerNoCaller(logLevel int, logFile string, maxErrors int) *Logger {
saveOrigStderr()
zerolog.CallerMarshalFunc = callerMarshalFunc
zerolog.TimeFieldFormat = timeFormatMS
var level zerolog.Level
if logLevel == 0 {
level = zerolog.ErrorLevel
} else {
level = zerolog.DebugLevel
}
consoleWriter := zerolog.ConsoleWriter{
Out: os.Stderr,
TimeFormat: timeFormatMS,
NoColor: false,
FormatLevel: formatLevelColor,
}
console := newLevelFilterWriter(consoleWriter, zerolog.DebugLevel)
var writers []io.Writer
writers = append(writers, console)
if logFile != "" {
fw := &TemplateFileWriter{pathTemplate: logFile}
writers = append(writers, fw)
}
mw := &multiWriter{writers: writers}
zl := zerolog.New(mw).
Level(level).
With().
Timestamp().
Logger()
l := &Logger{
zl: zl,
mw: mw,
console: console,
logLevel: logLevel,
store: &errorStore{maxErrors: maxErrors},
}
if maxErrors > 0 {
l.store.errors = make([]ErrorRecord, maxErrors)
}
return l
}
// WithFields 派生子 Logger,附加成对 key,value 字段;mw/错误历史与父共享,父不受污染。
func (l *Logger) WithFields(kvs ...string) *Logger {
if l == nil {
return nil
}
ctx := l.zl.With()
for i := 0; i+1 < len(kvs); i += 2 {
if kvs[i] == "" {
continue
}
ctx = ctx.Str(kvs[i], kvs[i+1])
}
return &Logger{
zl: ctx.Logger(),
mw: l.mw,
console: l.console,
logLevel: l.logLevel,
store: l.store,
}
}
// 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)
l.mw = nil
return
}
consoleWriter := zerolog.ConsoleWriter{
Out: w,
TimeFormat: timeFormatMS,
NoColor: true,
FormatLevel: formatLevelPlain,
FormatCaller: func(i interface{}) string {
if i == nil {
return ""
}
return fmt.Sprintf("[%s]", i)
},
}
mw := &multiWriter{writers: []io.Writer{consoleWriter}}
l.mw = mw
l.zl = zerolog.New(mw).
With().
Timestamp().
Caller().
Logger()
}
// GetLevel 获取当前日志等级
func (l *Logger) GetLevel() int {
return l.logLevel
}
// EmitCaptured 将捕获的 stdout/stderr 等输出直写 multiWriter,绕过主 Logger 的 level 过滤。
// 保证 logLevel=0 时 fmt.Println / log.Println 仍能进 Seq。
func (l *Logger) EmitCaptured(level zerolog.Level, source, msg string) {
if l == nil || l.mw == nil || msg == "" {
return
}
zl := zerolog.New(l.mw)
zl.WithLevel(level).Timestamp().Str("source", source).Msg(msg)
}
// EmitRecoveredPanic 记录 recover 到的 panic:错误原因 + 调用栈,source=panic。
func (l *Logger) EmitRecoveredPanic(err interface{}) {
if l == nil || l.mw == nil || err == nil {
return
}
zl := zerolog.New(l.mw)
zl.WithLevel(zerolog.ErrorLevel).Timestamp().
Str("source", "panic").
Str("stack", string(debugStack())).
Msg(fmt.Sprint(err))
}
func debugStack() []byte {
buf := make([]byte, 64*1024)
n := runtime.Stack(buf, false)
return buf[:n]
}
// CaptureStream 从 reader 持续读取行并写入 Logger(用于 stdout/stderr 管道)。
// 单行超过 maxCaptureLine 时截断并标注,读取出错后重启读取,避免永久失效。
func CaptureStream(l *Logger, level zerolog.Level, source string, r io.Reader) {
rd := bufio.NewReader(r)
for {
line, err := rd.ReadString('\n')
if len(line) > 0 {
msg := strings.TrimRight(line, "\r\n")
if msg != "" {
if len(msg) > maxCaptureLine {
msg = msg[:maxCaptureLine] + "...(truncated)"
}
l.EmitCaptured(level, source, msg)
}
}
if err != nil {
if err == io.EOF {
if len(line) == 0 {
return
}
continue
}
l.EmitCaptured(zerolog.ErrorLevel, source+"-capture", err.Error())
return
}
}
}
// --- 链式调用 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.store != nil && l.store.maxErrors > 0 {
l.recordError(msg, nil)
}
l.zl.Error().Msg(msg)
nop := zerolog.Nop()
return nop.Error()
}
if l.store != nil && l.store.maxErrors > 0 {
l.recordError("", nil)
}
return l.zl.Error()
}
// RecordError 手动记录一条错误到历史(用于需要指定详情的场景)
func (l *Logger) RecordError(err error, msg string, caller string) {
if l == nil || l.store == nil || l.store.maxErrors <= 0 {
return
}
s := l.store
s.mu.Lock()
defer s.mu.Unlock()
s.errors[s.errIdx] = ErrorRecord{
Err: err,
Msg: msg,
Time: time.Now(),
Caller: caller,
}
s.errIdx = (s.errIdx + 1) % s.maxErrors
s.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 == nil || l.store == nil || l.store.maxErrors <= 0 {
return nil
}
s := l.store
s.mu.RLock()
defer s.mu.RUnlock()
total := s.errCount
if total > s.maxErrors {
total = s.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 := (s.errIdx - 1 - i + s.maxErrors) % s.maxErrors
if s.errors[idx].Time.IsZero() {
break
}
result = append(result, s.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.store != nil && l.store.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
}
// --- multiWriter: 支持运行时追加输出目标 ---
// multiWriter 实现 zerolog.LevelWriter,允许在 Logger 创建后动态追加新的输出(如 SeqWriter)
type multiWriter struct {
mu sync.RWMutex
writers []io.Writer
}
func (mw *multiWriter) Write(p []byte) (int, error) {
mw.mu.RLock()
defer mw.mu.RUnlock()
for _, w := range mw.writers {
_, _ = w.Write(p)
}
return len(p), nil
}
// WriteLevel 实现 zerolog.LevelWriter,使 zerolog 的级别过滤正确传递给各子 Writer
func (mw *multiWriter) WriteLevel(l zerolog.Level, p []byte) (int, error) {
mw.mu.RLock()
defer mw.mu.RUnlock()
for _, w := range mw.writers {
if lw, ok := w.(zerolog.LevelWriter); ok {
_, _ = lw.WriteLevel(l, p)
} else {
_, _ = w.Write(p)
}
}
return len(p), nil
}
func (mw *multiWriter) add(w io.Writer) {
mw.mu.Lock()
mw.writers = append(mw.writers, w)
mw.mu.Unlock()
}
// --- SeqWriter: 异步推送到 SeqCLEF 格式)---
// SeqWriter 将 zerolog JSON 日志通过 channel 队列异步发送到 Seq。
// Write() 仅做 channel <- bytesO(1) 非阻塞,绝不阻塞 web 请求处理 goroutine。
// channel 容量 10000,满时丢弃新条目(dropped 计数),不会影响主服务。
type SeqWriter struct {
seqUrl string
apiKey string
instance string
queue chan []byte
dropped int64
client *http.Client
closed int32
done chan struct{}
}
func newSeqWriter(seqUrl, apiKey, instance string) *SeqWriter {
w := &SeqWriter{
seqUrl: strings.TrimRight(seqUrl, "/"),
apiKey: apiKey,
instance: instance,
queue: make(chan []byte, 10000),
client: &http.Client{Timeout: 5 * time.Second},
done: make(chan struct{}),
}
go w.run()
return w
}
// Write 将字节塞入 channel 即返回,由后台 goroutine 消费发送
func (w *SeqWriter) Write(p []byte) (int, error) {
if atomic.LoadInt32(&w.closed) != 0 {
return len(p), nil
}
clef := w.toClef(p)
select {
case w.queue <- clef:
default:
n := atomic.AddInt64(&w.dropped, 1)
if n == 1 || n%100 == 0 {
seqWriteStderr("[seq] queue full, dropped=%d\n", n)
}
}
return len(p), nil
}
// Close 停止接收新条目,并在约 2s 内冲刷残留批次(幂等)
func (w *SeqWriter) Close() {
if w == nil {
return
}
if !atomic.CompareAndSwapInt32(&w.closed, 0, 1) {
return
}
close(w.queue)
select {
case <-w.done:
case <-time.After(2 * time.Second):
seqWriteStderr("[seq] Close timeout, some events may be dropped\n")
}
}
// toClef 将 zerolog JSON 字段映射到 Seq CLEF 格式
// zerolog: time/level/message(msg) → CLEF: @t/@l/@m
func (w *SeqWriter) toClef(p []byte) []byte {
trimmed := bytes.TrimSpace(p)
var m map[string]interface{}
if err := json.Unmarshal(trimmed, &m); err != nil {
// 非 JSON(如 fmt.Println 直接输出),包装为纯文本日志
return []byte(fmt.Sprintf(`{"@t":%q,"@l":"Information","@m":%q,"source":"stdout","instance":%q}`,
time.Now().Format("2006-01-02T15:04:05.000Z07:00"), strings.TrimSpace(string(p)), w.instance))
}
clef := make(map[string]interface{}, len(m)+3)
for k, v := range m {
clef[k] = v
}
// time → @t(优先毫秒格式,回退秒级)
if t, ok := m["time"].(string); ok {
delete(clef, "time")
if pt, err := time.ParseInLocation(timeFormatMS, t, time.Local); err == nil {
clef["@t"] = pt.Format("2006-01-02T15:04:05.000Z07:00")
} else if pt, err := time.ParseInLocation("2006-01-02 15:04:05", t, time.Local); err == nil {
clef["@t"] = pt.Format("2006-01-02T15:04:05.000Z07:00")
} else {
clef["@t"] = t
}
} else {
clef["@t"] = time.Now().Format("2006-01-02T15:04:05.000Z07:00")
}
// level → @l
if lv, ok := m["level"].(string); ok {
delete(clef, "level")
switch lv {
case "debug":
clef["@l"] = "Debug"
case "info":
clef["@l"] = "Information"
case "warn":
clef["@l"] = "Warning"
case "error":
clef["@l"] = "Error"
case "fatal":
clef["@l"] = "Fatal"
default:
clef["@l"] = lv
}
}
// message/msg → @m(不用 @mt,避免 Seq 把 {xxx} 当消息模板)
if msg, ok := m["message"].(string); ok {
delete(clef, "message")
clef["@m"] = msg
} else if msg, ok := m["msg"].(string); ok {
delete(clef, "msg")
clef["@m"] = msg
}
if w.instance != "" {
clef["instance"] = w.instance
}
b, _ := json.Marshal(clef)
return b
}
// run 后台 goroutine:每 100 条或 500ms 批量 POST 到 Seqqueue 关闭后冲刷并退出
func (w *SeqWriter) run() {
defer close(w.done)
batch := make([][]byte, 0, 100)
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
for {
select {
case entry, ok := <-w.queue:
if !ok {
if len(batch) > 0 {
w.flush(batch)
}
return
}
batch = append(batch, entry)
if len(batch) >= 100 {
w.flush(batch)
batch = batch[:0]
}
case <-ticker.C:
if len(batch) > 0 {
w.flush(batch)
batch = batch[:0]
}
}
}
}
func (w *SeqWriter) flush(batch [][]byte) {
if err := w.postBatch(batch); err != nil {
time.Sleep(1 * time.Second)
if err2 := w.postBatch(batch); err2 != nil {
seqWriteStderr("[seq] send failed after retry: %v (dropped=%d)\n", err2, atomic.LoadInt64(&w.dropped))
}
}
}
func (w *SeqWriter) postBatch(batch [][]byte) error {
var buf bytes.Buffer
for _, entry := range batch {
buf.Write(entry)
buf.WriteByte('\n')
}
req, err := http.NewRequest("POST", w.seqUrl+"/api/events/raw?clef", &buf)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/vnd.serilog.clef")
if w.apiKey != "" {
req.Header.Set("X-Seq-ApiKey", w.apiKey)
}
resp, err := w.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("status %d: %s", resp.StatusCode, string(body))
}
return nil
}
// SetSeqWriter 将 Seq HTTP 推送器附加到当前 Logger(不影响已有的控制台/文件输出)
// seqUrl: Seq 服务地址,如 "http://127.0.0.1:5341"
// apiKey: Seq API Key,免费单用户版留空
// instance: 实例标识,建议用 config 的 port 字段区分同机多进程,如 "8085"
func (l *Logger) SetSeqWriter(seqUrl, apiKey, instance string) {
if l == nil || seqUrl == "" || l.mw == nil {
return
}
sw := newSeqWriter(seqUrl, apiKey, instance)
l.mw.add(sw)
l.seqsMu.Lock()
l.seqs = append(l.seqs, sw)
l.seqsMu.Unlock()
// 挂上 Seq 后控制台只打 Warn+Info/Debug/SQL/访问日志仍进 Seq 与文件
l.console.setMinLevel(zerolog.WarnLevel)
}
// CloseSeq 冲刷并关闭本 Logger 挂载的全部 SeqWriter(幂等、nil 安全)
func (l *Logger) CloseSeq() {
if l == nil {
return
}
l.seqsMu.Lock()
seqs := l.seqs
l.seqs = nil
l.seqsMu.Unlock()
for _, sw := range seqs {
sw.Close()
}
}
// 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
}