diff options
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)) + } +} |