feat(日志): 增强日志功能以显示调用方源码
This commit is contained in:
parent
8cce9edbde
commit
78d135c6bf
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue