Skip to content

Commit 78d135c

Browse files
author
piexlMax(奇淼
committed
feat(日志): 增强日志功能以显示调用方源码
1 parent 8cce9ed commit 78d135c

File tree

3 files changed

+160
-8
lines changed

3 files changed

+160
-8
lines changed

server/core/zap.go

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
99
"github.com/flipped-aurora/gin-vue-admin/server/service"
1010
"github.com/flipped-aurora/gin-vue-admin/server/utils"
11+
astutil "github.com/flipped-aurora/gin-vue-admin/server/utils/ast"
12+
"github.com/flipped-aurora/gin-vue-admin/server/utils/stacktrace"
1113
"go.uber.org/zap"
1214
"go.uber.org/zap/zapcore"
1315
"os"
@@ -53,6 +55,15 @@ func Zap() (logger *zap.Logger) {
5355
stack := entry.Stack
5456
if stack != "" {
5557
info = fmt.Sprintf("%s | stack=%s", info, stack)
58+
// 解析最终业务调用方,并提取其方法源码
59+
if frame, ok := stacktrace.FindFinalCaller(stack); ok {
60+
fnName, fnSrc, sLine, eLine, exErr := astutil.ExtractFuncSourceByPosition(frame.File, frame.Line)
61+
if exErr == nil {
62+
info = fmt.Sprintf("%s | final_caller=%s:%d (%s lines %d-%d)\n----- 产生日志的方法代码如下 -----\n%s", info, frame.File, frame.Line, fnName, sLine, eLine, fnSrc)
63+
} else {
64+
info = fmt.Sprintf("%s | final_caller=%s:%d (%s) | extract_err=%v", info, frame.File, frame.Line, fnName, exErr)
65+
}
66+
}
5667
}
5768

5869
// 使用后台上下文,避免依赖 gin.Context
@@ -65,12 +76,12 @@ func Zap() (logger *zap.Logger) {
6576
return nil
6677
})
6778

68-
logger = zap.New(zapcore.NewTee(cores...), dbHook)
69-
// 启用 Error 及以上级别的堆栈捕捉,确保 entry.Stack 可用
70-
opts := []zap.Option{zap.AddStacktrace(zapcore.ErrorLevel)}
71-
if global.GVA_CONFIG.Zap.ShowLine {
72-
opts = append(opts, zap.AddCaller())
73-
}
74-
logger = logger.WithOptions(opts...)
75-
return logger
79+
logger = zap.New(zapcore.NewTee(cores...), dbHook)
80+
// 启用 Error 及以上级别的堆栈捕捉,确保 entry.Stack 可用
81+
opts := []zap.Option{zap.AddStacktrace(zapcore.ErrorLevel)}
82+
if global.GVA_CONFIG.Zap.ShowLine {
83+
opts = append(opts, zap.AddCaller())
84+
}
85+
logger = logger.WithOptions(opts...)
86+
return logger
7687
}

server/utils/ast/extract_func.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package ast
2+
3+
import (
4+
"fmt"
5+
"go/ast"
6+
"go/parser"
7+
"go/token"
8+
"os"
9+
)
10+
11+
// ExtractFuncSourceByPosition 根据文件路径与行号,提取包含该行的整个方法源码
12+
// 返回:方法名、完整源码、起止行号
13+
func ExtractFuncSourceByPosition(filePath string, line int) (name string, source string, startLine int, endLine int, err error) {
14+
// 读取源文件
15+
src, readErr := os.ReadFile(filePath)
16+
if readErr != nil {
17+
err = fmt.Errorf("read file failed: %w", readErr)
18+
return
19+
}
20+
21+
// 解析 AST
22+
fset := token.NewFileSet()
23+
file, parseErr := parser.ParseFile(fset, filePath, src, parser.ParseComments)
24+
if parseErr != nil {
25+
err = fmt.Errorf("parse file failed: %w", parseErr)
26+
return
27+
}
28+
29+
// 在 AST 中定位包含指定行号的函数声明
30+
var target *ast.FuncDecl
31+
ast.Inspect(file, func(n ast.Node) bool {
32+
fd, ok := n.(*ast.FuncDecl)
33+
if !ok {
34+
return true
35+
}
36+
s := fset.Position(fd.Pos()).Line
37+
e := fset.Position(fd.End()).Line
38+
if line >= s && line <= e {
39+
target = fd
40+
startLine = s
41+
endLine = e
42+
return false
43+
}
44+
return true
45+
})
46+
47+
if target == nil {
48+
err = fmt.Errorf("no function encloses line %d in %s", line, filePath)
49+
return
50+
}
51+
52+
// 使用字节偏移精确提取源码片段(包含注释与原始格式)
53+
start := fset.Position(target.Pos()).Offset
54+
end := fset.Position(target.End()).Offset
55+
if start < 0 || end > len(src) || start >= end {
56+
err = fmt.Errorf("invalid offsets for function: start=%d end=%d len=%d", start, end, len(src))
57+
return
58+
}
59+
source = string(src[start:end])
60+
name = target.Name.Name
61+
return
62+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package stacktrace
2+
3+
import (
4+
"regexp"
5+
"strconv"
6+
"strings"
7+
)
8+
9+
// Frame 表示一次栈帧解析结果
10+
type Frame struct {
11+
File string
12+
Line int
13+
Func string
14+
}
15+
16+
var fileLineRe = regexp.MustCompile(`\s*(.+\.go):(\d+)\s*$`)
17+
18+
// FindFinalCaller 从 zap 的 entry.Stack 文本中,解析“最终业务调用方”的文件与行号
19+
// 策略:自顶向下解析,优先选择第一条项目代码帧,过滤第三方库/标准库/框架中间件
20+
func FindFinalCaller(stack string) (Frame, bool) {
21+
if stack == "" {
22+
return Frame{}, false
23+
}
24+
lines := strings.Split(stack, "\n")
25+
var currFunc string
26+
for i := 0; i < len(lines); i++ {
27+
line := strings.TrimSpace(lines[i])
28+
if line == "" {
29+
continue
30+
}
31+
if m := fileLineRe.FindStringSubmatch(line); m != nil {
32+
file := m[1]
33+
ln, _ := strconv.Atoi(m[2])
34+
if shouldSkip(file) {
35+
// 跳过此帧,同时重置函数名以避免错误配对
36+
currFunc = ""
37+
continue
38+
}
39+
return Frame{File: file, Line: ln, Func: currFunc}, true
40+
}
41+
// 记录函数名行,下一行通常是文件:行
42+
currFunc = line
43+
}
44+
return Frame{}, false
45+
}
46+
47+
func shouldSkip(file string) bool {
48+
// 第三方库与 Go 模块缓存
49+
if strings.Contains(file, "/go/pkg/mod/") {
50+
return true
51+
}
52+
if strings.Contains(file, "/go.uber.org/") {
53+
return true
54+
}
55+
if strings.Contains(file, "/gorm.io/") {
56+
return true
57+
}
58+
// 标准库
59+
if strings.Contains(file, "/go/go") && strings.Contains(file, "/src/") { // e.g. /Users/name/go/go1.24.2/src/net/http/server.go
60+
return true
61+
}
62+
// 框架内不需要作为最终调用方的路径
63+
if strings.Contains(file, "/server/core/zap.go") {
64+
return true
65+
}
66+
if strings.Contains(file, "/server/core/") {
67+
return true
68+
}
69+
if strings.Contains(file, "/server/utils/errorhook/") {
70+
return true
71+
}
72+
if strings.Contains(file, "/server/middleware/") {
73+
return true
74+
}
75+
if strings.Contains(file, "/server/router/") {
76+
return true
77+
}
78+
return false
79+
}

0 commit comments

Comments
 (0)