LogoPly
Middleware

Structured Logging

Log requests with slog and request IDs.

Structured Logging

middleware.SlogLogger writes one structured log record per request using log/slog.

logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

app.Use(
	middleware.RequestID(),
	middleware.SlogLogger(logger),
)

Each record contains:

AttributeValue
methodHTTP method
pathRequest path
statusResponse status
durationRequest duration
remote_ipClient IP
request_idID from RequestID, when present

The message is request. The level is Info by default.

Options

app.Use(middleware.SlogLogger(logger, middleware.SlogLoggerOptions{
	Level:     slog.LevelDebug,
	SkipPaths: []string{"/healthz", "/readyz"},
}))
OptionDescription
LevelLog level for request records
SkipPathsExact paths that produce no log record

Use RequestID before SlogLogger so the request_id attribute is present.

JSON output

With a JSON handler, a record looks like:

{
  "time": "2026-11-06T12:00:00.000Z",
  "level": "INFO",
  "msg": "request",
  "method": "GET",
  "path": "/api/users",
  "status": 200,
  "duration": 1200000,
  "remote_ip": "203.0.113.10",
  "request_id": "8f14e45fceea167a5a36dedd4bea2543"
}

duration is encoded by slog as nanoseconds in JSON output.

remote_ip is the direct peer address. Ply does not trust X-Forwarded-For or X-Real-IP by default. When the service runs behind a proxy, set the client address at the proxy or add a middleware that validates the forwarded header.

Logger ownership

The middleware does not create a logger or write to standard output. Pass the logger from the application so its handler, level, and output are controlled in one place.

handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
	Level: slog.LevelInfo,
})
logger := slog.New(handler)

SlogLogger panics if the logger is nil.

Existing logger

The built-in middleware.Logger prints human-readable colored lines for development. Use SlogLogger when logs are collected by a service and need machine-readable fields. Running both doubles request logs.