From 78d135c6bf286d10bd86b8bcebe870bb8afb2349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?piexlMax=28=E5=A5=87=E6=B7=BC?= Date: Tue, 11 Nov 2025 18:11:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=97=A5=E5=BF=97):=20=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=8A=9F=E8=83=BD=E4=BB=A5=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E6=96=B9=E6=BA=90=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/core/zap.go | 27 ++++++--- server/utils/ast/extract_func.go | 62 +++++++++++++++++++++ server/utils/stacktrace/stacktrace.go | 79 +++++++++++++++++++++++++++ 3 files changed, 160 insertions(+), 8 deletions(-) create mode 100644 server/utils/ast/extract_func.go create mode 100644 server/utils/stacktrace/stacktrace.go diff --git a/server/core/zap.go b/server/core/zap.go index aa1c2e01..3cf7e097 100644 --- a/server/core/zap.go +++ b/server/core/zap.go @@ -8,6 +8,8 @@ import ( "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/service" "github.com/flipped-aurora/gin-vue-admin/server/utils" + astutil "github.com/flipped-aurora/gin-vue-admin/server/utils/ast" + "github.com/flipped-aurora/gin-vue-admin/server/utils/stacktrace" "go.uber.org/zap" "go.uber.org/zap/zapcore" "os" @@ -53,6 +55,15 @@ func Zap() (logger *zap.Logger) { stack := entry.Stack if stack != "" { info = fmt.Sprintf("%s | stack=%s", info, stack) + // 解析最终业务调用方,并提取其方法源码 + if frame, ok := stacktrace.FindFinalCaller(stack); ok { + fnName, fnSrc, sLine, eLine, exErr := astutil.ExtractFuncSourceByPosition(frame.File, frame.Line) + if exErr == nil { + info = fmt.Sprintf("%s | final_caller=%s:%d (%s lines %d-%d)\n----- 产生日志的方法代码如下 -----\n%s", info, frame.File, frame.Line, fnName, sLine, eLine, fnSrc) + } else { + info = fmt.Sprintf("%s | final_caller=%s:%d (%s) | extract_err=%v", info, frame.File, frame.Line, fnName, exErr) + } + } } // 使用后台上下文,避免依赖 gin.Context @@ -65,12 +76,12 @@ func Zap() (logger *zap.Logger) { return nil }) - logger = zap.New(zapcore.NewTee(cores...), dbHook) - // 启用 Error 及以上级别的堆栈捕捉,确保 entry.Stack 可用 - opts := []zap.Option{zap.AddStacktrace(zapcore.ErrorLevel)} - if global.GVA_CONFIG.Zap.ShowLine { - opts = append(opts, zap.AddCaller()) - } - logger = logger.WithOptions(opts...) - return logger + logger = zap.New(zapcore.NewTee(cores...), dbHook) + // 启用 Error 及以上级别的堆栈捕捉,确保 entry.Stack 可用 + opts := []zap.Option{zap.AddStacktrace(zapcore.ErrorLevel)} + if global.GVA_CONFIG.Zap.ShowLine { + opts = append(opts, zap.AddCaller()) + } + logger = logger.WithOptions(opts...) + return logger } diff --git a/server/utils/ast/extract_func.go b/server/utils/ast/extract_func.go new file mode 100644 index 00000000..4b880c1f --- /dev/null +++ b/server/utils/ast/extract_func.go @@ -0,0 +1,62 @@ +package ast + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "os" +) + +// ExtractFuncSourceByPosition 根据文件路径与行号,提取包含该行的整个方法源码 +// 返回:方法名、完整源码、起止行号 +func ExtractFuncSourceByPosition(filePath string, line int) (name string, source string, startLine int, endLine int, err error) { + // 读取源文件 + src, readErr := os.ReadFile(filePath) + if readErr != nil { + err = fmt.Errorf("read file failed: %w", readErr) + return + } + + // 解析 AST + fset := token.NewFileSet() + file, parseErr := parser.ParseFile(fset, filePath, src, parser.ParseComments) + if parseErr != nil { + err = fmt.Errorf("parse file failed: %w", parseErr) + return + } + + // 在 AST 中定位包含指定行号的函数声明 + var target *ast.FuncDecl + ast.Inspect(file, func(n ast.Node) bool { + fd, ok := n.(*ast.FuncDecl) + if !ok { + return true + } + s := fset.Position(fd.Pos()).Line + e := fset.Position(fd.End()).Line + if line >= s && line <= e { + target = fd + startLine = s + endLine = e + return false + } + return true + }) + + if target == nil { + err = fmt.Errorf("no function encloses line %d in %s", line, filePath) + return + } + + // 使用字节偏移精确提取源码片段(包含注释与原始格式) + start := fset.Position(target.Pos()).Offset + end := fset.Position(target.End()).Offset + if start < 0 || end > len(src) || start >= end { + err = fmt.Errorf("invalid offsets for function: start=%d end=%d len=%d", start, end, len(src)) + return + } + source = string(src[start:end]) + name = target.Name.Name + return +} \ No newline at end of file diff --git a/server/utils/stacktrace/stacktrace.go b/server/utils/stacktrace/stacktrace.go new file mode 100644 index 00000000..dbae2c96 --- /dev/null +++ b/server/utils/stacktrace/stacktrace.go @@ -0,0 +1,79 @@ +package stacktrace + +import ( + "regexp" + "strconv" + "strings" +) + +// Frame 表示一次栈帧解析结果 +type Frame struct { + File string + Line int + Func string +} + +var fileLineRe = regexp.MustCompile(`\s*(.+\.go):(\d+)\s*$`) + +// FindFinalCaller 从 zap 的 entry.Stack 文本中,解析“最终业务调用方”的文件与行号 +// 策略:自顶向下解析,优先选择第一条项目代码帧,过滤第三方库/标准库/框架中间件 +func FindFinalCaller(stack string) (Frame, bool) { + if stack == "" { + return Frame{}, false + } + lines := strings.Split(stack, "\n") + var currFunc string + for i := 0; i < len(lines); i++ { + line := strings.TrimSpace(lines[i]) + if line == "" { + continue + } + if m := fileLineRe.FindStringSubmatch(line); m != nil { + file := m[1] + ln, _ := strconv.Atoi(m[2]) + if shouldSkip(file) { + // 跳过此帧,同时重置函数名以避免错误配对 + currFunc = "" + continue + } + return Frame{File: file, Line: ln, Func: currFunc}, true + } + // 记录函数名行,下一行通常是文件:行 + currFunc = line + } + return Frame{}, false +} + +func shouldSkip(file string) bool { + // 第三方库与 Go 模块缓存 + if strings.Contains(file, "/go/pkg/mod/") { + return true + } + if strings.Contains(file, "/go.uber.org/") { + return true + } + if strings.Contains(file, "/gorm.io/") { + return true + } + // 标准库 + if strings.Contains(file, "/go/go") && strings.Contains(file, "/src/") { // e.g. /Users/name/go/go1.24.2/src/net/http/server.go + return true + } + // 框架内不需要作为最终调用方的路径 + if strings.Contains(file, "/server/core/zap.go") { + return true + } + if strings.Contains(file, "/server/core/") { + return true + } + if strings.Contains(file, "/server/utils/errorhook/") { + return true + } + if strings.Contains(file, "/server/middleware/") { + return true + } + if strings.Contains(file, "/server/router/") { + return true + } + return false +} \ No newline at end of file