Frontend Labs
⋯

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.

Generated from week3.md · 8 Oct 2026 16:36