10

I am setting up a small API using Go Gin, however I am unable to convince the logger to output JSON. By default it's a key/value string, but I need it as json.

How can I achieve that? I feel it should be easily support but I've been struggling with a custom formatting function where I need to account for various parameters myself.

Alternatively, I also log manually using uber/zap logger but I haven't found a way to replace gin's logger with mine.

Any pointers would be appreciated as the documentation on gin's github wasn't too helpful.

Thanks!

EDIT: to clarify, adding a middlware helps with logging requests, but I'm looking for single point of setting JSON logging for Gin (eg: including logs not related to requests, such as debug / info logs of framework internals)

Andrei Dascalu
  • 1,059
  • 2
  • 14
  • 26

3 Answers3

11

access logger

https://github.com/sbecker/gin-api-demo/blob/master/middleware/json_logger.go


// JSONLogMiddleware logs a gin HTTP request in JSON format, with some additional custom key/values
func JSONLogMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // Start timer
        start := time.Now()

        // Process Request
        c.Next()

        // Stop timer
        duration := util.GetDurationInMillseconds(start)

        entry := log.WithFields(log.Fields{
            "client_ip":  util.GetClientIP(c),
            "duration":   duration,
            "method":     c.Request.Method,
            "path":       c.Request.RequestURI,
            "status":     c.Writer.Status(),
            "user_id":    util.GetUserID(c),
            "referrer":   c.Request.Referer(),
            "request_id": c.Writer.Header().Get("Request-Id"),
            // "api_version": util.ApiVersion,
        })

        if c.Writer.Status() >= 500 {
            entry.Error(c.Errors.String())
        } else {
            entry.Info("")
        }
    }
}

debug logger

Looking at the gin source code, it is found that the debug log is output to an io.Writer. Rewriting this object redirects the output to json, similar to the method of processing the output of http.Server.Errorlog.

func debugPrint(format string, values ...interface{}) {
    if IsDebugging() {
        if !strings.HasSuffix(format, "\n") {
            format += "\n"
        }
        fmt.Fprintf(DefaultWriter, "[GIN-debug] "+format, values...)
    }
}

set debug write, this code is no test.

// WriteFunc convert func to io.Writer.
type WriteFunc func([]byte) (int, error)
func (fn WriteFunc) Write(data []byte) (int, error) {
    return fn(data)
}

func NewLogrusWrite() io.Writer {
    return WriteFunc(func(data []byte) (int, error) {
        logrus.Debugf("%s", data)
        return 0, nil
    })
}
// set gin write to logrus debug.
gin.DefaultWriter = NewLogrusWrite()

get all http.Server Error log.

htt.Server

htt.Server outputs logs to a log.Logger, and creates a log.Logger output by the specified io.Writer to receive the Error log from http.Sever There is no detailed write gin to use custom Sever code, please check the gin documentation.

srv := &http.Server{
    // log level is bebug, please create a error level io.Writer
    ErrorLog: log.New(NewLogrusWrite(), "", 0),
} 

eudore
  • 705
  • 5
  • 10
  • thanks for replying, but this isn't what I needed. I tried this, as a middleware, and it works for logging requests, but I want all logs to be json (for example including framework's debug logs, other internals that are logged depending on level, etc). – Andrei Dascalu Apr 29 '20 at 13:25
  • Update answer in now. – eudore Apr 29 '20 at 14:14
  • @eudore why isn't this supported with gin by default? – uberrebu Oct 17 '21 at 22:10
  • @uberrebu Maybe only the routing function is designed, and there is no log design. – eudore Oct 18 '21 at 01:24
5

In addition to @audore's answer; His method will change the default output which is stdout at default. We still want to use stdout but if we'd like to change the whole output we should do these steps.

1- Change gin initialization from

r := gin.Default()

to

r := gin.New()
r.Use(gin.Recovery()) // to recover gin automatically
r.Use(jsonLoggerMiddleware()) // we'll define it later

because gin.Default() is using the default Logger() middleware. If we keep using it gin will output twice

2- add the middleware to your application

func jsonLoggerMiddleware() gin.HandlerFunc {
    return gin.LoggerWithFormatter(
        func(params gin.LogFormatterParams) string {
            log := make(map[string]interface{})

            log["status_code"] = params.StatusCode
            log["path"] = params.Path
            log["method"] = params.Method
            log["start_time"] = params.TimeStamp.Format("2006/01/02 - 15:04:05")
            log["remote_addr"] = params.ClientIP
            log["response_time"] = params.Latency.String()

            s, _ := json.Marshal(log)
            return string(s) + "\n"
        },
    )
}

and your output will be:

{"method":"GET","path":"/v1/somepath","remote_addr":"::1","response_time":"5.719ms","start_time":"2022/10/03 - 17:26:11","status_code":200}

It'll be too much easy to parse from logging listeners like Loki+Grafana

Mehmet Gökalp
  • 314
  • 3
  • 6
0

for go > 1.21 we have log/slog package which helps in structured logging

so currently best slog option for gin is: samber/slog-gin package

hmmftg
  • 1,274
  • 1
  • 18
  • 31