Week 3 Mini Project: Structured Request Logger Middleware
Build an HTTP middleware that logs every request as structured JSON to a rotating file. This is the audit trail for your WAF and API gateway.
Full Implementation
// middleware/logger.go
package middleware
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"net"
"net/http"
"time"
)
type ctxKey string
const requestIDKey ctxKey = "request_id"
// requestCapture wraps ResponseWriter to capture status code and response size
type requestCapture struct {
http.ResponseWriter
statusCode int
size int64
}
func (rc *requestCapture) WriteHeader(code int) {
rc.statusCode = code
rc.ResponseWriter.WriteHeader(code)
}
func (rc *requestCapture) Write(b []byte) (int, error) {
n, err := rc.ResponseWriter.Write(b)
rc.size += int64(n)
return n, err
}
type LogEntry struct {
Time string `json:"time"`
RequestID string `json:"request_id"`
Method string `json:"method"`
Path string `json:"path"`
Query string `json:"query,omitempty"`
Status int `json:"status"`
LatencyMS float64 `json:"latency_ms"`
ClientIP string `json:"client_ip"`
UserAgent string `json:"user_agent,omitempty"`
ReqSize int64 `json:"req_size_bytes"`
RespSize int64 `json:"resp_size_bytes"`
}
func generateRequestID() string {
b := make([]byte, 8)
rand.Read(b)
return hex.EncodeToString(b)
}
func realIP(r *http.Request) string {
if ip := r.Header.Get("X-Real-IP"); ip != "" {
return ip
}
if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
// Take only the first IP (rightmost is added by our proxy)
parts := strings.Split(forwarded, ",")
return strings.TrimSpace(parts[0])
}
host, _, _ := net.SplitHostPort(r.RemoteAddr)
return host
}
// Logger returns a middleware that logs each request to the provided writer
func Logger(log *RotatingLog) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
requestID := generateRequestID()
// Inject request ID into context
ctx := context.WithValue(r.Context(), requestIDKey, requestID)
r = r.WithContext(ctx)
// Inject request ID into response header
w.Header().Set("X-Request-ID", requestID)
// Wrap writer to capture status + size
capture := &requestCapture{ResponseWriter: w, statusCode: 200}
next.ServeHTTP(capture, r)
entry := LogEntry{
Time: time.Now().UTC().Format(time.RFC3339Nano),
RequestID: requestID,
Method: r.Method,
Path: r.URL.Path,
Query: r.URL.RawQuery,
Status: capture.statusCode,
LatencyMS: float64(time.Since(start).Microseconds()) / 1000.0,
ClientIP: realIP(r),
UserAgent: r.UserAgent(),
ReqSize: r.ContentLength,
RespSize: capture.size,
}
data, _ := json.Marshal(entry)
log.Write(string(data))
})
}
}
// GetRequestID retrieves the request ID from context
func GetRequestID(ctx context.Context) string {
if id, ok := ctx.Value(requestIDKey).(string); ok {
return id
}
return ""
}
Engineer Takeaway
Structured JSON logging is what lets your SIEM (Splunk, Datadog, Elastic) correlate events across services. The request_id field ties together the WAF log, auth log, and application log for a single request — critical for forensics after a security incident. The 0600 file permission on logs is an audit requirement.