- With issues:
- custom logger
Description
What I would expect is to log the body data of the request if the method of the request is not GET. However, in the repository that handles the contents of config, it works properly. In the place where the repository is imported, the request body is output as empty.
How to reproduce
Origin Repository
package footprint
import (
"fmt"
"github.com/gin-gonic/gin"
satori "github.com/satori/go.uuid"
"io/ioutil"
"net/http"
"time"
)
const (
defaultLogFormat = "%s %-7s %s %s %3d %s [remote ip: \"%15s\", proto: \"%s\", agent: \"%s\"] -> \"%s\"(request id: %s) %v %13v error: \"%s\"\n"
defaultLogFormatWithReqBody = "%s %-7s %s %s %3d %s [remote ip: \"%15s\", proto: \"%s\", agent: \"%s\"] -> \"%s\"(request id: %s) %v %13v request: \"%s\" error: \"%s\"\n"
)
func NewLoggerConfig(formatter func(gin.LogFormatterParams) string, skipPaths []string) gin.LoggerConfig {
return gin.LoggerConfig{
Formatter: formatter,
SkipPaths: skipPaths,
}
}
func DefaultLoggerFormatter(param gin.LogFormatterParams) string {
requestID := generateRequestID()
param.Request.Header.Set("X-Request-Id", requestID)
if param.Method != http.MethodGet {
reqBody, _ := ioutil.ReadAll(param.Request.Body)
return loggingWithReqBodyLog(param, string(reqBody))
}
return loggingDefaultLog(param)
}
func loggingWithReqBodyLog(param gin.LogFormatterParams, reqBody string) string {
var statusColor, methodColor, resetColor string
if param.IsOutputColor() {
statusColor = param.StatusCodeColor()
methodColor = param.MethodColor()
resetColor = param.ResetColor()
}
return fmt.Sprintf(defaultLogFormatWithReqBody,
methodColor, param.Method, resetColor,
statusColor, param.StatusCode, resetColor,
param.ClientIP,
param.Request.Proto,
param.Request.UserAgent(),
param.Path,
param.Request.Header.Get("X-Request-Id"),
param.TimeStamp.Format(time.RFC3339),
param.Latency,
reqBody,
param.ErrorMessage,
)
}
Repository to import and use
func setUpRouter() *gin.Engine {
config := newCorsConfig()
r := gin.New()
r.Use(config, useLogger(footprint.DefaultLoggerFormatter), gin.Recovery())
// health checker
r.GET("/")
registerRoute(r)
return r
}
func useLogger(formatter func(params gin.LogFormatterParams) string) gin.HandlerFunc {
loggerConfig := footprint.NewLoggerConfig(formatter, skipPaths)
return gin.LoggerWithConfig(loggerConfig)
}
## Expectations
POST 400 [remote ip: " ", proto: "HTTP/1.1", agent: ""] -> "/"(request id: e3b81155-d137-45f2-9f94-a7d8ab318de8) 2020-06-23T10:34:15+09:00 16.022µs request: "{"id":1,"name":"test","created_at":"2020-06-23T10:34:15.179062+09:00"}" error: ""
## Actual result
POST 400 [remote ip: " ", proto: "HTTP/1.1", agent: ""] -> "/"(request id: 982ad35f-991b-4591-932a-d770dc5adb52) 2020-06-23T10:30:58+09:00 6.891µs request: "" error: ""
Environment
- go version: 1.12
- gin version (or commit ref): 1.6.3
- operating system: MacOS Catalina(10.15.5)
Comment From: seblegall
Hi, I've just tested it and it seems to work well.
Here is the source code I used :
main.go
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/seblegall/gist/gin-custom-log/footprint"
)
func main() {
r := gin.New()
r.Use(useLogger(footprint.DefaultLoggerFormatter), gin.Recovery())
r.GET("/")
r.POST("/")
r.Run(fmt.Sprintf(":%d", 8080))
}
func useLogger(formatter func(params gin.LogFormatterParams) string) gin.HandlerFunc {
loggerConfig := footprint.NewLoggerConfig(formatter, []string{})
return gin.LoggerWithConfig(loggerConfig)
}
footprint/footprint.go
package footprint
import (
"fmt"
"io/ioutil"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
const (
defaultLogFormat = "%s %-7s %s %s %3d %s [remote ip: \"%15s\", proto: \"%s\", agent: \"%s\"] -> \"%s\"(request id: %s) %v %13v error: \"%s\"\n"
defaultLogFormatWithReqBody = "%s %-7s %s %s %3d %s [remote ip: \"%15s\", proto: \"%s\", agent: \"%s\"] -> \"%s\"(request id: %s) %v %13v request: \"%s\" error: \"%s\"\n"
)
func NewLoggerConfig(formatter func(gin.LogFormatterParams) string, skipPaths []string) gin.LoggerConfig {
return gin.LoggerConfig{
Formatter: formatter,
SkipPaths: skipPaths,
}
}
func DefaultLoggerFormatter(param gin.LogFormatterParams) string {
if param.Method != http.MethodGet {
reqBody, _ := ioutil.ReadAll(param.Request.Body)
return loggingWithReqBodyLog(param, string(reqBody))
}
return fmt.Sprintf("Default loging")
}
func loggingWithReqBodyLog(param gin.LogFormatterParams, reqBody string) string {
var statusColor, methodColor, resetColor string
if param.IsOutputColor() {
statusColor = param.StatusCodeColor()
methodColor = param.MethodColor()
resetColor = param.ResetColor()
}
return fmt.Sprintf(defaultLogFormatWithReqBody,
methodColor, param.Method, resetColor,
statusColor, param.StatusCode, resetColor,
param.ClientIP,
param.Request.Proto,
param.Request.UserAgent(),
param.Path,
param.Request.Header.Get("X-Request-Id"),
param.TimeStamp.Format(time.RFC3339),
param.Latency,
reqBody,
param.ErrorMessage,
)
}
When I call my server, here is what I get :
POST 200 [remote ip: " ::1", proto: "HTTP/1.1", agent: "PostmanRuntime/7.20.1"] -> "/"(request id: ) 2020-06-23T15:58:18+02:00 764ns request: "{
"status_code": 200,
"body" : {
"field": "test"
}
}" error: ""
Comment From: Kyocfe
@seblegall I don't know what's wrong. When I run it with test code in the footprint repository, I am logged as well. but, if it is imported and used, logging, whether using Postman or using test code, cannot be performed properly. so confused..
POST 201 [remote ip: " ", proto: "HTTP/1.1", agent: ""] -> "/schedule"(request id: 3bb4f943-4d5a-47c8-9a82-c34fd0e2961a) 2020-06-25T09:22:04+09:00 60.171µs request: "" error: ""
Comment From: granty1
@seblegall I don't know what's wrong. When I run it with test code in the footprint repository, I am logged as well. but, if it is imported and used, logging, whether using Postman or using test code, cannot be performed properly. so confused..
POST 201 [remote ip: " ", proto: "HTTP/1.1", agent: ""] -> "/schedule"(request id: 3bb4f943-4d5a-47c8-9a82-c34fd0e2961a) 2020-06-25T09:22:04+09:00 60.171µs request: "" error: ""
You should read the request body in your handle func body. When printing in the log, the response has been written, but the above example does not ?
Comment From: Kyocfe
@seblegall I don't know what's wrong. When I run it with test code in the footprint repository, I am logged as well. but, if it is imported and used, logging, whether using Postman or using test code, cannot be performed properly. so confused..
POST 201 [remote ip: " ", proto: "HTTP/1.1", agent: ""] -> "/schedule"(request id: 3bb4f943-4d5a-47c8-9a82-c34fd0e2961a) 2020-06-25T09:22:04+09:00 60.171µs request: "" error: ""
You should read the request body in your handle func body. When printing in the log, the response has been written, but the above example does not ?
@granty1 I'm sorry, i can't understand what are you talking about that. the gin.LogFormatterParams has request body parameters. doesn't that mean I should be able to use it? can you give me an example with code because my English is not good.
gin.HandlerFunc is already use it.
func main() {
r.Use(config, useLogger(footprint.DefaultLoggerFormatter), gin.Recovery())
}
func useLogger(formatter func(params gin.LogFormatterParams) string) gin.HandlerFunc {
loggerConfig := footprint.NewLoggerConfig(formatter, skipPaths)
return gin.LoggerWithConfig(loggerConfig)
}
Comment From: Kyocfe
@seblegall @granty1 Thank you for the answers :) the problem was solved. this is my solution.
func loggingWithReqBodyLog(param gin.LogFormatterParams) string {
var statusColor, methodColor, resetColor string
if param.IsOutputColor() {
statusColor = param.StatusCodeColor()
methodColor = param.MethodColor()
resetColor = param.ResetColor()
}
body, _ := param.Request.GetBody()
reqBody, _ := ioutil.ReadAll(body)
return fmt.Sprintf(defaultLogFormatWithReqBody,
methodColor, param.Method, resetColor,
statusColor, param.StatusCode, resetColor,
param.ClientIP,
param.Request.Proto,
param.Request.UserAgent(),
param.Path,
param.Request.Header.Get("X-Request-Id"),
param.TimeStamp.Format(time.RFC3339),
param.Latency,
string(reqBody),
param.ErrorMessage,
)
}
As a result of checking with the debugger, when I tried to read param.Request.Body, the data disappeared. I don't know exactly why, but when I get it through the function GetBody(), there is no problem.
Comment From: Kyocfe
go test -v is fine
body, err := param.Request.GetBody()
but, request GetBody() nil pointer error occurred
Comment From: Kyocfe
func main() {
r := gin.New()
loggerConfig := footprint.NewLoggerConfig(footprint.DefaultLoggerFormatter, nil)
r.Use(corsConfig, gin.LoggerWithConfig(loggerConfig), gin.Recovery())
r.POST("", CreateGroup())
}
If the router group is not used, the log does work. but, if a router group is used, a blank value is output.
Comment From: Kyocfe
Success case
FootPrint Repo
main.go
package main
func main() {
router = setUpRouter()
router.Run(":8080")
}
func setUpRouter() *gin.Engine {
r := gin.New()
r.Use(useLogger(footprint.DefaultLoggerFormatter))
r.Use(gin.Recovery())
r.GET("")
r.POST("", postTest())
r.PUT("")
r.DELETE("")
group := r.Group("group")
groupRouter(group)
return r
}
func groupRouter(group *gin.RouterGroup) {
group.POST("")
}
func useLogger(formatter func(params gin.LogFormatterParams) string) gin.HandlerFunc {
loggerConfig := footprint.NewLoggerConfig(formatter, skipPaths)
return gin.LoggerWithConfig(loggerConfig)
}
footprint/logger.go
const (
defaultLogFormat = "%s %-7s %s %s %3d %s [remote ip: \"%15s\", proto: \"%s\", agent: \"%s\"] -> \"%s\"(request id: %s) %v %13v error: \"%s\"\n"
defaultLogFormatWithReqBody = "%s %-7s %s %s %3d %s [remote ip: \"%15s\", proto: \"%s\", agent: \"%s\"] -> \"%s\"(request id: %s) %v %13v request: \"%s\" error: \"%s\"\n"
)
func NewLoggerConfig(formatter func(gin.LogFormatterParams) string, skipPaths []string) gin.LoggerConfig {
return gin.LoggerConfig{
Formatter: formatter,
SkipPaths: skipPaths,
}
}
func DefaultLoggerFormatter(param gin.LogFormatterParams) string {
requestID := generateRequestID()
param.Request.Header.Set("X-Request-Id", requestID)
if param.Method != http.MethodGet {
return loggingWithReqBodyLog(param)
}
return loggingDefaultLog(param)
}
func loggingWithReqBodyLog(param gin.LogFormatterParams) string {
var statusColor, methodColor, resetColor string
if param.IsOutputColor() {
statusColor = param.StatusCodeColor()
methodColor = param.MethodColor()
resetColor = param.ResetColor()
}
limitedReader := &io.LimitedReader{R: param.Request.Body, N: math.MaxInt64}
blob, err := ioutil.ReadAll(limitedReader)
if err != nil {
fmt.Printf("read request body error: %v", err)
}
return fmt.Sprintf(defaultLogFormatWithReqBody,
methodColor, param.Method, resetColor,
statusColor, param.StatusCode, resetColor,
param.ClientIP,
param.Request.Proto,
param.Request.UserAgent(),
param.Path,
param.Request.Header.Get("X-Request-Id"),
param.TimeStamp.Format(time.RFC3339),
param.Latency,
blob,
param.ErrorMessage,
)
}
Fail Case
API Repo
main.go
package main
func main() {
router = setUpRouter()
router.Run(fmt.Sprintf(":%d", 8080))
}
func setUpRouter() *gin.Engine {
config := newCorsConfig()
r := gin.New()
r.Use(useLogger(footprint.DefaultLoggerFormatter))
r.Use(config)
r.Use(gin.Recovery())
r.GET("")
registerRoute(r)
return r
}
func registerRoute(r *gin.Engine) {
group := r.Group("group")
group.POST("")
}
Debugger Result
limitedReader := &io.LimitedReader{R: param.Request.Body, N: math.MaxInt64}
blob, err := ioutil.ReadAll(limitedReader)
if err != nil {
fmt.Printf("read request body error: %v", err)
}
// FootPrint Repo
// ReadAll Result is []byte
// API Repo
// ReadAll Result is []byte length 0
// data exists in the body src of the pointer of the limitedReader, but when ReadAll() is executed, the data is 0.
// i was check using breakpoints
Comment From: Kyocfe
anybody here?
Comment From: patarapolw
I had to do it in another Middleware, as param.Request.Body
always read nil (and param.Request.GetBody()
always throw nil pointer reference).
app.Use(func(c *gin.Context) {
b, _ := ioutil.ReadAll(c.Request.Body)
if len(b) > 0 {
c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(b))
gin.DefaultWriter.Write([]byte("body: "))
gin.DefaultWriter.Write(b)
gin.DefaultWriter.Write([]byte("\n"))
}
c.Next()
})
Comment From: rohitChaku
Since the body is already consumed before the gin logger logs the request, I decoded to parse the body in another middleware and store it in the gin request context c.Set("body", <parsed_request_body>)
, then use it in the gin formatter as params.Keys["body"]
so that I can see all the relevant info together.
Pls advise if there is a better way, thanks.