146 lines
4.0 KiB
Go
146 lines
4.0 KiB
Go
package logging
|
|
|
|
import (
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type stackFrame struct {
|
|
File string
|
|
Line int
|
|
Func string
|
|
}
|
|
|
|
var stackFileLinePattern = regexp.MustCompile(`\s*(.+\.go):(\d+)\s*$`)
|
|
|
|
type sourceFunction struct {
|
|
name string
|
|
source string
|
|
startLine, endLine int
|
|
}
|
|
|
|
type sourceFileCache struct {
|
|
size int64
|
|
modTime time.Time
|
|
items []sourceFunction
|
|
}
|
|
|
|
var sourceCache = struct {
|
|
sync.RWMutex
|
|
files map[string]sourceFileCache
|
|
}{files: make(map[string]sourceFileCache)}
|
|
|
|
func finalApplicationCaller(stack string) (stackFrame, bool) {
|
|
if stack == "" {
|
|
return stackFrame{}, false
|
|
}
|
|
functionName := ""
|
|
for _, raw := range strings.Split(stack, "\n") {
|
|
line := strings.TrimSpace(raw)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
matches := stackFileLinePattern.FindStringSubmatch(line)
|
|
if matches == nil {
|
|
functionName = line
|
|
continue
|
|
}
|
|
lineNumber, _ := strconv.Atoi(matches[2])
|
|
if skipStackFile(matches[1]) {
|
|
functionName = ""
|
|
continue
|
|
}
|
|
return stackFrame{File: matches[1], Line: lineNumber, Func: functionName}, true
|
|
}
|
|
return stackFrame{}, false
|
|
}
|
|
|
|
func skipStackFile(filename string) bool {
|
|
normalized := strings.ReplaceAll(filename, "\\", "/")
|
|
for _, marker := range []string{
|
|
"/go/pkg/mod/",
|
|
"/go.uber.org/",
|
|
"/gorm.io/",
|
|
"/internal/logging/",
|
|
"/internal/transport/middleware/",
|
|
"/internal/transport/router/",
|
|
"/internal/server/handler/",
|
|
"/internal/server/middleware/",
|
|
"/internal/server/router/",
|
|
"/internal/server/httpx/",
|
|
"/internal/server/middleware_",
|
|
"/internal/server/route_",
|
|
} {
|
|
if strings.Contains(normalized, marker) {
|
|
return true
|
|
}
|
|
}
|
|
return strings.Contains(normalized, "/src/") &&
|
|
(strings.Contains(normalized, "/go/go") || strings.Contains(normalized, "/go/src/"))
|
|
}
|
|
|
|
func functionSourceAt(filename string, line int) (name, source string, startLine, endLine int, err error) {
|
|
filename = filepath.Clean(filename)
|
|
info, err := os.Stat(filename)
|
|
if err != nil {
|
|
return "", "", 0, 0, fmt.Errorf("stat file failed: %w", err)
|
|
}
|
|
sourceCache.RLock()
|
|
cached, found := sourceCache.files[filename]
|
|
sourceCache.RUnlock()
|
|
if found && cached.size == info.Size() && cached.modTime.Equal(info.ModTime()) {
|
|
return sourceFunctionAtLine(cached.items, line, filename)
|
|
}
|
|
content, err := os.ReadFile(filename)
|
|
if err != nil {
|
|
return "", "", 0, 0, fmt.Errorf("read file failed: %w", err)
|
|
}
|
|
files := token.NewFileSet()
|
|
parsed, err := parser.ParseFile(files, filename, content, parser.ParseComments)
|
|
if err != nil {
|
|
return "", "", 0, 0, fmt.Errorf("parse file failed: %w", err)
|
|
}
|
|
sourceCache.Lock()
|
|
sourceCache.files[filename] = sourceFileCache{size: info.Size(), modTime: info.ModTime(), items: allFunctions(parsed, files, content)}
|
|
cached = sourceCache.files[filename]
|
|
sourceCache.Unlock()
|
|
return sourceFunctionAtLine(cached.items, line, filename)
|
|
}
|
|
|
|
func allFunctions(parsed *ast.File, files *token.FileSet, content []byte) []sourceFunction {
|
|
items := make([]sourceFunction, 0)
|
|
ast.Inspect(parsed, func(node ast.Node) bool {
|
|
declaration, ok := node.(*ast.FuncDecl)
|
|
if !ok {
|
|
return true
|
|
}
|
|
start := files.Position(declaration.Pos()).Line
|
|
end := files.Position(declaration.End()).Line
|
|
startOffset := files.Position(declaration.Pos()).Offset
|
|
endOffset := files.Position(declaration.End()).Offset
|
|
if startOffset >= 0 && endOffset <= len(content) && startOffset < endOffset {
|
|
items = append(items, sourceFunction{name: declaration.Name.Name, source: string(content[startOffset:endOffset]), startLine: start, endLine: end})
|
|
}
|
|
return true
|
|
})
|
|
return items
|
|
}
|
|
|
|
func sourceFunctionAtLine(items []sourceFunction, line int, filename string) (string, string, int, int, error) {
|
|
for _, item := range items {
|
|
if line >= item.startLine && line <= item.endLine {
|
|
return item.name, item.source, item.startLine, item.endLine, nil
|
|
}
|
|
}
|
|
return "", "", 0, 0, fmt.Errorf("no function encloses line %d in %s", line, filename)
|
|
}
|