go 52 lines · 7 steps

Capturing response bodies in Gin middleware

A Gin middleware wraps the response writer to tee output into a buffer, then logs a structured audit record after the handler runs.

Explained by highlit
1package middleware
2 
3import (
4 "bytes"
5 "time"
6 
7 "github.com/gin-gonic/gin"
8 "github.com/rs/zerolog/log"
9)
10 
11type responseCapture struct {
12 gin.ResponseWriter
13 body *bytes.Buffer
14}
15 
16func (w *responseCapture) Write(b []byte) (int, error) {
17 w.body.Write(b)
18 return w.ResponseWriter.Write(b)
19}
20 
21func (w *responseCapture) WriteString(s string) (int, error) {
22 w.body.WriteString(s)
23 return w.ResponseWriter.WriteString(s)
24}
25 
26func AuditLogger(maxBody int) gin.HandlerFunc {
27 return func(c *gin.Context) {
28 capture := &responseCapture{
29 ResponseWriter: c.Writer,
30 body: bytes.NewBuffer(make([]byte, 0, 1024)),
31 }
32 c.Writer = capture
33 
34 start := time.Now()
35 c.Next()
36 
37 payload := capture.body.Bytes()
38 if len(payload) > maxBody {
39 payload = payload[:maxBody]
40 }
41 
42 log.Info().
43 Str("method", c.Request.Method).
44 Str("path", c.FullPath()).
45 Int("status", c.Writer.Status()).
46 Int("bytes", capture.body.Len()).
47 Dur("latency", time.Since(start)).
48 Str("client_ip", c.ClientIP()).
49 Bytes("response", payload).
50 Msg("request audited")
51 }
52}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Embedding an interface lets you override just the methods you care about while delegating the rest.
  2. 2Swapping c.Writer before calling c.Next lets middleware observe everything downstream handlers produce.
  3. 3Truncating captured payloads before logging bounds memory and log volume for large responses.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Capturing response bodies in Gin middleware — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code