logger.go (3501B)
1 // GoToSocial 2 // Copyright (C) GoToSocial Authors admin@gotosocial.org 3 // SPDX-License-Identifier: AGPL-3.0-or-later 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package middleware 19 20 import ( 21 "fmt" 22 "net/http" 23 "time" 24 25 "codeberg.org/gruf/go-bytesize" 26 "codeberg.org/gruf/go-errors/v2" 27 "codeberg.org/gruf/go-kv" 28 "codeberg.org/gruf/go-logger/v2/level" 29 "github.com/gin-gonic/gin" 30 "github.com/superseriousbusiness/gotosocial/internal/gtserror" 31 "github.com/superseriousbusiness/gotosocial/internal/log" 32 ) 33 34 // Logger returns a gin middleware which provides request logging and panic recovery. 35 func Logger(logClientIP bool) gin.HandlerFunc { 36 return func(c *gin.Context) { 37 // Initialize the logging fields 38 fields := make(kv.Fields, 5, 7) 39 40 // Determine pre-handler time 41 before := time.Now() 42 43 // defer so that we log *after the request has completed* 44 defer func() { 45 code := c.Writer.Status() 46 path := c.Request.URL.Path 47 48 if r := recover(); r != nil { 49 if c.Writer.Status() == 0 { 50 // No response was written, send a generic Internal Error 51 c.Writer.WriteHeader(http.StatusInternalServerError) 52 } 53 54 // Append panic information to the request ctx 55 err := fmt.Errorf("recovered panic: %v", r) 56 _ = c.Error(err) 57 58 // Dump a stacktrace to error log 59 callers := errors.GetCallers(3, 10) 60 log.WithContext(c.Request.Context()). 61 WithField("stacktrace", callers).Error(err) 62 } 63 64 // NOTE: 65 // It is very important here that we are ONLY logging 66 // the request path, and none of the query parameters. 67 // Query parameters can contain sensitive information 68 // and could lead to storing plaintext API keys in logs 69 70 // Set request logging fields 71 fields[0] = kv.Field{"latency", time.Since(before)} 72 fields[1] = kv.Field{"userAgent", c.Request.UserAgent()} 73 fields[2] = kv.Field{"method", c.Request.Method} 74 fields[3] = kv.Field{"statusCode", code} 75 fields[4] = kv.Field{"path", path} 76 if logClientIP { 77 fields = append(fields, kv.Field{ 78 "clientIP", c.ClientIP(), 79 }) 80 } 81 82 // Create log entry with fields 83 l := log.WithContext(c.Request.Context()). 84 WithFields(fields...) 85 86 // Default is info 87 lvl := level.INFO 88 89 if code >= 500 { 90 // This is a server error 91 lvl = level.ERROR 92 l = l.WithField("error", c.Errors) 93 } 94 95 // Get appropriate text for this code. 96 statusText := http.StatusText(code) 97 if statusText == "" { 98 // Look for custom codes. 99 switch code { 100 case gtserror.StatusClientClosedRequest: 101 statusText = gtserror.StatusTextClientClosedRequest 102 default: 103 statusText = "Unknown Status" 104 } 105 } 106 107 // Generate a nicer looking bytecount 108 size := bytesize.Size(c.Writer.Size()) 109 110 // Finally, write log entry with status text + body size. 111 l.Logf(lvl, "%s: wrote %s", statusText, size) 112 }() 113 114 // Process request 115 c.Next() 116 } 117 }