Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
dmartinol committed Aug 2, 2023
1 parent a5816c2 commit bf120ff
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 9 deletions.
60 changes: 51 additions & 9 deletions logger.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package logger

import (
"bytes"
"io"
"net/http"
"os"
"regexp"
"strings"
"time"

"github.com/gin-gonic/gin"
Expand All @@ -30,17 +32,36 @@ type config struct {
clientErrorLevel zerolog.Level
// the log level used for request with status code >= 500
serverErrorLevel zerolog.Level
// whether to log response body for request with status code >= 400
logErrorResponseBody bool
// whether to log response body for request with status code < 400
logResponseBody bool
// max len of response body message (whatever the status code)
maxResponseBodyLen int
}

type bodyLogWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}

func (w bodyLogWriter) Write(b []byte) (int, error) {
w.body.Write(b)
return w.ResponseWriter.Write(b)
}

var isTerm bool = isatty.IsTerminal(os.Stdout.Fd())

// SetLogger initializes the logging middleware.
func SetLogger(opts ...Option) gin.HandlerFunc {
cfg := &config{
defaultLevel: zerolog.InfoLevel,
clientErrorLevel: zerolog.WarnLevel,
serverErrorLevel: zerolog.ErrorLevel,
output: gin.DefaultWriter,
defaultLevel: zerolog.InfoLevel,
clientErrorLevel: zerolog.WarnLevel,
serverErrorLevel: zerolog.ErrorLevel,
output: gin.DefaultWriter,
logErrorResponseBody: false,
logResponseBody: false,
maxResponseBodyLen: 50,
}

// Loop through each option
Expand Down Expand Up @@ -80,6 +101,11 @@ func SetLogger(opts ...Option) gin.HandlerFunc {
path = path + "?" + raw
}

var blw *bodyLogWriter
if cfg.logErrorResponseBody || cfg.logResponseBody {
blw = &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
c.Writer = blw
}
c.Next()
track := true

Expand All @@ -100,10 +126,26 @@ func SetLogger(opts ...Option) gin.HandlerFunc {
}
latency := end.Sub(start)

l = l.With().
Int("status", c.Writer.Status()).
statusCode := c.Writer.Status()
var response string
withResponse := (cfg.logErrorResponseBody && statusCode >= 400) || (cfg.logResponseBody && statusCode < 400)
if withResponse && blw.body != nil {
response = blw.body.String()
response = strings.TrimPrefix(response, "\"")
response = strings.TrimSuffix(response, "\"")
if len(response) > cfg.maxResponseBodyLen {
response = response[:cfg.maxResponseBodyLen] + "..."
}
}

ctx := l.With().
Int("status", statusCode).
Str("method", c.Request.Method).
Str("path", c.Request.URL.Path).
Str("path", c.Request.URL.Path)
if withResponse {
ctx = ctx.Logger().With().Str("response", response)
}
l = ctx.Logger().With().
Str("ip", c.ClientIP()).
Dur("latency", latency).
Str("user_agent", c.Request.UserAgent()).Logger()
Expand All @@ -114,12 +156,12 @@ func SetLogger(opts ...Option) gin.HandlerFunc {
}

switch {
case c.Writer.Status() >= http.StatusBadRequest && c.Writer.Status() < http.StatusInternalServerError:
case statusCode >= http.StatusBadRequest && statusCode < http.StatusInternalServerError:
{
l.WithLevel(cfg.clientErrorLevel).
Msg(msg)
}
case c.Writer.Status() >= http.StatusInternalServerError:
case statusCode >= http.StatusInternalServerError:
{
l.WithLevel(cfg.serverErrorLevel).
Msg(msg)
Expand Down
64 changes: 64 additions & 0 deletions logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"net/http"
"net/http/httptest"
"regexp"
"strings"
"testing"

"github.com/gin-gonic/gin"
Expand Down Expand Up @@ -183,6 +184,69 @@ func TestLoggerParseLevel(t *testing.T) {
}
}

func TestLoggerWithErrorResponse(t *testing.T) {
buffer := new(bytes.Buffer)
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.Use(SetLogger(WithWriter(buffer), WithLogErrorResponseBody(true)))
r.GET("/example", func(c *gin.Context) {})
r.POST("/example", func(c *gin.Context) {
c.String(http.StatusBadRequest, "bad status")
})

performRequest(r, "GET", "/example?a=100")
assert.NotContains(t, buffer.String(), "response= ")

buffer.Reset()
performRequest(r, "POST", "/example?a=100")
assert.Contains(t, buffer.String(), "response=")
assert.Contains(t, buffer.String(), "\"bad status\"")
}

func TestLoggerWithResponse(t *testing.T) {
buffer := new(bytes.Buffer)
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.Use(SetLogger(WithWriter(buffer), WithLogResponseBody(true)))
r.GET("/example", func(c *gin.Context) {})
r.POST("/example", func(c *gin.Context) {
c.String(http.StatusOK, "example response")
})

performRequest(r, "GET", "/example?a=100")
assert.Contains(t, buffer.String(), "response=")

buffer.Reset()
performRequest(r, "POST", "/example?a=100")
assert.Contains(t, buffer.String(), "response=")
assert.Contains(t, buffer.String(), "\"example response\"")
}

func TestLoggerWithTruncatedResponse(t *testing.T) {
longMessage := strings.Repeat("X", 20)
truncatedMessage := strings.Repeat("X", 10) + "..."
buffer := new(bytes.Buffer)
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.Use(SetLogger(WithWriter(buffer), WithLogErrorResponseBody(true), WithLogResponseBody(true), WithMaxResponseBodyLen(10)))
r.GET("/example", func(c *gin.Context) {
c.String(http.StatusBadRequest, longMessage)
})
r.POST("/example", func(c *gin.Context) {
// c.String(http.StatusOK, strings.Repeat("X", 20))
c.String(http.StatusOK, longMessage)
})

performRequest(r, "GET", "/example?a=100")
assert.Contains(t, buffer.String(), "response=")
assert.Contains(t, buffer.String(), truncatedMessage)

buffer.Reset()
performRequest(r, "POST", "/example?a=100")
assert.Contains(t, buffer.String(), "response=")
assert.Contains(t, buffer.String(), truncatedMessage)
}

func BenchmarkLogger(b *testing.B) {
gin.SetMode(gin.ReleaseMode)
r := gin.New()
Expand Down
20 changes: 20 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,23 @@ func WithServerErrorLevel(lvl zerolog.Level) Option {
c.serverErrorLevel = lvl
})
}

func WithLogErrorResponseBody(logErrorResponseBody bool) Option {
return optionFunc(func(c *config) {
c.logErrorResponseBody = logErrorResponseBody
})
}

func WithLogResponseBody(logResponseBody bool) Option {
return optionFunc(func(c *config) {
c.logResponseBody = logResponseBody
})
}

func WithMaxResponseBodyLen(maxResponseBodyLen int) Option {
return optionFunc(func(c *config) {
if maxResponseBodyLen > 0 {
c.maxResponseBodyLen = maxResponseBodyLen
}
})
}

0 comments on commit bf120ff

Please sign in to comment.