Request Logging

requestLogger(…​) takes one lambda and calls it once per completed request:

app.requestLogger((req, completion) -> logger.info("{} {} -> {} ({}ms)",
        req.method(), req.path(), completion.statusCode(), completion.took().toMillis()));

Core carries no logging framework, so which logger, at which level, and in which format is the application’s call. Logging covers the backend behind that logger. The lambda receives a RequestCompletion after response writing finishes. statusCode() is the final status reported by the servlet response, including a status changed by a raw writer or by recovery from a write failure. took() is a Duration covering dispatch, rendering, decoration, and writing. response() is the response definition after decoration, or the last definition available if decoration failed. Its status and headers may differ from what a raw writer sent.

writeFailure() is the exception from decoration or writing, or null when neither failed. writeFailed() reports whether that exception is present. A failure before the response is committed can produce a 500, which carries the CORS and security headers as any other answer does. A failure after it is committed leaves the status at 200 and aborts the transfer. The client then sees a chunked body with no last chunk, or fewer bytes than Content-Length announced, rather than a partial body that looks complete. Check writeFailed() rather than inferring a failure from the status code. An application exception handled during dispatch is represented by its response status, not by a transmission failure. Normal SSE closure is not a transmission failure. A completion does not confirm that the remote client received the entire body.

thrown() is what a handler, a filter, a template, or a status page threw, whether an exception handler answered it or the framework’s 500 did. threw() reports whether that exception is present. It is null when nothing threw, and for an HttpException, which is a status the handler chose rather than a failure. An Error such as an AssertionError or a StackOverflowError is reported as an exception is, so thrown() and writeFailure() are a Throwable. Read it together with statusCode(): an exception mapped to a 400 is the caller’s mistake, and one answered with a 500 is the application’s. A logger that records failures on a tracing span or in an error list reads it here, instead of registering a catch-all exception(…​) handler that would also have to answer the request:

app.requestLogger((req, completion) -> {
    if (completion.threw() && completion.statusCode() >= 500) {
        Span.current().recordException(completion.thrown());
    }
});

req.route() is the route that answered, when one did, so a log line or a metric groups requests by /decks/{deckId} rather than by every id. It is null for a static file, a 404, a 405, and the automatic OPTIONS answer. Route Introspection has the rules. req.body() reads the text a filter or handler already read, since the text is kept for the whole request. An exception or an Error the logger throws is written to the servlet log and leaves the response alone.