code.gitea.io/gitea@v1.19.3/modules/web/routing/funcinfo.go (about)

     1  // Copyright 2021 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package routing
     5  
     6  import (
     7  	"fmt"
     8  	"reflect"
     9  	"runtime"
    10  	"strings"
    11  	"sync"
    12  )
    13  
    14  var (
    15  	funcInfoMap     = map[uintptr]*FuncInfo{}
    16  	funcInfoNameMap = map[string]*FuncInfo{}
    17  	funcInfoMapMu   sync.RWMutex
    18  )
    19  
    20  // FuncInfo contains information about the function to be logged by the router log
    21  type FuncInfo struct {
    22  	file      string
    23  	shortFile string
    24  	line      int
    25  	name      string
    26  	shortName string
    27  }
    28  
    29  // String returns a string form of the FuncInfo for logging
    30  func (info *FuncInfo) String() string {
    31  	if info == nil {
    32  		return "unknown-handler"
    33  	}
    34  	return fmt.Sprintf("%s:%d(%s)", info.shortFile, info.line, info.shortName)
    35  }
    36  
    37  // GetFuncInfo returns the FuncInfo for a provided function and friendlyname
    38  func GetFuncInfo(fn interface{}, friendlyName ...string) *FuncInfo {
    39  	// ptr represents the memory position of the function passed in as v.
    40  	// This will be used as program counter in FuncForPC below
    41  	ptr := reflect.ValueOf(fn).Pointer()
    42  
    43  	// if we have been provided with a friendlyName look for the named funcs
    44  	if len(friendlyName) == 1 {
    45  		name := friendlyName[0]
    46  		funcInfoMapMu.RLock()
    47  		info, ok := funcInfoNameMap[name]
    48  		funcInfoMapMu.RUnlock()
    49  		if ok {
    50  			return info
    51  		}
    52  	}
    53  
    54  	// Otherwise attempt to get pre-cached information for this function pointer
    55  	funcInfoMapMu.RLock()
    56  	info, ok := funcInfoMap[ptr]
    57  	funcInfoMapMu.RUnlock()
    58  
    59  	if ok {
    60  		if len(friendlyName) == 1 {
    61  			name := friendlyName[0]
    62  			info = copyFuncInfo(info)
    63  			info.shortName = name
    64  
    65  			funcInfoNameMap[name] = info
    66  			funcInfoMapMu.Lock()
    67  			funcInfoNameMap[name] = info
    68  			funcInfoMapMu.Unlock()
    69  		}
    70  		return info
    71  	}
    72  
    73  	// This is likely the first time we have seen this function
    74  	//
    75  	// Get the runtime.func for this function (if we can)
    76  	f := runtime.FuncForPC(ptr)
    77  	if f != nil {
    78  		info = convertToFuncInfo(f)
    79  
    80  		// cache this info globally
    81  		funcInfoMapMu.Lock()
    82  		funcInfoMap[ptr] = info
    83  
    84  		// if we have been provided with a friendlyName override the short name we've generated
    85  		if len(friendlyName) == 1 {
    86  			name := friendlyName[0]
    87  			info = copyFuncInfo(info)
    88  			info.shortName = name
    89  			funcInfoNameMap[name] = info
    90  		}
    91  		funcInfoMapMu.Unlock()
    92  	}
    93  	return info
    94  }
    95  
    96  // convertToFuncInfo take a runtime.Func and convert it to a logFuncInfo, fill in shorten filename, etc
    97  func convertToFuncInfo(f *runtime.Func) *FuncInfo {
    98  	file, line := f.FileLine(f.Entry())
    99  
   100  	info := &FuncInfo{
   101  		file: strings.ReplaceAll(file, "\\", "/"),
   102  		line: line,
   103  		name: f.Name(),
   104  	}
   105  
   106  	// only keep last 2 names in path, fall back to funcName if not
   107  	info.shortFile = shortenFilename(info.file, info.name)
   108  
   109  	// remove package prefix. eg: "xxx.com/pkg1/pkg2.foo" => "pkg2.foo"
   110  	pos := strings.LastIndexByte(info.name, '/')
   111  	if pos >= 0 {
   112  		info.shortName = info.name[pos+1:]
   113  	} else {
   114  		info.shortName = info.name
   115  	}
   116  
   117  	// remove ".func[0-9]*" suffix for anonymous func
   118  	info.shortName = trimAnonymousFunctionSuffix(info.shortName)
   119  
   120  	return info
   121  }
   122  
   123  func copyFuncInfo(l *FuncInfo) *FuncInfo {
   124  	return &FuncInfo{
   125  		file:      l.file,
   126  		shortFile: l.shortFile,
   127  		line:      l.line,
   128  		name:      l.name,
   129  		shortName: l.shortName,
   130  	}
   131  }
   132  
   133  // 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"
   134  func shortenFilename(filename, fallback string) string {
   135  	if filename == "" {
   136  		return fallback
   137  	}
   138  	if lastIndex := strings.LastIndexByte(filename, '/'); lastIndex >= 0 {
   139  		if secondLastIndex := strings.LastIndexByte(filename[:lastIndex], '/'); secondLastIndex >= 0 {
   140  			return filename[secondLastIndex+1:]
   141  		}
   142  	}
   143  	return filename
   144  }
   145  
   146  // trimAnonymousFunctionSuffix trims ".func[0-9]*" from the end of anonymous function names, we only want to see the main function names in logs
   147  func trimAnonymousFunctionSuffix(name string) string {
   148  	// if the name is an anonymous name, it should be like "{main-function}.func1", so the length can not be less than 7
   149  	if len(name) < 7 {
   150  		return name
   151  	}
   152  
   153  	funcSuffixIndex := strings.LastIndex(name, ".func")
   154  	if funcSuffixIndex < 0 {
   155  		return name
   156  	}
   157  
   158  	hasFuncSuffix := true
   159  
   160  	// len(".func") = 5
   161  	for i := funcSuffixIndex + 5; i < len(name); i++ {
   162  		if name[i] < '0' || name[i] > '9' {
   163  			hasFuncSuffix = false
   164  			break
   165  		}
   166  	}
   167  
   168  	if hasFuncSuffix {
   169  		return name[:funcSuffixIndex]
   170  	}
   171  	return name
   172  }