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  }