feat: verbose mode + custom attributes

This commit is contained in:
Samuel Berthe
2023-10-16 01:00:56 +02:00
parent 1546b4083a
commit 2fa3d7e62c
4 changed files with 150 additions and 46 deletions
+19 -39
View File
@@ -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
```
### Verbose
```go
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
import (
"github.com/gin-gonic/gin"
sloggin "github.com/samber/slog-gin"
"log/slog"
)
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()
+25
View File
@@ -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,
}
}
+6 -3
View File
@@ -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:
+100 -4
View File
@@ -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))
}
}