diff options
author | wxiaoguang <wxiaoguang@gmail.com> | 2022-01-20 19:41:25 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-01-20 19:41:25 +0800 |
commit | 5bf8d5445e3e3ec6b2be156654dcb233d78a570e (patch) | |
tree | 9a426fd03b619947b88931465c1a645540b94bfb /modules | |
parent | bbd30787d3d33fe66896c7e758e6922be459e252 (diff) | |
download | gitea-5bf8d5445e3e3ec6b2be156654dcb233d78a570e.tar.gz gitea-5bf8d5445e3e3ec6b2be156654dcb233d78a570e.zip |
Refactor Router Logger (#17308)
Make router logger more friendly, show the related function name/file/line.
[BREAKING]
This PR substantially changes the logging format of the router logger. If you use this logging for monitoring e.g. fail2ban you will need to update this to match the new format.
Diffstat (limited to 'modules')
-rw-r--r-- | modules/log/colors_router.go | 11 | ||||
-rw-r--r-- | modules/log/log.go | 4 | ||||
-rw-r--r-- | modules/public/public.go | 89 | ||||
-rw-r--r-- | modules/setting/log.go | 7 | ||||
-rw-r--r-- | modules/setting/setting.go | 10 | ||||
-rw-r--r-- | modules/web/route.go | 138 | ||||
-rw-r--r-- | modules/web/routing/context.go | 51 | ||||
-rw-r--r-- | modules/web/routing/funcinfo.go | 171 | ||||
-rw-r--r-- | modules/web/routing/funcinfo_test.go | 81 | ||||
-rw-r--r-- | modules/web/routing/logger.go | 106 | ||||
-rw-r--r-- | modules/web/routing/logger_manager.go | 122 | ||||
-rw-r--r-- | modules/web/routing/requestrecord.go | 29 | ||||
-rw-r--r-- | modules/web/wrap.go | 117 | ||||
-rw-r--r-- | modules/web/wrap_convert.go | 101 |
14 files changed, 843 insertions, 194 deletions
diff --git a/modules/log/colors_router.go b/modules/log/colors_router.go index 3064e005cf..888198d4e9 100644 --- a/modules/log/colors_router.go +++ b/modules/log/colors_router.go @@ -5,6 +5,7 @@ package log import ( + "fmt" "time" ) @@ -72,12 +73,16 @@ var ( wayTooLong = ColorBytes(BgMagenta) ) -// ColoredTime adds colors for time on log +// ColoredTime converts the provided time to a ColoredValue for logging. The duration is always formatted in milliseconds. func ColoredTime(duration time.Duration) *ColoredValue { + // the output of duration in Millisecond is more readable: + // * before: "100.1ms" "100.1μs" "100.1s" + // * better: "100.1ms" "0.1ms" "100100.0ms", readers can compare the values at first glance. + str := fmt.Sprintf("%.1fms", float64(duration.Microseconds())/1000) for i, k := range durations { if duration < k { - return NewColoredValueBytes(duration, &durationColors[i]) + return NewColoredValueBytes(str, &durationColors[i]) } } - return NewColoredValueBytes(duration, &wayTooLong) + return NewColoredValueBytes(str, &wayTooLong) } diff --git a/modules/log/log.go b/modules/log/log.go index cae24f53d8..f805a36231 100644 --- a/modules/log/log.go +++ b/modules/log/log.go @@ -288,4 +288,8 @@ func (l *LoggerAsWriter) Log(msg string) { func init() { _, filename, _, _ := runtime.Caller(0) prefix = strings.TrimSuffix(filename, "modules/log/log.go") + if prefix == filename { + // in case the source code file is moved, we can not trim the suffix, the code above should also be updated. + panic("unable to detect correct package prefix, please update file: " + filename) + } } diff --git a/modules/public/public.go b/modules/public/public.go index a58709d86f..f4a769f27a 100644 --- a/modules/public/public.go +++ b/modules/public/public.go @@ -23,8 +23,11 @@ type Options struct { CorsHandler func(http.Handler) http.Handler } -// AssetsHandler implements the static handler for serving custom or original assets. -func AssetsHandler(opts *Options) func(next http.Handler) http.Handler { +// AssetsURLPathPrefix is the path prefix for static asset files +const AssetsURLPathPrefix = "/assets/" + +// AssetsHandlerFunc implements the static handler for serving custom or original assets. +func AssetsHandlerFunc(opts *Options) http.HandlerFunc { var custPath = filepath.Join(setting.CustomPath, "public") if !filepath.IsAbs(custPath) { custPath = filepath.Join(setting.AppWorkPath, custPath) @@ -36,52 +39,46 @@ func AssetsHandler(opts *Options) func(next http.Handler) http.Handler { opts.Prefix += "/" } - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - if !strings.HasPrefix(req.URL.Path, opts.Prefix) { - next.ServeHTTP(resp, req) - return - } - if req.Method != "GET" && req.Method != "HEAD" { - resp.WriteHeader(http.StatusNotFound) - return - } - - file := req.URL.Path - file = file[len(opts.Prefix):] - if len(file) == 0 { - resp.WriteHeader(http.StatusNotFound) - return - } - if strings.Contains(file, "\\") { - resp.WriteHeader(http.StatusBadRequest) - return - } - file = "/" + file - - var written bool - if opts.CorsHandler != nil { - written = true - opts.CorsHandler(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { - written = false - })).ServeHTTP(resp, req) - } - if written { - return - } - - // custom files - if opts.handle(resp, req, http.Dir(custPath), file) { - return - } - - // internal files - if opts.handle(resp, req, fileSystem(opts.Directory), file) { - return - } + return func(resp http.ResponseWriter, req *http.Request) { + if req.Method != "GET" && req.Method != "HEAD" { + resp.WriteHeader(http.StatusNotFound) + return + } + file := req.URL.Path + file = file[len(opts.Prefix):] + if len(file) == 0 { resp.WriteHeader(http.StatusNotFound) - }) + return + } + if strings.Contains(file, "\\") { + resp.WriteHeader(http.StatusBadRequest) + return + } + file = "/" + file + + var written bool + if opts.CorsHandler != nil { + written = true + opts.CorsHandler(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { + written = false + })).ServeHTTP(resp, req) + } + if written { + return + } + + // custom files + if opts.handle(resp, req, http.Dir(custPath), file) { + return + } + + // internal files + if opts.handle(resp, req, fileSystem(opts.Directory), file) { + return + } + + resp.WriteHeader(http.StatusNotFound) } } diff --git a/modules/setting/log.go b/modules/setting/log.go index b0ae5fb646..a53b431196 100644 --- a/modules/setting/log.go +++ b/modules/setting/log.go @@ -126,6 +126,7 @@ func getStacktraceLogLevel(section *ini.Section, key, defaultValue string) strin func generateLogConfig(sec *ini.Section, name string, defaults defaultLogOptions) (mode, jsonConfig, levelName string) { level := getLogLevel(sec, "LEVEL", LogLevel) + levelName = level.String() stacktraceLevelName := getStacktraceLogLevel(sec, "STACKTRACE_LEVEL", StacktraceLogLevel) stacktraceLevel := log.FromString(stacktraceLevelName) mode = name @@ -254,8 +255,10 @@ func generateNamedLogger(key string, options defaultLogOptions) *LogDescription func newAccessLogService() { EnableAccessLog = Cfg.Section("log").Key("ENABLE_ACCESS_LOG").MustBool(false) AccessLogTemplate = Cfg.Section("log").Key("ACCESS_LOG_TEMPLATE").MustString( - `{{.Ctx.RemoteAddr}} - {{.Identity}} {{.Start.Format "[02/Jan/2006:15:04:05 -0700]" }} "{{.Ctx.Req.Method}} {{.Ctx.Req.URL.RequestURI}} {{.Ctx.Req.Proto}}" {{.ResponseWriter.Status}} {{.ResponseWriter.Size}} "{{.Ctx.Req.Referer}}\" \"{{.Ctx.Req.UserAgent}}"`) - Cfg.Section("log").Key("ACCESS").MustString("file") + `{{.Ctx.RemoteAddr}} - {{.Identity}} {{.Start.Format "[02/Jan/2006:15:04:05 -0700]" }} "{{.Ctx.Req.Method}} {{.Ctx.Req.URL.RequestURI}} {{.Ctx.Req.Proto}}" {{.ResponseWriter.Status}} {{.ResponseWriter.Size}} "{{.Ctx.Req.Referer}}\" \"{{.Ctx.Req.UserAgent}}"`, + ) + // the `MustString` updates the default value, and `log.ACCESS` is used by `generateNamedLogger("access")` later + _ = Cfg.Section("log").Key("ACCESS").MustString("file") if EnableAccessLog { options := newDefaultLogOptions() options.filename = filepath.Join(LogRootPath, "access.log") diff --git a/modules/setting/setting.go b/modules/setting/setting.go index abd6716c74..26eeda525f 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -333,13 +333,14 @@ var ( LogLevel log.Level StacktraceLogLevel string LogRootPath string - DisableRouterLog bool - RouterLogLevel log.Level - EnableAccessLog bool EnableSSHLog bool - AccessLogTemplate string EnableXORMLog bool + DisableRouterLog bool + + EnableAccessLog bool + AccessLogTemplate string + // Time settings TimeFormat string // UILocation is the location on the UI, so that we can display the time on UI. @@ -601,7 +602,6 @@ func loadFromConf(allowEmpty bool, extraConfig string) { StacktraceLogLevel = getStacktraceLogLevel(Cfg.Section("log"), "STACKTRACE_LEVEL", "None") LogRootPath = Cfg.Section("log").Key("ROOT_PATH").MustString(path.Join(AppWorkPath, "log")) forcePathSeparator(LogRootPath) - RouterLogLevel = log.FromString(Cfg.Section("log").Key("ROUTER_LOG_LEVEL").MustString("Info")) sec := Cfg.Section("server") AppName = Cfg.Section("").Key("APP_NAME").MustString("Gitea: Git with a cup of tea") diff --git a/modules/web/route.go b/modules/web/route.go index 19193773d5..86e407701c 100644 --- a/modules/web/route.go +++ b/modules/web/route.go @@ -18,144 +18,6 @@ import ( chi "github.com/go-chi/chi/v5" ) -// Wrap converts all kinds of routes to standard library one -func Wrap(handlers ...interface{}) http.HandlerFunc { - if len(handlers) == 0 { - panic("No handlers found") - } - - for _, handler := range handlers { - switch t := handler.(type) { - case http.HandlerFunc, func(http.ResponseWriter, *http.Request), - func(ctx *context.Context), - func(ctx *context.Context) goctx.CancelFunc, - func(*context.APIContext), - func(*context.APIContext) goctx.CancelFunc, - func(*context.PrivateContext), - func(*context.PrivateContext) goctx.CancelFunc, - func(http.Handler) http.Handler: - default: - panic(fmt.Sprintf("Unsupported handler type: %#v", t)) - } - } - return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - for i := 0; i < len(handlers); i++ { - handler := handlers[i] - switch t := handler.(type) { - case http.HandlerFunc: - t(resp, req) - if r, ok := resp.(context.ResponseWriter); ok && r.Status() > 0 { - return - } - case func(http.ResponseWriter, *http.Request): - t(resp, req) - if r, ok := resp.(context.ResponseWriter); ok && r.Status() > 0 { - return - } - case func(ctx *context.Context) goctx.CancelFunc: - ctx := context.GetContext(req) - cancel := t(ctx) - if cancel != nil { - defer cancel() - } - if ctx.Written() { - return - } - case func(*context.APIContext) goctx.CancelFunc: - ctx := context.GetAPIContext(req) - cancel := t(ctx) - if cancel != nil { - defer cancel() - } - if ctx.Written() { - return - } - case func(*context.PrivateContext) goctx.CancelFunc: - ctx := context.GetPrivateContext(req) - cancel := t(ctx) - if cancel != nil { - defer cancel() - } - if ctx.Written() { - return - } - case func(ctx *context.Context): - ctx := context.GetContext(req) - t(ctx) - if ctx.Written() { - return - } - case func(*context.APIContext): - ctx := context.GetAPIContext(req) - t(ctx) - if ctx.Written() { - return - } - case func(*context.PrivateContext): - ctx := context.GetPrivateContext(req) - t(ctx) - if ctx.Written() { - return - } - case func(http.Handler) http.Handler: - var next = http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}) - if len(handlers) > i+1 { - next = Wrap(handlers[i+1:]...) - } - t(next).ServeHTTP(resp, req) - return - default: - panic(fmt.Sprintf("Unsupported handler type: %#v", t)) - } - } - }) -} - -// Middle wrap a context function as a chi middleware -func Middle(f func(ctx *context.Context)) func(netx http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - ctx := context.GetContext(req) - f(ctx) - if ctx.Written() { - return - } - next.ServeHTTP(ctx.Resp, ctx.Req) - }) - } -} - -// MiddleCancel wrap a context function as a chi middleware -func MiddleCancel(f func(ctx *context.Context) goctx.CancelFunc) func(netx http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - ctx := context.GetContext(req) - cancel := f(ctx) - if cancel != nil { - defer cancel() - } - if ctx.Written() { - return - } - next.ServeHTTP(ctx.Resp, ctx.Req) - }) - } -} - -// MiddleAPI wrap a context function as a chi middleware -func MiddleAPI(f func(ctx *context.APIContext)) func(netx http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - ctx := context.GetAPIContext(req) - f(ctx) - if ctx.Written() { - return - } - next.ServeHTTP(ctx.Resp, ctx.Req) - }) - } -} - // Bind binding an obj to a handler func Bind(obj interface{}) http.HandlerFunc { var tp = reflect.TypeOf(obj) diff --git a/modules/web/routing/context.go b/modules/web/routing/context.go new file mode 100644 index 0000000000..f57b2e9026 --- /dev/null +++ b/modules/web/routing/context.go @@ -0,0 +1,51 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package routing + +import ( + "context" + "net/http" +) + +type contextKeyType struct{} + +var contextKey contextKeyType + +//UpdateFuncInfo updates a context's func info +func UpdateFuncInfo(ctx context.Context, funcInfo *FuncInfo) { + record, ok := ctx.Value(contextKey).(*requestRecord) + if !ok { + return + } + + record.lock.Lock() + record.funcInfo = funcInfo + record.lock.Unlock() + +} + +// MarkLongPolling marks the reuqest is a long-polling request, and the logger may output different message for it +func MarkLongPolling(resp http.ResponseWriter, req *http.Request) { + record, ok := req.Context().Value(contextKey).(*requestRecord) + if !ok { + return + } + + record.lock.Lock() + record.isLongPolling = true + record.lock.Unlock() +} + +//UpdatePanicError updates a context's error info, a panic may be recovered by other middlewares, but we still need to know that. +func UpdatePanicError(ctx context.Context, err interface{}) { + record, ok := ctx.Value(contextKey).(*requestRecord) + if !ok { + return + } + + record.lock.Lock() + record.panicError = err + record.lock.Unlock() +} diff --git a/modules/web/routing/funcinfo.go b/modules/web/routing/funcinfo.go new file mode 100644 index 0000000000..6d3986e364 --- /dev/null +++ b/modules/web/routing/funcinfo.go @@ -0,0 +1,171 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package routing + +import ( + "fmt" + "reflect" + "runtime" + "strings" + "sync" +) + +var funcInfoMap = map[uintptr]*FuncInfo{} +var funcInfoNameMap = map[string]*FuncInfo{} +var 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 interface{}, 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 +} diff --git a/modules/web/routing/funcinfo_test.go b/modules/web/routing/funcinfo_test.go new file mode 100644 index 0000000000..9cea7e2817 --- /dev/null +++ b/modules/web/routing/funcinfo_test.go @@ -0,0 +1,81 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package routing + +import ( + "fmt" + "testing" +) + +func Test_shortenFilename(t *testing.T) { + tests := []struct { + filename string + fallback string + expected string + }{ + { + "code.gitea.io/routers/common/logger_context.go", + "NO_FALLBACK", + "common/logger_context.go", + }, + { + "common/logger_context.go", + "NO_FALLBACK", + "common/logger_context.go", + }, + { + "logger_context.go", + "NO_FALLBACK", + "logger_context.go", + }, + { + "", + "USE_FALLBACK", + "USE_FALLBACK", + }, + } + for _, tt := range tests { + t.Run(fmt.Sprintf("shortenFilename('%s')", tt.filename), func(t *testing.T) { + if gotShort := shortenFilename(tt.filename, tt.fallback); gotShort != tt.expected { + t.Errorf("shortenFilename('%s'), expect '%s', but get '%s'", tt.filename, tt.expected, gotShort) + } + }) + } +} + +func Test_trimAnonymousFunctionSuffix(t *testing.T) { + tests := []struct { + name string + want string + }{ + { + "notAnonymous", + "notAnonymous", + }, + { + "anonymous.func1", + "anonymous", + }, + { + "notAnonymous.funca", + "notAnonymous.funca", + }, + { + "anonymous.func100", + "anonymous", + }, + { + "anonymous.func100.func6", + "anonymous.func100", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := trimAnonymousFunctionSuffix(tt.name); got != tt.want { + t.Errorf("trimAnonymousFunctionSuffix() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/modules/web/routing/logger.go b/modules/web/routing/logger.go new file mode 100644 index 0000000000..cf3b50725d --- /dev/null +++ b/modules/web/routing/logger.go @@ -0,0 +1,106 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package routing + +import ( + "net/http" + "time" + + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" +) + +// NewLoggerHandler is a handler that will log routing to the router log taking account of +// routing information +func NewLoggerHandler() func(next http.Handler) http.Handler { + manager := requestRecordsManager{ + requestRecords: map[uint64]*requestRecord{}, + } + manager.startSlowQueryDetector(3 * time.Second) + + logger := log.GetLogger("router") + manager.print = logPrinter(logger) + return manager.handler +} + +var ( + startMessage = log.NewColoredValueBytes("started ", log.DEBUG.Color()) + slowMessage = log.NewColoredValueBytes("slow ", log.WARN.Color()) + pollingMessage = log.NewColoredValueBytes("polling ", log.INFO.Color()) + failedMessage = log.NewColoredValueBytes("failed ", log.WARN.Color()) + completedMessage = log.NewColoredValueBytes("completed", log.INFO.Color()) + unknownHandlerMessage = log.NewColoredValueBytes("completed", log.ERROR.Color()) +) + +func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) { + return func(trigger Event, record *requestRecord) { + if trigger == StartEvent { + if !logger.IsTrace() { + // for performance, if the "started" message shouldn't be logged, we just return as early as possible + // developers can set the router log level to TRACE to get the "started" request messages. + return + } + // when a request starts, we have no information about the handler function information, we only have the request path + req := record.request + logger.Trace("router: %s %v %s for %s", startMessage, log.ColoredMethod(req.Method), req.RequestURI, req.RemoteAddr) + return + } + + req := record.request + + // Get data from the record + record.lock.Lock() + handlerFuncInfo := record.funcInfo.String() + isLongPolling := record.isLongPolling + isUnknownHandler := record.funcInfo == nil + panicErr := record.panicError + record.lock.Unlock() + + if trigger == StillExecutingEvent { + message := slowMessage + level := log.WARN + if isLongPolling { + level = log.INFO + message = pollingMessage + } + _ = logger.Log(0, level, "router: %s %v %s for %s, elapsed %v @ %s", + message, + log.ColoredMethod(req.Method), req.RequestURI, req.RemoteAddr, + log.ColoredTime(time.Since(record.startTime)), + handlerFuncInfo, + ) + return + } + + if panicErr != nil { + _ = logger.Log(0, log.WARN, "router: %s %v %s for %s, panic in %v @ %s, err=%v", + failedMessage, + log.ColoredMethod(req.Method), req.RequestURI, req.RemoteAddr, + log.ColoredTime(time.Since(record.startTime)), + handlerFuncInfo, + panicErr, + ) + return + } + + var status int + if v, ok := record.responseWriter.(context.ResponseWriter); ok { + status = v.Status() + } + level := log.INFO + message := completedMessage + if isUnknownHandler { + level = log.ERROR + message = unknownHandlerMessage + } + + _ = logger.Log(0, level, "router: %s %v %s for %s, %v %v in %v @ %s", + message, + log.ColoredMethod(req.Method), req.RequestURI, req.RemoteAddr, + log.ColoredStatus(status), log.ColoredStatus(status, http.StatusText(status)), log.ColoredTime(time.Since(record.startTime)), + handlerFuncInfo, + ) + } +} diff --git a/modules/web/routing/logger_manager.go b/modules/web/routing/logger_manager.go new file mode 100644 index 0000000000..31fd09f312 --- /dev/null +++ b/modules/web/routing/logger_manager.go @@ -0,0 +1,122 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package routing + +import ( + "context" + "net/http" + "sync" + "time" + + "code.gitea.io/gitea/modules/graceful" +) + +// Event indicates when the printer is triggered +type Event int + +const ( + //StartEvent at the beginning of a request + StartEvent Event = iota + + //StillExecutingEvent the request is still executing + StillExecutingEvent + + //EndEvent the request has ended (either completed or failed) + EndEvent +) + +// Printer is used to output the log for a request +type Printer func(trigger Event, record *requestRecord) + +type requestRecordsManager struct { + print Printer + + lock sync.Mutex + + requestRecords map[uint64]*requestRecord + count uint64 +} + +func (manager *requestRecordsManager) startSlowQueryDetector(threshold time.Duration) { + go graceful.GetManager().RunWithShutdownContext(func(baseCtx context.Context) { + // This go-routine checks all active requests every second. + // If a request has been running for a long time (eg: /user/events), we also print a log with "still-executing" message + // After the "still-executing" log is printed, the record will be removed from the map to prevent from duplicated logs in future + + // We do not care about accurate duration here. It just does the check periodically, 0.5s or 1.5s are all OK. + t := time.NewTicker(time.Second) + for { + select { + case <-baseCtx.Done(): + return + case <-t.C: + now := time.Now() + + var slowRequests []*requestRecord + + // find all slow requests with lock + manager.lock.Lock() + for index, record := range manager.requestRecords { + if now.Sub(record.startTime) < threshold { + continue + } + + slowRequests = append(slowRequests, record) + delete(manager.requestRecords, index) + } + manager.lock.Unlock() + + // print logs for slow requests + for _, record := range slowRequests { + manager.print(StillExecutingEvent, record) + } + } + } + }) +} + +func (manager *requestRecordsManager) handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + record := &requestRecord{ + startTime: time.Now(), + request: req, + responseWriter: w, + } + + // generate a record index an insert into the map + manager.lock.Lock() + record.index = manager.count + manager.count++ + manager.requestRecords[record.index] = record + manager.lock.Unlock() + + defer func() { + // just in case there is a panic. now the panics are all recovered in middleware.go + localPanicErr := recover() + if localPanicErr != nil { + record.lock.Lock() + record.panicError = localPanicErr + record.lock.Unlock() + } + + // remove from the record map + manager.lock.Lock() + delete(manager.requestRecords, record.index) + manager.lock.Unlock() + + // log the end of the request + manager.print(EndEvent, record) + + if localPanicErr != nil { + // the panic wasn't recovered before us, so we should pass it up, and let the framework handle the panic error + panic(localPanicErr) + } + }() + + req = req.WithContext(context.WithValue(req.Context(), contextKey, record)) + manager.print(StartEvent, record) + next.ServeHTTP(w, req) + }) +} diff --git a/modules/web/routing/requestrecord.go b/modules/web/routing/requestrecord.go new file mode 100644 index 0000000000..5e225f32e4 --- /dev/null +++ b/modules/web/routing/requestrecord.go @@ -0,0 +1,29 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package routing + +import ( + "net/http" + "sync" + "time" +) + +type requestRecord struct { + // index of the record in the records map + index uint64 + + // immutable fields + startTime time.Time + request *http.Request + responseWriter http.ResponseWriter + + // mutex + lock sync.RWMutex + + // mutable fields + isLongPolling bool + funcInfo *FuncInfo + panicError interface{} +} diff --git a/modules/web/wrap.go b/modules/web/wrap.go new file mode 100644 index 0000000000..b55b876069 --- /dev/null +++ b/modules/web/wrap.go @@ -0,0 +1,117 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package web + +import ( + goctx "context" + "net/http" + "strings" + + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/web/routing" +) + +// Wrap converts all kinds of routes to standard library one +func Wrap(handlers ...interface{}) http.HandlerFunc { + if len(handlers) == 0 { + panic("No handlers found") + } + + ourHandlers := make([]wrappedHandlerFunc, 0, len(handlers)) + + for _, handler := range handlers { + ourHandlers = append(ourHandlers, convertHandler(handler)) + } + return wrapInternal(ourHandlers) +} + +func wrapInternal(handlers []wrappedHandlerFunc) http.HandlerFunc { + return func(resp http.ResponseWriter, req *http.Request) { + var defers []func() + defer func() { + for i := len(defers) - 1; i >= 0; i-- { + defers[i]() + } + }() + for i := 0; i < len(handlers); i++ { + handler := handlers[i] + others := handlers[i+1:] + done, deferrable := handler(resp, req, others...) + if deferrable != nil { + defers = append(defers, deferrable) + } + if done { + return + } + } + } +} + +// Middle wrap a context function as a chi middleware +func Middle(f func(ctx *context.Context)) func(next http.Handler) http.Handler { + funcInfo := routing.GetFuncInfo(f) + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + routing.UpdateFuncInfo(req.Context(), funcInfo) + ctx := context.GetContext(req) + f(ctx) + if ctx.Written() { + return + } + next.ServeHTTP(ctx.Resp, ctx.Req) + }) + } +} + +// MiddleCancel wrap a context function as a chi middleware +func MiddleCancel(f func(ctx *context.Context) goctx.CancelFunc) func(netx http.Handler) http.Handler { + funcInfo := routing.GetFuncInfo(f) + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + routing.UpdateFuncInfo(req.Context(), funcInfo) + ctx := context.GetContext(req) + cancel := f(ctx) + if cancel != nil { + defer cancel() + } + if ctx.Written() { + return + } + next.ServeHTTP(ctx.Resp, ctx.Req) + }) + } +} + +// MiddleAPI wrap a context function as a chi middleware +func MiddleAPI(f func(ctx *context.APIContext)) func(next http.Handler) http.Handler { + funcInfo := routing.GetFuncInfo(f) + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + routing.UpdateFuncInfo(req.Context(), funcInfo) + ctx := context.GetAPIContext(req) + f(ctx) + if ctx.Written() { + return + } + next.ServeHTTP(ctx.Resp, ctx.Req) + }) + } +} + +// WrapWithPrefix wraps a provided handler function at a prefix +func WrapWithPrefix(pathPrefix string, handler http.HandlerFunc, friendlyName ...string) func(next http.Handler) http.Handler { + funcInfo := routing.GetFuncInfo(handler, friendlyName...) + + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + if !strings.HasPrefix(req.URL.Path, pathPrefix) { + next.ServeHTTP(resp, req) + return + } + routing.UpdateFuncInfo(req.Context(), funcInfo) + handler(resp, req) + }) + } +} diff --git a/modules/web/wrap_convert.go b/modules/web/wrap_convert.go new file mode 100644 index 0000000000..f2e05de7c1 --- /dev/null +++ b/modules/web/wrap_convert.go @@ -0,0 +1,101 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package web + +import ( + goctx "context" + "fmt" + "net/http" + + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/web/routing" +) + +type wrappedHandlerFunc func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) + +func convertHandler(handler interface{}) wrappedHandlerFunc { + funcInfo := routing.GetFuncInfo(handler) + switch t := handler.(type) { + case http.HandlerFunc: + return func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) { + routing.UpdateFuncInfo(req.Context(), funcInfo) + t(resp, req) + if r, ok := resp.(context.ResponseWriter); ok && r.Status() > 0 { + done = true + } + return + } + case func(http.ResponseWriter, *http.Request): + return func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) { + routing.UpdateFuncInfo(req.Context(), funcInfo) + t(resp, req) + if r, ok := resp.(context.ResponseWriter); ok && r.Status() > 0 { + done = true + } + return + } + + case func(ctx *context.Context): + return func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) { + routing.UpdateFuncInfo(req.Context(), funcInfo) + ctx := context.GetContext(req) + t(ctx) + done = ctx.Written() + return + } + case func(ctx *context.Context) goctx.CancelFunc: + return func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) { + routing.UpdateFuncInfo(req.Context(), funcInfo) + ctx := context.GetContext(req) + deferrable = t(ctx) + done = ctx.Written() + return + } + case func(*context.APIContext): + return func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) { + routing.UpdateFuncInfo(req.Context(), funcInfo) + ctx := context.GetAPIContext(req) + t(ctx) + done = ctx.Written() + return + } + case func(*context.APIContext) goctx.CancelFunc: + return func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) { + routing.UpdateFuncInfo(req.Context(), funcInfo) + ctx := context.GetAPIContext(req) + deferrable = t(ctx) + done = ctx.Written() + return + } + case func(*context.PrivateContext): + return func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) { + routing.UpdateFuncInfo(req.Context(), funcInfo) + ctx := context.GetPrivateContext(req) + t(ctx) + done = ctx.Written() + return + } + case func(*context.PrivateContext) goctx.CancelFunc: + return func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) { + routing.UpdateFuncInfo(req.Context(), funcInfo) + ctx := context.GetPrivateContext(req) + deferrable = t(ctx) + done = ctx.Written() + return + } + case func(http.Handler) http.Handler: + return func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) { + var next = http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}) + if len(others) > 0 { + next = wrapInternal(others) + } + routing.UpdateFuncInfo(req.Context(), funcInfo) + t(next).ServeHTTP(resp, req) + return + } + default: + panic(fmt.Sprintf("Unsupported handler type: %#v", t)) + } +} |