diff options
Diffstat (limited to 'modules/web/routing/funcinfo.go')
-rw-r--r-- | modules/web/routing/funcinfo.go | 172 |
1 files changed, 172 insertions, 0 deletions
diff --git a/modules/web/routing/funcinfo.go b/modules/web/routing/funcinfo.go new file mode 100644 index 00000000..f4e9731a --- /dev/null +++ b/modules/web/routing/funcinfo.go @@ -0,0 +1,172 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package routing + +import ( + "fmt" + "reflect" + "runtime" + "strings" + "sync" +) + +var ( + funcInfoMap = map[uintptr]*FuncInfo{} + funcInfoNameMap = map[string]*FuncInfo{} + funcInfoMapMu sync.RWMutex +) + +// FuncInfo contains information about the function to be logged by the router log +type FuncInfo struct { + file string + shortFile string + line int + name string + shortName string +} + +// String returns a string form of the FuncInfo for logging +func (info *FuncInfo) String() string { + if info == nil { + return "unknown-handler" + } + return fmt.Sprintf("%s:%d(%s)", info.shortFile, info.line, info.shortName) +} + +// GetFuncInfo returns the FuncInfo for a provided function and friendlyname +func GetFuncInfo(fn any, friendlyName ...string) *FuncInfo { + // ptr represents the memory position of the function passed in as v. + // This will be used as program counter in FuncForPC below + ptr := reflect.ValueOf(fn).Pointer() + + // if we have been provided with a friendlyName look for the named funcs + if len(friendlyName) == 1 { + name := friendlyName[0] + funcInfoMapMu.RLock() + info, ok := funcInfoNameMap[name] + funcInfoMapMu.RUnlock() + if ok { + return info + } + } + + // Otherwise attempt to get pre-cached information for this function pointer + funcInfoMapMu.RLock() + info, ok := funcInfoMap[ptr] + funcInfoMapMu.RUnlock() + + if ok { + if len(friendlyName) == 1 { + name := friendlyName[0] + info = copyFuncInfo(info) + info.shortName = name + + funcInfoNameMap[name] = info + funcInfoMapMu.Lock() + funcInfoNameMap[name] = info + funcInfoMapMu.Unlock() + } + return info + } + + // This is likely the first time we have seen this function + // + // Get the runtime.func for this function (if we can) + f := runtime.FuncForPC(ptr) + if f != nil { + info = convertToFuncInfo(f) + + // cache this info globally + funcInfoMapMu.Lock() + funcInfoMap[ptr] = info + + // if we have been provided with a friendlyName override the short name we've generated + if len(friendlyName) == 1 { + name := friendlyName[0] + info = copyFuncInfo(info) + info.shortName = name + funcInfoNameMap[name] = info + } + funcInfoMapMu.Unlock() + } + return info +} + +// convertToFuncInfo take a runtime.Func and convert it to a logFuncInfo, fill in shorten filename, etc +func convertToFuncInfo(f *runtime.Func) *FuncInfo { + file, line := f.FileLine(f.Entry()) + + info := &FuncInfo{ + file: strings.ReplaceAll(file, "\\", "/"), + line: line, + name: f.Name(), + } + + // only keep last 2 names in path, fall back to funcName if not + info.shortFile = shortenFilename(info.file, info.name) + + // remove package prefix. eg: "xxx.com/pkg1/pkg2.foo" => "pkg2.foo" + pos := strings.LastIndexByte(info.name, '/') + if pos >= 0 { + info.shortName = info.name[pos+1:] + } else { + info.shortName = info.name + } + + // remove ".func[0-9]*" suffix for anonymous func + info.shortName = trimAnonymousFunctionSuffix(info.shortName) + + return info +} + +func copyFuncInfo(l *FuncInfo) *FuncInfo { + return &FuncInfo{ + file: l.file, + shortFile: l.shortFile, + line: l.line, + name: l.name, + shortName: l.shortName, + } +} + +// shortenFilename generates a short source code filename from a full package path, eg: "code.gitea.io/routers/common/logger_context.go" => "common/logger_context.go" +func shortenFilename(filename, fallback string) string { + if filename == "" { + return fallback + } + if lastIndex := strings.LastIndexByte(filename, '/'); lastIndex >= 0 { + if secondLastIndex := strings.LastIndexByte(filename[:lastIndex], '/'); secondLastIndex >= 0 { + return filename[secondLastIndex+1:] + } + } + return filename +} + +// trimAnonymousFunctionSuffix trims ".func[0-9]*" from the end of anonymous function names, we only want to see the main function names in logs +func trimAnonymousFunctionSuffix(name string) string { + // if the name is an anonymous name, it should be like "{main-function}.func1", so the length can not be less than 7 + if len(name) < 7 { + return name + } + + funcSuffixIndex := strings.LastIndex(name, ".func") + if funcSuffixIndex < 0 { + return name + } + + hasFuncSuffix := true + + // len(".func") = 5 + for i := funcSuffixIndex + 5; i < len(name); i++ { + if name[i] < '0' || name[i] > '9' { + hasFuncSuffix = false + break + } + } + + if hasFuncSuffix { + return name[:funcSuffixIndex] + } + return name +} |