summaryrefslogtreecommitdiffstats
path: root/modules/web/routing/funcinfo.go
blob: f4e9731a630d4dc93aea28bb173afe2ff43a131f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
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
}