loq_shelf 0.2.0
loq_shelf: ^0.2.0 copied to clipboard
Structured request logging middleware for Shelf, powered by loq. Drop-in replacement for logRequests() with structured fields, zone context, and configurable log levels.
0.2.0 #
Breaking #
-
Sealed event hierarchy. Hooks now receive a typed
ShelfLogEventinstead of rawRequest/Response/Durationpositional arguments. Three concrete variants live under the sealed base:ShelfRequestStartEventfor the start log. Carriesrequestanddefaults;elapsedisnull.ShelfResponseEventfor the completion log. Carriesrequest,response,elapsed,defaults.ShelfRequestErrorEventfor the error log. Carriesrequest,elapsed,defaults. The caught error and stack trace reach hooks througherrorFields:'s extra positional parameters.
Branch on the event with
switchfor per-path shaping. -
Hook collapse. The three separate
fields:/responseFields:/errorFields:callbacks are now two, both event-typed:fields: (ShelfLogEvent event) -> Map<String, Object?>for the start and completion paths.errorFields: (ShelfLogEvent event, Object error, StackTrace stack) -> Map<String, Object?>for the error path.
-
levelResolversignature is now(ShelfLogEvent event, Object? error) -> Level?. The previousResponse?/Durationtop-level parameters are reachable through the event.levelResolvernow also applies toShelfRequestStartEvent, with a fallback ofLevel.info. Returningnullfalls back to the per-event default (Level.infofor start, status-family mapping for response,Level.errorfor error). -
Error event defaults inherit request defaults. The error event's
defaultsnow includes everything the start event carried (method, path, requestId, captured headers, and so on) plus the error-specific fields (duration_ms,error.type,error.message,slow). ReplacingerrorFields:with a partial map no longer silently loses the request context inside the hook; users see the full picture inevent.defaults. The emitted record was already inheriting these via the bound logger; the change is to what the hook sees, making it consistent withloq_drift. -
Requires
loq ^0.1.2.
Migration #
// 0.1.x
loqMiddleware(
fields: (req, defaults) => {...defaults, 'tenant': resolveTenant(req)},
responseFields: (response, elapsed, defaults) => {
...defaults,
'cache_hit': response.headers['cache-status'] == 'HIT',
},
errorFields: (error, stack, elapsed, defaults) => {
...defaults,
'error.retryable': error is TimeoutException,
},
levelResolver: (response, elapsed, error) {
if (response?.statusCode == 404) return Level.info;
return null;
},
)
// 0.2.0
loqMiddleware(
fields: (event) => switch (event) {
ShelfRequestStartEvent() => {
...event.defaults,
'tenant': resolveTenant(event.request),
},
ShelfResponseEvent(:final response) => {
...event.defaults,
'cache_hit': response.headers['cache-status'] == 'HIT',
},
ShelfRequestErrorEvent() => event.defaults,
},
errorFields: (event, error, stack) => {
...event.defaults,
'error.retryable': error is TimeoutException,
},
levelResolver: (event, error) => switch (event) {
ShelfResponseEvent(:final response) when response.statusCode == 404 =>
Level.info,
_ => null,
},
)
Internal #
-
Inner middleware closure restructured around named
logStartEvent/logCompletion/logFailurehelpers. The try/catch is now pure flow control. Cuts the closure body roughly in half without changing observable behavior. -
Tests split by feature area to keep each file readable:
loq_middleware_test.dart(core, OTel defaults, request ID, skip, slow, messages, concurrency),loq_middleware_hooks_test.dart(fields, errorFields, levelResolver, route/clientIp resolvers, default level mapping),loq_middleware_capture_test.dart(header and query capture, redaction, prefixed-fields constants),shelf_log_event_test.dart(sealed event unit tests). -
Test fixtures moved to
test/test_helpers.dart(FakeConnectionInfo,request()). The hand-rolledTestLogHandleris gone; tests useRecordingHandlerfrompackage:loq/testing.dartand lean on itsfrom,messageContaining,at,atOrAbovefilters where they shorten the assertions.
0.1.0 #
- Initial release.
loqMiddleware(), structured request logging for Shelf.- Default fields aligned with OpenTelemetry HTTP server semantic
conventions:
http.request.method,url.path,url.scheme,server.address,server.port,network.protocol.version,client.address,user_agent.original,http.request.body.size,http.route,http.response.status_code,http.response.body.size,http.response.header.content-type,error.type,error.message. PlusrequestId(loq-specific) andduration_ms. - Request fields propagate via zone context, so any log inside the handler inherits them.
- Configurable request ID extraction via
requestIdResolver. fields/responseFields/errorFieldsare the single transformation point for their respective logs. Each receives adefaultsmap containing everything the middleware would contribute (OTel core, captured headers,url.query,slow, whichever apply) and returns the final field map. Spread...defaultsto compose on top; return a different map to replace (which drops anything not re-included).logStartflag to suppress start logs in production.levelResolver, single hook that overrides level for both completion and error paths; sees response, elapsed duration, and error. Returningnullfalls back to the default mapping (5xx -> error, 4xx -> warn, else -> info;errorfor errors).slowRequestThreshold's warn-bump still stacks on top.routeResolver, supplies route templates (e.g./users/{id}) forhttp.route, preventing per-path cardinality explosions in dashboards.clientIpResolver, override forclient.address. Defaults to readingshelf_ioconnection info.skippredicate to bypass logging for health checks, readiness probes, and metrics scrapers.slowRequestThresholdto flag and escalate requests that exceed a duration: addsslow: trueand ensures the level is at leastwarn.captureRequestHeaders/captureResponseHeadersallowlists for binding selected headers as structured fields. Output field names follow the OTel conventionhttp.request.header.<lowercase>andhttp.response.header.<lowercase>. Request headers propagate via zone context.captureQueryParamsto opt in to logging the query string asurl.query(raw wire form, sensitive values masked, order and repeats preserved).- Secure-by-default redaction.
authorization,proxy-authorization,cookie,x-api-key,x-auth-token(request),set-cookie(response), and common token-bearing query keys (token,access_token,refresh_token,api_key,apikey,key,password,secret,signature,sig) are masked to***when captured. Override viaredactRequestHeaders/redactResponseHeaders/redactQueryParams; pass{}to disable. - Exposed
defaultRedactedRequestHeaderFieldsanddefaultRedactedResponseHeaderFields, the same threat model with the OTelhttp.request.header./http.response.header.prefix applied. Plug into loq core'sredact()processor to share the HTTP defaults with redaction of fields contributed by user code, without manually rebuilding the prefix mapping. startMessage/completeMessage/errorMessageoverrides.