Idea: I want to log incoming and outcoming requests to my Gin server with unique request ID. Also I want to log all HTTP client's requests inside my Gin's routes using the same request ID that route has.
All of that should to work under the hood using middleware.
To log each request to my server I wrote this middleware:
import (
"bytes"
"context"
"github.com/gin-contrib/requestid"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"io/ioutil"
"net/http"
"time"
)
type responseBodyWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
func (r responseBodyWriter) Write(b []byte) (int, error) {
r.body.Write(b)
return r.ResponseWriter.Write(b)
}
func LoggerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
w := &responseBodyWriter{body: &bytes.Buffer{}, ResponseWriter: c.Writer}
c.Writer = w
msg := "Input:"
path := c.Request.URL.Path
raw := c.Request.URL.RawQuery
if raw != "" {
path = path + "?" + raw
}
// Read from body and write here again.
var bodyBytes []byte
if c.Request.Body != nil {
bodyBytes, _ = ioutil.ReadAll(c.Request.Body)
}
c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
inputLogger := log.With().
Str("method", c.Request.Method).
Str("path", path).
Str("requestId", requestid.Get(c)).
Logger()
if len(bodyBytes) > 0 {
inputLogger.Info().RawJSON("body", bodyBytes).Msg(msg)
} else {
inputLogger.Info().Msg(msg)
}
c.Next()
end := time.Now()
latency := end.Sub(start)
msg = "Output:"
outputLogger := log.With().
Str("method", c.Request.Method).
Str("path", path).
Str("requestId", requestid.Get(c)).
RawJSON("body", w.body.Bytes()).
Int("status", c.Writer.Status()).
Dur("latency", latency).
Logger()
switch {
case c.Writer.Status() >= http.StatusBadRequest && c.Writer.Status() < http.StatusInternalServerError:
{
outputLogger.Warn().Msg(msg)
}
case c.Writer.Status() >= http.StatusInternalServerError:
{
outputLogger.Error().Msg(msg)
}
default:
outputLogger.Info().Msg(msg)
}
}
}
Here is the problem: I don't know how to pass request ID (or Gin's context), created by Gin's middleware to the RoundTrip
function:
type Transport struct {
Transport http.RoundTripper
}
var defaultTransport = Transport{
Transport: http.DefaultTransport,
}
func init() {
http.DefaultTransport = &defaultTransport
}
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
ctx := context.WithValue(req.Context(), ContextKeyRequestStart, time.Now())
req = req.WithContext(ctx)
t.logRequest(req)
resp, err := t.transport().RoundTrip(req)
if err != nil {
return resp, err
}
t.logResponse(resp)
return resp, err
}
func (t *Transport) logRequest(req *http.Request) {
log.Info().
Str("method", req.Method).
Str("path", req.URL.String()).
Str("requestId", "how can I get request id here???").
Msg("Api request: ")
}
func (t *Transport) logResponse(resp *http.Response) {
var bodyBytes []byte
if resp.Body != nil {
bodyBytes, _ = ioutil.ReadAll(resp.Body)
}
resp.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
ctx := resp.Request.Context()
log.Info().
Str("method", resp.Request.Method).
Str("path", resp.Request.URL.String()).
Str("requestId", "how can I get request id here???").
RawJSON("body", bodyBytes).
Int("status", resp.StatusCode).
Dur("latency", time.Now().Sub(ctx.Value(ContextKeyRequestStart).(time.Time))).
Msg("API response: ")
}
func (t *Transport) transport() http.RoundTripper {
if t.Transport != nil {
return t.Transport
}
return http.DefaultTransport
}
The Transport.RoundTrip
function takes a *http.Request
parameter, so you should be able to pass the Gin context by just creating a request in your handlers with it:
func MyHandler(c *gin.Context) {
// passing context to the request
req := http.NewRequestWithContext(c, "GET", "http://localhost:8080", nil)
resp, err := http.DefaultClient.Do(req)
}
Note that to be able to make use of the default RoundTripper
that you overwrote without additional initialization, you should use the http.DefaultClient
.