as suggested by @PRATHEESH PC, I put together a small example based on your needs. The code is divided up into three parts: the middleware, the HTTP handler, and the main package. Let's start with the HTTP handler.
handlers/handlers.go
file
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
)
type message struct {
Name string `json:"name" binding:"required"`
}
func Ping(c *gin.Context) {
var message message
if err := c.ShouldBindBodyWith(&message, binding.JSON); err != nil {
c.JSON(http.StatusBadRequest, err.Error())
return
}
c.JSON(http.StatusOK, message)
}
The only thing to mention here is the usage of the ShouldBindBodyWith
method that gives you the possibility to read the request payload multiple times. In fact, the first time it has been called (within the middleware), it copies the request body into the context. The subsequent calls will read the body from there (e.g. the call in the HTTP handler).
middlewares/middlewares.go
file
package middlewares
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/sirupsen/logrus"
log "github.com/sirupsen/logrus"
)
type ginBodyLogger struct {
// get all the methods implementation from the original one
// override only the Write() method
gin.ResponseWriter
body bytes.Buffer
}
func (g *ginBodyLogger) Write(b []byte) (int, error) {
g.body.Write(b)
return g.ResponseWriter.Write(b)
}
func RequestLoggingMiddleware(logger *logrus.Logger) gin.HandlerFunc {
return func(ctx *gin.Context) {
ginBodyLogger := &ginBodyLogger{
body: bytes.Buffer{},
ResponseWriter: ctx.Writer,
}
ctx.Writer = ginBodyLogger
var req interface{}
if err := ctx.ShouldBindBodyWith(&req, binding.JSON); err != nil {
ctx.JSON(http.StatusBadRequest, err.Error())
return
}
data, err := json.Marshal(req)
if err != nil {
panic(fmt.Errorf("err while marshaling req msg: %v", err))
}
ctx.Next()
logger.WithFields(log.Fields{
"status": ctx.Writer.Status(),
"method": ctx.Request.Method,
"path": ctx.Request.URL.Path,
"query_params": ctx.Request.URL.Query(),
"req_body": string(data),
"res_body": ginBodyLogger.body.String(),
}).Info("request details")
}
}
Here, we did three main things.
First, we defined the ginBodyLogger
struct that embeds the gin.ResponseWriter
struct and adds a bytes.Buffer
to record the response payload we care about.
Then, we provided a custom implementation for the method Write
that will be called when writing to the response stream. Before writing to it, we'll save the information in the buffer that belongs to the ginBodyLogger
struct.
Finally, we trace this information through the logger
we provided to the middleware function.
main.go
file
package main
import (
"ginlogger/handlers"
"ginlogger/middlewares"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
log "github.com/sirupsen/logrus"
)
var logger *log.Logger
func init() {
logger = logrus.New()
logger.SetLevel(log.InfoLevel)
}
func main() {
gin.SetMode(gin.DebugMode)
r := gin.Default()
r.Use(middlewares.RequestLoggingMiddleware(logger))
r.Use(gin.LoggerWithWriter(logger.Writer()))
r.POST("/ping", handlers.Ping)
r.Run(":8000")
}
The main
package is in charge of initializing everything we need in our program. It involves two functions: init
and main
.
Within the init
function, we initialize the logger.
Within the main
function, we initialize the gin.Engine
instance, instrument it, and run.
If you run the code, you should have the logs as desired. If that's not the case, just let me know and I'll come back to you, thanks!