spring webclient log

27 December 2019

spring boot webclient에서 request와 response를 logging하는 방법을 알아보도록 하겠습니다.
먼저 아래와 같이 logSupport가 가능하게 webClient를 설정해주도록 합니다.

package org.shashaka.io;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.channel.BootstrapHandlers;
import reactor.netty.http.client.HttpClient;

@Configuration
public class WebClientConfig {

    @Bean
    public WebClient webClient() {
        return WebClient
                .builder()
                .clientConnector(new ReactorClientHttpConnector(HttpClient
                        .create()
                        .tcpConfiguration(
                                tc -> tc.bootstrap(
                                        b -> BootstrapHandlers.updateLogSupport(b, new CustomLogger(HttpClient.class))))))
                .build();
    }
}
그리고 아래와 같이 request와 response 호출시에 log가 되게 설정해주도록 합니다.
같은 request와 response를 구분하기 위해 context에서 channel session id를 같이 출력해줍니다.
package org.shashaka.io;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import java.net.SocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

@Slf4j
public class CustomLogger extends LoggingHandler {
    public CustomLogger(Class<?> clazz) {
        super(clazz);
    }

    @Override
    protected String format(ChannelHandlerContext ctx, String event, Object arg) {
        if (arg instanceof ByteBuf) {
            ByteBuf msg = (ByteBuf) arg;
            return msg.toString(StandardCharsets.UTF_8);
        }
        return super.format(ctx, event, arg);
    }

    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {

        ctx.fireChannelRegistered();
    }

    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {


        ctx.fireChannelUnregistered();
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {


        ctx.fireChannelActive();
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {

        ctx.fireChannelInactive();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {

        ctx.fireExceptionCaught(cause);
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {

        ctx.fireUserEventTriggered(evt);
    }

    @Override
    public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) throws Exception {

        ctx.bind(localAddress, promise);
    }

    @Override
    public void connect(
            ChannelHandlerContext ctx,
            SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) throws Exception {

        ctx.connect(remoteAddress, localAddress, promise);
    }

    @Override
    public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {

        ctx.disconnect(promise);
    }

    @Override
    public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {

        ctx.close(promise);
    }

    @Override
    public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {

        ctx.deregister(promise);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.fireChannelReadComplete();
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        log.info("postlog [{}] : {}", ctx.channel().id(), ((ByteBuf) msg).toString(StandardCharsets.UTF_8));

        ctx.fireChannelRead(msg);
    }

    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        String content = ((ByteBuf) msg).toString(StandardCharsets.UTF_8);
        if (!StringUtils.isEmpty(content)) {
            log.info(" prelog [{}] : {}", ctx.channel().id(), content);
        }
        ctx.write(msg, promise);
    }

    @Override
    public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
        ctx.fireChannelWritabilityChanged();
    }

    @Override
    public void flush(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }

}
아래와 같이 log가 출력되는 것을 확인할 수 있습니다.
23:04:39.032 [reactor-http-nio-1] INFO  org.shashaka.io.CustomLogger -  prelog [8d5c8ff2] : POST /v2/5e0600593300004900ec5c2c HTTP/1.1
user-agent: ReactorNetty/0.9.2.RELEASE
host: www.mocky.io
accept: */*
Content-Type: text/plain;charset=UTF-8
content-length: 18

{"request":"test"}
23:04:39.376 [reactor-http-nio-1] INFO  org.shashaka.io.CustomLogger - postlog [8d5c8ff2] : HTTP/1.1 201 Created
Server: Cowboy
Connection: keep-alive
Date: Fri, 27 Dec 2019 14:04:39 GMT
Content-Type: application/json
Content-Length: 15
Via: 1.1 vegur

{"test":"test"}

설정은 서버의 용도에 따라 변경해주도록 합니다.