Go/Gin

Go Gin Structured Logging Middleware With Request IDs

4 min read by DebuggedIt

Quick answer

Structured logging middleware for Gin records one machine-readable log line per request, with a request ID that ties together every log message produced while...

Structured logging middleware for Gin records one machine-readable log line per request, with a request ID that ties together every log message produced while handling it. Generate or accept an ID, store a logger carrying that ID in the request context, and log method, path, status, and latency after the handler finishes. This makes production debugging far faster than grepping plain text.

Why Do You Need Request IDs and Structured Logs?

When many requests run at once, interleaved log lines are nearly impossible to follow. A request ID attached to every line lets you filter all messages belonging to one request, across handlers, services, and database calls. If the ID is also passed to downstream services, you can follow a request across the whole system.

Structured logs, written as JSON key-value pairs, are queryable by field. You can ask for all 500 responses on one route, or the slowest requests for one user, without writing fragile text patterns. Go's standard library includes the log/slog package for this purpose, available in recent Go versions, so you may not need a third-party library.

How Do You Write the Middleware?

The middleware does four things: obtain a request ID, create a request-scoped logger, run the rest of the chain, and log a summary. Accept an incoming ID header from trusted upstream proxies, and generate a new one otherwise.

func RequestLogger(base *slog.Logger) gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        id := c.GetHeader("X-Request-ID")
        if id == "" {
            id = uuid.NewString()
        }
        c.Header("X-Request-ID", id)

        logger := base.With("request_id", id)
        ctx := context.WithValue(c.Request.Context(), loggerKey{}, logger)
        c.Request = c.Request.WithContext(ctx)

        c.Next()

        logger.Info("request",
            "method", c.Request.Method,
            "path", c.FullPath(),
            "status", c.Writer.Status(),
            "latency_ms", time.Since(start).Milliseconds(),
            "bytes", c.Writer.Size(),
        )
    }
}

Use c.FullPath, which returns the route pattern such as /users/:id, instead of the raw URL path. High-cardinality paths make metrics and dashboards messy and can inflate storage.

How Do Other Layers Use the Logger?

Store the logger in the context and provide a small helper that retrieves it, falling back to a default logger when none is present. Services and repositories receive the context anyway, so they can log with the same request ID without extra parameters.

  • Pass context.Context through every call, and use it for logging, cancellation, and tracing alike.
  • Add fields progressively, such as user_id after authentication, using logger.With.
  • Keep a consistent set of field names across services so queries work everywhere.
  • Consider adopting trace and span identifiers if you use distributed tracing, so logs and traces correlate.

Validate incoming request IDs. Accepting arbitrary values from untrusted clients lets them inject long or misleading strings into your logs. Restrict length and character set, or ignore the header unless the request came from a trusted proxy.

What Should You Log, and What Should You Avoid?

Choose the log level from the outcome. A reasonable convention is info for successful responses, warn for client errors such as 4xx, and error for server errors with 5xx status. Logging every 404 at error level creates noise that hides real problems.

  • Never log passwords, tokens, API keys, or full request bodies by default.
  • Redact sensitive headers such as Authorization and cookies.
  • Log errors where they are handled, once, with context, instead of at every layer they pass through.
  • Skip or sample very frequent health check requests so they do not flood the logs.
  • Write to standard output and let the platform collect and ship logs.

Pair the logger with recovery middleware so panics are logged with the request ID and a stack trace. Order matters: the request ID and logger middleware should run first, so that everything after it, including recovery, can use them.

Frequently Asked Questions

Should I use Gin's default logger?

It is fine for development, but it writes text lines. For production, custom structured middleware gives queryable fields and request IDs.

Where do I get a request ID if none is provided?

Generate one in the middleware, such as a UUID, and return it in a response header so clients can quote it in support requests.

Does slog work well with Gin?

Yes. Create a slog logger with a JSON handler and use it inside middleware. Check the Go version requirements for the package.

How do I keep logging from slowing requests?

Log concisely, avoid expensive formatting, and write to standard output. Sample noisy routes if volume is high.

Summary

Create middleware that assigns request IDs, stores a request-scoped logger in the context, and logs a structured summary per request. Keep sensitive data out of logs and use consistent fields. Check the documentation for your Go and Gin versions.