diff --git a/README.md b/README.md index 9c5907f..ac4b243 100644 --- a/README.md +++ b/README.md @@ -82,15 +82,25 @@ router.Run(":1234") // time=2023-04-10T14:00:0.000000Z level=INFO msg="Incoming request" status=200 method=GET path=/pong route=/pong ip=127.0.0.1 latency=25.5µs user-agent=curl/7.77.0 time=2023-04-10T14:00:00.000Z ``` -### Filters +### Verbose ```go -import ( - "github.com/gin-gonic/gin" - sloggin "github.com/samber/slog-gin" - "log/slog" -) +logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + +config := sloggin.Config{ + WithRequestBody: true, + WithResponseBody: true, + WithRequestHeader: true, + WithResponseHeader: true, +} + +router := chi.NewRouter() +router.Use(sloggin.NewWithConfig(logger, config)) +``` + +### Filters +```go logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) router := gin.New() @@ -164,14 +174,6 @@ router.Run(":1234") ### Using custom logger sub-group ```go -import ( - "github.com/gin-gonic/gin" - sloggin "github.com/samber/slog-gin" - "log/slog" -) - -// Create a slog logger, which: -// - Logs to stdout. logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) router := gin.New() @@ -194,14 +196,6 @@ router.Run(":1234") ### Add logger to a single route ```go -import ( - "github.com/gin-gonic/gin" - sloggin "github.com/samber/slog-gin" - "log/slog" -) - -// Create a slog logger, which: -// - Logs to stdout. logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) router := gin.New() @@ -221,14 +215,6 @@ router.Run(":1234") ### Adding custom attributes ```go -import ( - "github.com/gin-gonic/gin" - sloggin "github.com/samber/slog-gin" - "log/slog" -) - -// Create a slog logger, which: -// - Logs to stdout. logger := slog.New(slog.NewTextHandler(os.Stdout, nil)). With("environment", "production"). With("server", "gin/1.9.0"). @@ -243,26 +229,20 @@ router.Use(sloggin.New(logger)) // Example pong request. router.GET("/pong", func(c *gin.Context) { + // Add an attribute to a single log entry. + sloggin.AddCustomAttributes(c, slog.String("foo", "bar")) c.String(http.StatusOK, "pong") }) router.Run(":1234") // output: -// time=2023-04-10T14:00:0.000000+02:00 level=INFO msg="Incoming request" environment=production server=gin/1.9.0 gin_mode=release server_start_time=2023-04-10T10:00:00.000+02:00 status=200 method=GET path=/pong route=/pong ip=127.0.0.1 latency=25.5µs user-agent=curl/7.77.0 time=2023-04-10T14:00:00.000+02:00 +// time=2023-04-10T14:00:0.000000+02:00 level=INFO msg="Incoming request" environment=production server=gin/1.9.0 gin_mode=release server_start_time=2023-04-10T10:00:00.000+02:00 status=200 method=GET path=/pong route=/pong ip=127.0.0.1 latency=25.5µs user-agent=curl/7.77.0 time=2023-04-10T14:00:00.000+02:00 foo=bar ``` ### JSON output ```go -import ( - "github.com/gin-gonic/gin" - sloggin "github.com/samber/slog-gin" - "log/slog" -) - -// Create a slog logger, which: -// - Logs to stdout. logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) router := gin.New() diff --git a/dump.go b/dump.go new file mode 100644 index 0000000..ff36c56 --- /dev/null +++ b/dump.go @@ -0,0 +1,25 @@ +package sloggin + +import ( + "bytes" + + "github.com/gin-gonic/gin" +) + +type bodyWriter struct { + gin.ResponseWriter + body *bytes.Buffer +} + +// implements gin.ResponseWriter +func (w bodyWriter) Write(b []byte) (int, error) { + w.body.Write(b) + return w.ResponseWriter.Write(b) +} + +func newBodyWriter(writer gin.ResponseWriter) *bodyWriter { + return &bodyWriter{ + body: bytes.NewBufferString(""), + ResponseWriter: writer, + } +} diff --git a/examples/example.go b/example/example.go similarity index 75% rename from examples/example.go rename to example/example.go index 2aecc15..5f2724f 100644 --- a/examples/example.go +++ b/example/example.go @@ -21,7 +21,7 @@ func main() { slogformatter.TimezoneConverter(time.UTC), slogformatter.TimeFormatter(time.RFC3339, nil), )( - slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{}), + slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{}), ), ) @@ -33,9 +33,12 @@ func main() { // Add the sloggin middleware to all routes. // The middleware will log all requests attributes under a "http" group. router.Use(sloggin.New(logger)) + // config := sloggin.Config{WithRequestBody: true, WithResponseBody: true, WithRequestHeader: true, WithResponseHeader: true} + // router.Use(sloggin.NewWithConfig(logger, config)) // Example pong request. router.GET("/pong", func(c *gin.Context) { + sloggin.AddCustomAttributes(c, slog.String("foo", "bar")) c.String(http.StatusOK, "pong") }) router.GET("/pong/:id", func(c *gin.Context) { @@ -43,8 +46,8 @@ func main() { }) logger.Info("Starting server") - if err := router.Run(":1234"); err != nil { - logger.Error("can' start server with 1234 port") + if err := router.Run(":4242"); err != nil { + logger.Error("can' start server with 4242 port") } // output: diff --git a/middleware.go b/middleware.go index c775df1..5543a40 100644 --- a/middleware.go +++ b/middleware.go @@ -1,8 +1,11 @@ package sloggin import ( + "bytes" "context" + "io" "net/http" + "strings" "time" "log/slog" @@ -11,14 +14,35 @@ import ( "github.com/google/uuid" ) -const requestIDCtx = "slog-gin.request-id" +const ( + customAttributesCtxKey = "slog-gin.custom-attributes" + requestIDCtx = "slog-gin.request-id" +) + +var ( + HiddenRequestHeaders = map[string]struct{}{ + "authorization": {}, + "cookie": {}, + "set-cookie": {}, + "x-auth-token": {}, + "x-csrf-token": {}, + "x-xsrf-token": {}, + } + HiddenResponseHeaders = map[string]struct{}{ + "set-cookie": {}, + } +) type Config struct { DefaultLevel slog.Level ClientErrorLevel slog.Level ServerErrorLevel slog.Level - WithRequestID bool + WithRequestID bool + WithRequestBody bool + WithRequestHeader bool + WithResponseBody bool + WithResponseHeader bool Filters []Filter } @@ -33,7 +57,11 @@ func New(logger *slog.Logger) gin.HandlerFunc { ClientErrorLevel: slog.LevelWarn, ServerErrorLevel: slog.LevelError, - WithRequestID: true, + WithRequestID: true, + WithRequestBody: false, + WithRequestHeader: false, + WithResponseBody: false, + WithResponseHeader: false, Filters: []Filter{}, }) @@ -49,7 +77,11 @@ func NewWithFilters(logger *slog.Logger, filters ...Filter) gin.HandlerFunc { ClientErrorLevel: slog.LevelWarn, ServerErrorLevel: slog.LevelError, - WithRequestID: true, + WithRequestID: true, + WithRequestBody: false, + WithRequestHeader: false, + WithResponseBody: false, + WithResponseHeader: false, Filters: filters, }) @@ -67,6 +99,21 @@ func NewWithConfig(logger *slog.Logger, config Config) gin.HandlerFunc { c.Header("X-Request-ID", requestID) } + // dump request body + var reqBody []byte + if config.WithRequestBody { + buf, err := io.ReadAll(c.Request.Body) + if err == nil { + c.Request.Body = io.NopCloser(bytes.NewBuffer(buf)) + reqBody = buf + } + } + + // dump response body + if config.WithResponseBody { + c.Writer = newBodyWriter(c.Writer) + } + c.Next() end := time.Now() @@ -87,6 +134,42 @@ func NewWithConfig(logger *slog.Logger, config Config) gin.HandlerFunc { attributes = append(attributes, slog.String("request-id", requestID)) } + // request + if config.WithRequestBody { + attributes = append(attributes, slog.Group("request", slog.String("body", string(reqBody)))) + } + if config.WithRequestHeader { + for k, v := range c.Request.Header { + if _, found := HiddenRequestHeaders[strings.ToLower(k)]; found { + continue + } + attributes = append(attributes, slog.Group("request", slog.Group("header", slog.Any(k, v)))) + } + } + + // response + if config.WithResponseBody { + if w, ok := c.Writer.(*bodyWriter); ok { + attributes = append(attributes, slog.Group("response", slog.String("body", w.body.String()))) + } + } + if config.WithResponseHeader { + for k, v := range c.Writer.Header() { + if _, found := HiddenResponseHeaders[strings.ToLower(k)]; found { + continue + } + attributes = append(attributes, slog.Group("response", slog.Group("header", slog.Any(k, v)))) + } + } + + // custom context values + if v, ok := c.Get(customAttributesCtxKey); ok { + switch attrs := v.(type) { + case []slog.Attr: + attributes = append(attributes, attrs...) + } + } + for _, filter := range config.Filters { if !filter(c) { return @@ -117,3 +200,16 @@ func GetRequestID(c *gin.Context) string { return "" } + +func AddCustomAttributes(c *gin.Context, attr slog.Attr) { + v, exists := c.Get(customAttributesCtxKey) + if !exists { + c.Set(customAttributesCtxKey, []slog.Attr{attr}) + return + } + + switch attrs := v.(type) { + case []slog.Attr: + c.Set(customAttributesCtxKey, append(attrs, attr)) + } +}