요청 로깅
requestLogger(…)는 람다 하나를 받아 완료된 요청마다 한 번씩 호출한다:
app.requestLogger((req, completion) -> logger.info("{} {} -> {} ({}ms)",
req.method(), req.path(), completion.statusCode(), completion.took().toMillis()));
코어는 로깅 프레임워크를 담지 않으므로, 어떤 로거를, 어떤 레벨로, 어떤 형식으로 쓸지는 애플리케이션의 선택이다.
로깅이 그 로거 뒤의 백엔드를 다룬다.
람다는 응답 쓰기가 끝난 뒤 RequestCompletion을 받는다.
statusCode()는 서블릿 응답이 보고한 최종 상태이며, raw 라이터가 바꾼 상태나 쓰기 실패에서 복구하며 바뀐 상태도 포함한다.
took()은 디스패치, 렌더링, 데코레이션, 쓰기를 아우르는 Duration이다.
response()는 데코레이션 이후의 응답 정의이며, 데코레이션이 실패했으면 마지막으로 확보된 정의다.
그 상태와 헤더는 raw 라이터가 보낸 것과 다를 수 있다.
writeFailure()는 데코레이션이나 쓰기에서 난 예외이며, 둘 다 실패하지 않았으면 null이다.
writeFailed()는 그 예외가 있는지 보고한다.
응답이 커밋되기 전의 실패는 500을 낳을 수 있으며, 그 500은 다른 응답처럼 CORS와 보안 헤더를 담는다.
커밋된 뒤의 실패는 상태를 200으로 남긴 채 전송을 중단시킨다.
그러면 클라이언트는 완전해 보이는 부분 본문이 아니라, 마지막 청크가 없는 청크 본문이나 Content-Length가 알린 것보다 적은 바이트를 받는다.
실패 여부는 상태 코드로 추론하지 말고 writeFailed()로 확인한다.
디스패치 중에 처리된 애플리케이션 예외는 전송 실패가 아니라 그 응답 상태로 표현된다.
SSE의 정상 종료는 전송 실패가 아니다.
완료는 원격 클라이언트가 본문 전체를 받았음을 확인하지 않는다.
thrown()은 핸들러, 필터, 템플릿, 상태 페이지가 던진 예외이며, 예외 핸들러가 답했든 프레임워크의 500이 답했든 마찬가지다.
threw()는 그 예외가 있는지 보고한다.
아무것도 던지지 않았을 때, 그리고 실패가 아니라 핸들러가 선택한 상태인 HttpException일 때 null이다.
AssertionError나 StackOverflowError 같은 Error도 예외와 같이 보고되므로, thrown()과 writeFailure()는 Throwable이다.
statusCode()와 함께 읽는다: 400에 매핑된 예외는 호출자의 실수이고, 500으로 답한 예외는 애플리케이션의 실수다.
트레이싱 스팬이나 에러 목록에 실패를 기록하는 로거는, 요청에도 답해야 하는 포괄적인 exception(…) 핸들러를 등록하는 대신 여기서 이 값을 읽는다:
app.requestLogger((req, completion) -> {
if (completion.threw() && completion.statusCode() >= 500) {
Span.current().recordException(completion.thrown());
}
});
req.route()는 요청에 답한 라우트가 있으면 그 라우트를 돌려주므로, 로그 한 줄이나 메트릭이 요청을 id마다가 아니라 /decks/{deckId} 단위로 묶는다.
정적 파일, 404, 405, 자동 OPTIONS 답에서는 null이다.
라우트 인트로스펙션에 규칙이 있다.
req.body()는 필터나 핸들러가 이미 읽은 텍스트를 읽는데, 텍스트는 요청 전체 동안 보관되기 때문이다.
로거가 던진 예외나 Error는 서블릿 로그에 기록되며 응답은 건드리지 않는다.