github.com/mendersoftware/go-lib-micro@v0.0.0-20240304135804-e8e39c59b148/accesslog/middleware_gin.go (about) 1 // Copyright 2023 Northern.tech AS 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package accesslog 16 17 import ( 18 "context" 19 "fmt" 20 "net/http" 21 "time" 22 23 "github.com/gin-gonic/gin" 24 "github.com/mendersoftware/go-lib-micro/log" 25 "github.com/mendersoftware/go-lib-micro/rest.utils" 26 "github.com/pkg/errors" 27 "github.com/sirupsen/logrus" 28 ) 29 30 type AccessLogger struct { 31 DisableLog func(c *gin.Context) bool 32 } 33 34 func (a AccessLogger) LogFunc( 35 ctx context.Context, 36 c *gin.Context, 37 startTime time.Time, 38 ) { 39 logCtx := logrus.Fields{ 40 "clientip": c.ClientIP(), 41 "method": c.Request.Method, 42 "path": c.Request.URL.Path, 43 "qs": c.Request.URL.RawQuery, 44 "ts": startTime. 45 Truncate(time.Millisecond). 46 Format(time.RFC3339Nano), 47 "type": c.Request.Proto, 48 "useragent": c.Request.UserAgent(), 49 } 50 lc := fromContext(ctx) 51 if lc != nil { 52 lc.addFields(logCtx) 53 } 54 if r := recover(); r != nil { 55 trace := collectTrace() 56 logCtx["trace"] = trace 57 logCtx["panic"] = r 58 59 func() { 60 // Try to respond with an internal server error. 61 // If the connection is broken it might panic again. 62 defer func() { recover() }() // nolint:errcheck 63 rest.RenderError(c, 64 http.StatusInternalServerError, 65 errors.New("internal error"), 66 ) 67 }() 68 } else if a.DisableLog != nil && a.DisableLog(c) { 69 return 70 } 71 latency := time.Since(startTime) 72 // We do not need more than 3 digit fraction 73 if latency > time.Second { 74 latency = latency.Round(time.Millisecond) 75 } else if latency > time.Millisecond { 76 latency = latency.Round(time.Microsecond) 77 } 78 code := c.Writer.Status() 79 select { 80 case <-ctx.Done(): 81 if errors.Is(ctx.Err(), context.Canceled) { 82 code = StatusClientClosedConnection 83 } 84 default: 85 } 86 logCtx["responsetime"] = latency.String() 87 logCtx["status"] = c.Writer.Status() 88 logCtx["byteswritten"] = c.Writer.Size() 89 90 var logLevel logrus.Level = logrus.InfoLevel 91 if code >= 500 { 92 logLevel = logrus.ErrorLevel 93 } else if code >= 400 { 94 logLevel = logrus.WarnLevel 95 } 96 if len(c.Errors) > 0 { 97 errs := c.Errors.Errors() 98 var errMsg string 99 if len(errs) == 1 { 100 errMsg = errs[0] 101 } else { 102 for i, err := range errs { 103 errMsg = errMsg + fmt.Sprintf( 104 "#%02d: %s\n", i+1, err, 105 ) 106 } 107 } 108 logCtx["error"] = errMsg 109 } 110 log.FromContext(c.Request.Context()). 111 WithFields(logCtx). 112 Log(logLevel) 113 } 114 115 func (a AccessLogger) Middleware(c *gin.Context) { 116 ctx := c.Request.Context() 117 startTime := time.Now() 118 ctx = withContext(ctx, &logContext{maxErrors: DefaultMaxErrors}) 119 c.Request = c.Request.WithContext(ctx) 120 defer a.LogFunc(ctx, c, startTime) 121 c.Next() 122 } 123 124 // Middleware provides accesslog middleware for the gin-gonic framework. 125 // This middleware will recover any panic from occurring in the API 126 // handler and log it to error level with panic and trace showing the panic 127 // message and traceback respectively. 128 // If an error status is returned in the response, the middleware tries 129 // to pop the topmost error from the gin.Context (c.Error) and puts it in 130 // the "error" context to the final log entry. 131 func Middleware() gin.HandlerFunc { 132 return AccessLogger{}.Middleware 133 }