Structured logging
Core declares gas.Logger as a fluent interface. gas/log supplies backends
that implement it, so application code logs against one API and the backend is a
wiring decision.
go get github.com/gasmod/gas/logChoosing a backend
Section titled “Choosing a backend”| Backend | Library | Notes |
|---|---|---|
NewZeroLogLogger() |
rs/zerolog | Fast structured JSON, full level support including Trace |
NewSlogLogger() |
log/slog |
Zero extra dependencies. Trace maps to Debug |
NewShippingLogger() |
log/slog plus HTTP |
Writes locally and batches records to an endpoint |
Each returns a constructor function, not a logger, so it drops straight into the container.
// A singleton logger is shared. Registering it scoped instead gives each// request its own instance, which is what lets middleware attach request// fields without leaking them across requests.func register() []gas.Option { return []gas.Option{ gas.WithSingletonService[gas.Logger](gaslog.NewSlogLogger()), gas.WithScopedService[gas.Logger](gaslog.NewZeroLogLogger()), }}Logging
Section titled “Logging”func fluent(logger gas.Logger) { logger.Info("request handled"). Str("method", "GET"). Str("path", "/api/notes"). Int("status", 200). Send()
// With branches into a new logger carrying persistent fields. sub := logger.With().Str("service", "notes").Logger() sub.Debug("cache miss").Send()}Every event ends with Send(). Fields available on events, on With(), and on
SetBaseFields(): Str, Int, Int64, Float64, Bool, Err, Duration,
and Any.
Request-scoped fields
Section titled “Request-scoped fields”With() branches into a new logger, which is wrong for middleware, since the
handler downstream holds the original. SetBaseFields() mutates in place
instead, so everything logged later in that request carries the fields, even
from code that never saw the middleware.
// SetBaseFields mutates the receiver instead of branching, so everything// logged later in the same request carries these fields, including from// handlers that never saw this middleware.func attachRequestFields(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { logger := gas.MustResolveFromRequestScope[gas.Logger](r) logger.SetBaseFields(). Str("user_id", "u-123"). Str("trace_id", r.Header.Get("X-Trace-Id")). Apply() next.ServeHTTP(w, r) })}Request logging
Section titled “Request logging”// The built-in request logger records method, path, status, bytes, duration// and remote address. Responses at 400 and above log at error level.func requestLogging(router *gas.Router) { router.UseMiddlewareFunc(gas.RequestLogger[gas.Logger]())}Logs method, path, status, bytes written, duration, and remote address for every
request, at error level for statuses at 400 and above. If chi’s RequestID
middleware is mounted upstream, the request id is attached automatically; pass
gas.WithRequestLoggerAppendRequestID(false) to turn that off.
Shipping logs
Section titled “Shipping logs”// Ships every record to an HTTP endpoint as well as writing locally. Delivery// is best-effort: records are dropped rather than blocking the caller, and// failures go to WithErrorHandler instead of the logging call site.func shipping(endpoint, apiKey string) gas.Logger { return gaslog.NewShippingLogger( endpoint, gaslog.NewOTLPMarshaler( gaslog.WithServiceName("notes"), gaslog.WithServiceVersion("1.4.2"), ), gaslog.WithHeader("X-API-Key", apiKey), gaslog.WithBatchSize(100), )()}Records are captured by an slog.Handler, batched, and delivered by a
background goroutine. The wire format is a pluggable Marshaler, with an
OTLP/HTTP JSON one included.
Delivery is best-effort by design: when the queue is full, records are dropped
rather than blocking the code that logged them, and delivery failures go to
WithErrorHandler rather than surfacing at the call site. Logging should not
be able to stall or fail a request.
The shipping logger implements gas.Service, so registering it in the container
means Close drains buffered records at shutdown. Outside the container, call
Flush() before exit.
Context propagation
Section titled “Context propagation”gas.WithLogger(ctx, logger) puts a logger on a context and
gas.LoggerFromContext(ctx) takes it back out, returning nil when absent. Useful
for code far from the handler that should still log with request fields
attached.
Levels
Section titled “Levels”gas.Logger |
Zerolog | Slog |
|---|---|---|
Trace |
TraceLevel |
LevelDebug |
Debug |
DebugLevel |
LevelDebug |
Info |
InfoLevel |
LevelInfo |
Warn |
WarnLevel |
LevelWarn |
Error |
ErrorLevel |
LevelError |
If no logger is registered at all, Gas falls back to a slog-backed one and warns once, so startup diagnostics are never silently lost.