【Spring Boot】リクエストのログ出力
ログ出力の基本
Spring Boot におけるログ出力については、別の記事にまとめてありますのでこちらを参考にしてください。
この記事ではリクエストの情報をログに出力する処理を個別に設定するのではなく、共通化することを考えていきます。
Interceptor
共通化する処理する方法の 1 つとして、Interceptor があります。 Interceptor の詳細については、こちらの記事を参考にしてください。
以下のようにリクエストの情報を出力する Interceptor を作成します。 出力する内容は適宜変更してください。
public class LoggingHandlerInterceptor implements HandlerInterceptor {
Logger logger = LoggerFactory.getLogger(LoggingHandlerInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String message = String.format("[%s] %-4s %s",
request.getLocalAddr(), request.getMethod(), request.getRequestURI());
logger.info(message);
return true;
}
}
作成した Interceptor を以下のように登録することで、指定したパスのリクエスト情報がログに出力されます。
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Bean
LoggingHandlerInterceptor loggingHandlerInterceptor() {
return new LoggingHandlerInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loggingHandlerInterceptor())
.addPathPatterns("/api/**");
}
}
[127.0.0.1] GET /api/sample
Interceptor を利用する場合の注意点として、リクエストボディの情報はログに出力することができません。 正確には、ここでリクエストボディの情報を読み込んでしまうと、後の処理で読み込み済みという扱いになりエラーとなってしまいます。
CommonsRequestLoggingFilter
以下のようにCommonsRequestLoggingFilter
を Bean として(DI コンテナに)登録することで、リクエストについて決まったフォーマットのログが出力されるようになります。
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Bean
public CommonsRequestLoggingFilter requestLoggingFilter() {
CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter();
filter.setIncludeClientInfo(true);
filter.setIncludeQueryString(true);
filter.setIncludeHeaders(true);
filter.setIncludePayload(true);
filter.setMaxPayloadLength(1024);
return filter;
}
}
ただし、ログレベルがDEBUG
の場合のみ出力されるため、以下のように設定を追加してください。
logging.level:
org.springframework.web.filter.CommonsRequestLoggingFilter: DEBUG
setIncludeClientInfo()
のように、出力する内容はメソッドにより設定できます。
メソッド | 説明 |
---|---|
setIncludeClientInfo() | IP アドレス、HTTP メソッド |
setIncludeQueryString() | クエリパラメーター |
setIncludeHeaders() | リクエストヘッダー |
setIncludePayload() | リクエストボディ |
setMaxPayloadLength() | リクエストボディの表示サイズ |
以下はContent-Type
がapplication/json
の場合の出力例です。
After request [POST /api, client=0:0:0:0:0:0:0:1, headers=[host:"localhost:8080", accept-encoding:"gzip, deflate, br", connection:"keep-alive", content-length:"110", Content-Type:"application/json;charset=UTF-8"], payload={
"id": "0001",
"name": "Tanaka",
"age": 25,
}]
実際には Before request と After Request の 2 つが出力されます。 詳細は割愛しますが、Before はリクエストの受信直後、After はレスポンスの送信直前くらいに考えてください。
基本的には同じなのですが、Before ではリクエストボディの情報が出力されません。 理由については後述します。
ここでCommonsRequestLoggingFilter
の実装をみてみます。
public class CommonsRequestLoggingFilter extends AbstractRequestLoggingFilter {
@Override
protected boolean shouldLog(HttpServletRequest request) {
return logger.isDebugEnabled();
}
@Override
protected void beforeRequest(HttpServletRequest request, String message) {
logger.debug(message);
}
@Override
protected void afterRequest(HttpServletRequest request, String message) {
logger.debug(message);
}
}
見ての通り実装はシンプルで、AbstractRequestLoggingFilter
を継承しています。
例えば Before の出力が不要な場合やログレベルを変更したい場合は、同じ構成でクラスを作成し、Bean として登録すればよいだけです。
public class CommonsRequestLoggingFilter extends AbstractRequestLoggingFilter {
@Override
protected boolean shouldLog(HttpServletRequest request) {
return logger.isDebugEnabled();
}
@Override
protected void beforeRequest(HttpServletRequest request, String message) {
//不要のため削除
}
@Override
protected void afterRequest(HttpServletRequest request, String message) {
logger.info(message); //ログレベルの変更
}
}
OncePerRequestFilter
CommonsRequestLoggingFilter
から出力されるメッセージフォーマットを変更したいなどをしたい場合は、OncePerRequestFilter
を継承したクラスを作成します。
先に言っておくと、果たしてここまでする必要があるのか、という疑問はありますが…。
CommonsRequestLoggingFilter
が継承していたAbstractRequestLoggingFilter
は、OncePerRequestFilter
を継承しています。
そのためAbstractRequestLoggingFilter
を参考に作成していきます。
AbstractRequestLoggingFilter
でログを出力しているのは以下のdoFilterInternal()
です。
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
boolean isFirstRequest = !isAsyncDispatch(request);
HttpServletRequest requestToUse = request;
//**ここがポイント**
if (isIncludePayload() && isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) {
requestToUse = new ContentCachingRequestWrapper(request, getMaxPayloadLength());
}
boolean shouldLog = shouldLog(requestToUse);
if (shouldLog && isFirstRequest) {
//リクエスト直後の処理
beforeRequest(requestToUse, getBeforeMessage(requestToUse));
}
try {
//**ここがポイント**
filterChain.doFilter(requestToUse, response);
}
finally {
if (shouldLog && !isAsyncStarted(requestToUse)) {
//レスポンス直前の処理
afterRequest(requestToUse, getAfterMessage(requestToUse));
}
}
}
いろいろ記載がありますが、ログの出力は抽象メソッドであるbeforeRequest()
とafterRequest()
によって行われます。
具体的なログの出力処理は継承先で定義されますが、getAfterMessage()
とあるようにログのメッセージはAbstractRequestLoggingFilter
内で作成されます。
さて、ここでポイントとなるのがContentCachingRequestWrapper
です。
これはリクエストをラップすることで、リクエストボディの情報をキャッシュできるようになります。
例えば以下のようにすることで、リクエストボディの内容をログに出力できるようになります。
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
//リクエストをキャッシュする
ContentCachingRequestWrapper wrapper = new ContentCachingRequestWrapper(request);
wrapper.getParameterNames();
//リクストボディの内容を出力
String requestBody = new String(wrapper.getContentAsByteArray(), wrapper.getCharacterEncoding());
logger.info(requestBody);
//次のフィルタへ
filterChain.doFilter(wrapper, response);
}
ContentCachingRequestWrapper
のgetParameterNames()
を実行することにより、リクエストボディの内容が内部にキャッシュされます。
キャッシュした内容はgetContentAsByteArray()
で取得します。
フィルターチェインをするときにContentCachingRequestWrapper
のオブジェクトを渡すことで、リクエストボディの内容を先に読んでいたとしても後続の処理でエラーにならなくなります。
キャッシュはgetParameterNames()
以外に、getParameter()
、getParameterMap()
、getParameterValues()
のいずれかで実行されます。
ただし、HTTP メソッドが POST かつContent-Type
がapplication/x-www-form-urlencoded
の場合に限ります。
Content-Type
がapplication/json
の場合は以下のようにします。
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
ContentCachingRequestWrapper wrapper = new ContentCachingRequestWrapper(request);
try {
//次のフィルタへ
filterChain.doFilter(wrapper, response);
} finally {
//リクストボディの内容を出力
String requestBody = new String(wrapper.getContentAsByteArray(), wrapper.getCharacterEncoding());
logger.info(requestBody);
}
}
先程の例との違いは、キャッシュの処理がなく、ログの出力を Filter の後処理として定義します。
ここからは推測になってしまうのですが、データバインドする際にキャッシュする処理が行われているため、後処理でキャッシュした内容を読み込むことでログが出力できるのではないかと考えています。 しかし、具体的にどこでどのようにキャッシュされているかのソースが見つからなかったため、あまり深く言及はしません。
CommonsRequestLoggingFilter
の説明で、Before request でリクエストボディの情報が表示されなかったのは、キャッシュが実行されていないからです。
After request では、(おそらく)キャッシュが実行されているためリクエストボディの情報が表示されます。