A collage plugin that writes one structured log line per request — method, path,
status, bytes, duration, client address, user agent, referer and request id —
through slog, and gives every request an id its handlers can log with.
app, err := collage.New(&collage.Config{
Plugins: []collage.Plugin{accesslog.New(accesslog.Options{})},
})Requires collage v0.61.0 or later.
time=2026-09-26T15:40:02.114+03:00 level=INFO msg=request method=GET path=/blog/hello status=200 bytes=5120 duration=1.84ms ip=203.0.113.9 user_agent="Mozilla/5.0 …" referer=https://example.org/ request_id=7f3c9a…
The fields are slog attributes, so the format is the logger's: the application's
own logger by default — whatever Config.Logger is — or Options.Logger. A
slog.NewJSONHandler writes the same line as JSON, where duration is in
nanoseconds.
A 5xx is logged at ERROR, everything else at INFO. path is the path without
its query: a query carries tokens and e-mail addresses often enough that logging it
by default is a leak waiting to be found. bytes is the body as written, before
any compression a proxy in front adds.
referer is the Referer header's scheme, host and path —
https://example.org/reset, not https://example.org/reset?token=… — since a
page linking to the site can carry a token in its query, fragment or user info.
A referer that does not parse as a URL is logged empty, never raw, and an opaque
one such as data:… as its scheme alone. fullReferer: true logs the header
exactly as it came.
Every request gets an id. The one in its X-Request-ID header is kept when it looks
like an id — up to 128 letters, digits and ._:;=+/-, which covers UUIDs, hex,
base64 and AWS's Root=1-… — and a new random one is made otherwise, so a client
cannot put a newline or a megabyte into the log.
The id is sent back on the response, and is in the request's context:
func checkout(ctx context.Context, rc *collage.RenderContext) (Order, []string, error) {
logger.InfoContext(ctx, "charging card", "request_id", accesslog.RequestID(ctx))
// ...
}accesslog.RequestID(rc.Context()) works in a data handler, an action, and any
handler of the application's own; outside a request it is "". The request's own
header is set to the id too, so a handler proxying the request onward passes on the
id that was logged. A page that renders the id must not be cached: a cached page
would show every reader the first reader's id.
Skip lists path prefixes that are never logged, /_collage/ — collage's
development endpoints — and /healthz and /readyz, the probe paths of
elagoht/health, by default. A different livePath or readyPath there must be
added to skip. A prefix covers whole segments, with or without a trailing
slash: /healthz and /healthz/ both cover /healthz and /healthz/db, not
/healthz-report. A skipped request still gets an id. An empty list logs
everything.
A busy site can log a fraction of its successful responses: Sample: 0.1 logs one
2xx in ten, chosen at random. A redirect, a 4xx and a 5xx are always logged —
they are the lines someone goes looking for.
ip is collage.ClientIP: the address the connection came from or, when that is
one of the application's Server.TrustedProxies, the client's address from
X-Forwarded-For, walked from the right past every trusted proxy. Everything to
the left of the first untrusted address was written by the client, who can write
anything there. With no trusted proxies listed — the default — X-Forwarded-For
is ignored; X-Real-IP is never read. Behind a reverse proxy, list it in
Config.Server.TrustedProxies (addresses or CIDR ranges), and every plugin that
asks collage for the client's address agrees with the log.
The response writer the plugin wraps passes Flush and Hijack through and has
Unwrap, so an event stream flushes and a WebSocket upgrades beneath it. A
hijacked connection is logged with status 101 when the request's handler returns;
its duration is how long the connection was open.
{
"elagoht/accesslog": {
"skip": ["/_collage/", "/healthz", "/readyz", "/static/"],
"sample": 0.25,
"requestIDHeader": "X-Request-ID",
"fullReferer": false
}
}Logger can only be set from Go. A sample outside 0 to 1, a skip prefix not
beginning with /, and a header name with anything but letters, digits and dashes stop the application from
starting.
- Where the plugin's middleware runs depends on collage's version. From collage
v0.38.0 it runs where the plugin was registered: in
Config.Pluginsit is outside everyapp.Usemiddleware, so a request the application's middleware answers itself — a401from an auth check — is logged, and the time that middleware spent is induration. Before v0.38.0 it ran inside the application's middleware, and such a request was neither logged nor timed. - With
elagoht/health, the order decides what is logged. Middleware is registered where it is listed, so with accesslog listed after health, a probe and a request health sheds with503 overloadednever reach it and are not logged. List accesslog before health to log shed requests; the probes then reach it too, which the defaultskipof/healthzand/readyzkeeps out of the log. - collage's development reload stream is served ahead of all middleware and is
never logged, whatever
Skipsays. - collage redirects a path with dot segments or doubled slashes to its clean spelling ahead of all middleware, so that redirect is not logged; the request for the clean path that follows is.
- Sampling to none is not offered:
Sampleis zero by default and zero means all. A site that wants no successful requests logged wantsSkip, or no access log.
Changed in v0.2.0, for the v1 plugin conventions. A removed key now stops the
application at start with unknown key, where it used to be ignored.
| was | now |
|---|---|
"trustProxy" / Options.TrustProxy |
removed: list the proxies in Config.Server.TrustedProxies |
"trustedProxies" / Options.TrustedProxies |
Config.Server.TrustedProxies |
"requestIdHeader" |
"requestIDHeader" (the old spelling still decodes: keys match case-insensitively) |
- The client's address is
collage.ClientIP. Two differences from before: with no trusted proxies listed nothing is trusted, where an emptytrustedProxiestrusted loopback and private addresses; andX-Real-IPis no longer read. - When a trusted proxy forwards an
X-Forwarded-Forhop that is not an address, the line shows the connection's own address; before, it showed the nearest trusted hop past it. - A
skipprefix covers whole segments, with or without a trailing slash:"/healthz"no longer covers/healthz-report, and"/static/"covers/staticitself. - Requires collage v0.61.0.
- Rebuilt against collage v0.60.0; the plugin's code is unchanged.
- Requires collage v0.60.0.
- collage.json is regenerated for editors:
skip's description names elagoht/health's paths, as the code does. Nothing else changes. - Requires collage v0.58.0.
- Behaviour change: the referer is trimmed.
refereris logged as its scheme, host and path, without its query, fragment or user info, where a reset token or a session id can travel; a referer that does not parse is logged as""rather than raw.fullReferer: true(Options.FullReferer) logs the header exactly as before. - Requires collage v0.55.0.
- Requires collage v0.53.0, the release
elagoht/healthneeds./readyzis skipped by default, beside/healthz: they are the paths ofelagoht/health. The README says where to list accesslog relative to health. Nothing else changes.
- Requires collage v0.52.0. A static build's header capture (
collage.IsCapture) is not logged and gets no request id:collage buildno longer writes a line for every file it asks for, nor reportsX-Request-IDas an unstable header.
- Retracts v0.1.6, tagged by mistake on the previous release's code. Use v0.1.7 or later. Nothing else changes.
- Requires collage v0.50.0. Plugin configuration is read with
collage.PluginConfig, sincehost.Configis gone. Nothing else changes.
- Requires collage v0.49.0. Tests only: the test site gives its fragments
typed data with
collage.Loadandcollage.DataHandler, sinceWithDataHandleris gone. The plugin itself is unchanged. - README: the request-id sample handler returns a real type, not
any.
- README: from collage v0.38.0 the plugin's middleware is outside the application's own, so a request an application middleware answers is logged.
collage.json: the plugin described to editors — its template functions, snippets and configuration schema — for the Collage Snippets & Highlighter extension and any tool reading it.
- README: a path collage v0.24.0 redirects to its clean spelling is redirected before middleware, so the redirect is not logged.
- Requires collage v0.24.0.