From d503b3c7311ac5b49d7d01eb590cc2850c5b86a8 Mon Sep 17 00:00:00 2001 From: Matthew Gabeler-Lee Date: Wed, 26 Feb 2025 17:30:40 -0500 Subject: [PATCH] feat: add option to omit query string from path matching and logging --- README.md | 17 +++++++++++++++++ _example/main.go | 17 +++++++++++++++++ logger.go | 25 +++++++++++++++++++------ logger_test.go | 37 +++++++++++++++++++++++++++++++++++++ options.go | 6 ++++++ 5 files changed, 96 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 98dbf9ff..ae974267 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,23 @@ func main() { }) } + // Example of WithPathNoQuery usage + nq := r.Group("/no-query", logger.SetLogger( + logger.WithPathNoQuery(true), + logger.WithPathLevel(map[string]zerolog.Level{ + // normally this wouldn't match /no-query/boring?foo, but WithPathNoQuery(true) makes it match + "/no-query/boring": zerolog.DebugLevel, + }), + )) + { + nq.GET("/boring", func(c *gin.Context) { + c.String(http.StatusOK, "OK") + }) + nq.GET("/interesting", func(c *gin.Context) { + c.String(http.StatusOK, "OK") + }) + } + // Listen and Server in 0.0.0.0:8080 if err := r.Run(":8080"); err != nil { log.Fatal().Msg("can' start server with 8080 port") diff --git a/_example/main.go b/_example/main.go index b9f150f8..f7040c0b 100644 --- a/_example/main.go +++ b/_example/main.go @@ -143,6 +143,23 @@ func main() { }) } + // Example of WithPathNoQuery usage + nq := r.Group("/no-query", logger.SetLogger( + logger.WithPathNoQuery(true), + logger.WithPathLevel(map[string]zerolog.Level{ + // normally this wouldn't match /no-query/boring?foo, but WithPathNoQuery(true) makes it match + "/no-query/boring": zerolog.DebugLevel, + }), + )) + { + nq.GET("/boring", func(c *gin.Context) { + c.String(http.StatusOK, "OK") + }) + nq.GET("/interesting", func(c *gin.Context) { + c.String(http.StatusOK, "OK") + }) + } + // Listen and Server in 0.0.0.0:8080 if err := r.Run(":8080"); err != nil { log.Fatal().Msg("can' start server with 8080 port") diff --git a/logger.go b/logger.go index f8357945..a7186818 100644 --- a/logger.go +++ b/logger.go @@ -47,6 +47,10 @@ type config struct { serverErrorLevel zerolog.Level // pathLevels is a map of specific paths to log levels for requests with status code < 400. pathLevels map[string]zerolog.Level + // pathNoQuery specifies if the path should be handled without the query + // string for both logging and matching against pathLevels. If enabled, the + // query string will be logged in its own field. + pathNoQuery bool } const loggerKey = "_gin-contrib/logger_" @@ -113,8 +117,9 @@ func SetLogger(opts ...Option) gin.HandlerFunc { start := time.Now() path := c.Request.URL.Path - if raw := c.Request.URL.RawQuery; raw != "" { - path += "?" + raw + rawQuery := c.Request.URL.RawQuery + if rawQuery != "" && !cfg.pathNoQuery { + path += "?" + rawQuery } track := true @@ -133,9 +138,13 @@ func SetLogger(opts ...Option) gin.HandlerFunc { contextLogger := rl if track { - contextLogger = rl.With(). + rlc := rl.With(). Str("method", c.Request.Method). - Str("path", path). + Str("path", path) + if cfg.pathNoQuery && rawQuery != "" { + rlc = rlc.Str("query", rawQuery) + } + contextLogger = rlc. Str("ip", c.ClientIP()). Str("user_agent", c.Request.UserAgent()). Logger() @@ -174,10 +183,14 @@ func SetLogger(opts ...Option) gin.HandlerFunc { evt = cfg.context(c, evt) } - evt. + evt = evt. Int("status", c.Writer.Status()). Str("method", c.Request.Method). - Str("path", path). + Str("path", path) + if cfg.pathNoQuery && rawQuery != "" { + evt = evt.Str("query", rawQuery) + } + evt. Str("ip", c.ClientIP()). Dur("latency", latency). Str("user_agent", c.Request.UserAgent()). diff --git a/logger_test.go b/logger_test.go index ff590443..402ba71f 100644 --- a/logger_test.go +++ b/logger_test.go @@ -310,6 +310,43 @@ func TestLoggerSkipper(t *testing.T) { assert.NotContains(t, buffer.String(), "/example2") } +func TestLoggerPathNoQuery(t *testing.T) { + buffer := new(bytes.Buffer) + gin.SetMode(gin.ReleaseMode) + r := gin.New() + r.Use(SetLogger( + WithWriter(buffer), + WithPathNoQuery(true), + WithPathLevel(map[string]zerolog.Level{ + "/example2": zerolog.WarnLevel, + }), + )) + r.GET("/example", func(c *gin.Context) { + l := Get(c) + l.Debug().Msg("contextlogger") + }) + r.GET("/example2", func(c *gin.Context) {}) + + performRequest(r, "GET", "/example?foo=bar") + lines := strings.Split(strings.TrimSpace(buffer.String()), "\n") + assert.Len(t, lines, 2) + for i, line := range lines { + assert.Contains(t, line, " query=foo=bar ", "line %d", i) + } + + // with no query string, the field should be omitted + buffer.Reset() + performRequest(r, "GET", "/example") + assert.NotContains(t, buffer.String(), "query=") + + buffer.Reset() + performRequest(r, "GET", "/example2?foo=bar") + // this one should be logged at warn level because the query-free path matches + // the level map entry + assert.Contains(t, buffer.String(), "WRN") + assert.Contains(t, buffer.String(), " query=foo=bar ") +} + func BenchmarkLogger(b *testing.B) { gin.SetMode(gin.ReleaseMode) r := gin.New() diff --git a/options.go b/options.go index 2c5f3dd9..e37c2398 100644 --- a/options.go +++ b/options.go @@ -157,3 +157,9 @@ func WithContext(fn func(*gin.Context, *zerolog.Event) *zerolog.Event) Option { c.context = fn }) } + +func WithPathNoQuery(b bool) Option { + return optionFunc(func(c *config) { + c.pathNoQuery = b + }) +}