개발 TIP

restTemplate LogInterceptor

앵낄낄 2022. 5. 3. 21:20
반응형

restTemplate 통신 시 내가 원하는 형태로 로깅을 할 수 있는 방법을 알아보려고 합니다.

restTemplate에서는 Interceptor 기능이 존재하는데 말그대로 중간에 가로채서 다른 행위를 하고플 때 작용되는 구간입니다.

Interceptor의 사용 사례는 다음과 같습니다.

  • 요청 및 응답 로깅
  • 구성 가능한 백 오프 전략으로 요청 재시도
  • 특정 요청 매개 변수를 기반으로 요청 거부
  • 요청 URL 주소 변경

restTemplate 과정에서 Interceptor가 어느 구간에서 캐치되는지는 정확하게 분석하진 못함..

[restTemplate Configuration]

 

@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {

        CloseableHttpClient httpClient = HttpClientBuilder.create()
                .setMaxConnTotal(120)   //연결을 유지할 최대 숫자
                .setMaxConnPerRoute(100)    //특정 경로당 최대 숫자
                .setConnectionTimeToLive(5, TimeUnit.SECONDS)   // keep - alive
                .build();

        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        factory.setHttpClient(httpClient);  //HttpComponentsClientHttpRequestFactory 생성자에 주입

        //인터셉터가 요청 / 응답 로거로서 기능하도록하려면 인터셉터가 처음으로, 클라이언트가 두 번째로 두 번 읽어야한다.
        //기본 구현에서는 응답 스트림을 한 번만 읽을 수 있습니다.
        // 이러한 특정 시나리오를 제공하기 위해 Spring은 BufferingClientHttpRequestFactory 라는 특수 클래스를 제공.
        // 이름에서 알 수 있듯이이 클래스는 여러 용도로 JVM 메모리에서 요청 / 응답을 버퍼링합니다.
        BufferingClientHttpRequestFactory bufferingClientHttpRequestFactory = new BufferingClientHttpRequestFactory(factory);

        return restTemplateBuilder
                .requestFactory(() -> bufferingClientHttpRequestFactory)
                .setConnectTimeout(Duration.ofMillis(5000)) //읽기시간초과, ms
                .setReadTimeout(Duration.ofMillis(5000))    //연결시간초과, ms
//                .additionalInterceptors(new RequestResponseLoggingInterceptor()) // 여기서 설정해도 되지만 Option으로 on, off가 가능하게끔 할 예정
                .build();
    }
}

[restTemplate Interceptor 적용]

public abstract class AbstractApiClient implements InitializingBean, ApplicationContextAware {

    private ApplicationContext ctx;

    @Override
    public void setApplicationContext(ApplicationContext ctx) throws BeansException {
        this.ctx = ctx;
    }

....
....
.... 생략
....

    /**
     * restTemplate customize 한다.
     */
    @Override
    public void afterPropertiesSet() {
				// request 및 response 를 일괄 logging 을 남긴다
        List<ClientHttpRequestInterceptor> interceptors = this.restTemplate.getInterceptors();
        ClientHttpRequestInterceptor logInterceptor = new RestClientLogInterceptor();
        log.info("### [{}] logging interceptor enable : [{}]", this.getClass().getSimpleName(), logInterceptor.hashCode());
        interceptors.add(logInterceptor);
    }
}

로그를 남기는 ClientHttpRequestInterceptor 구현체는 아래와 같습니다.

@Slf4j
public class RestClientLogInterceptor implements ClientHttpRequestInterceptor {

    public RestClientLogInterceptor() {
        try {
            host = InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            host = "unknown";
        }
    }

    private String host;

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        LocalDateTime start = LocalDateTime.now();
        StringBuilder sb = new StringBuilder();
        sb.append("\n==== API CALL BEGIN ====\n");
        ClientHttpResponse response;
        try {
            sb.append(" * HOST = ").append(host).append("\n");
            sb.append(" * LogKey = ").append(LogKey.get()).append("\n");
            sb.append(" * URI = ").append(request.getMethod()).append(" ").append(request.getURI()).append("\n");
            sb.append(" * HEADER = ").append(request.getHeaders()).append("\n");
            sb.append(" * BODY = ").append(new String(body, Charset.defaultCharset())).append("\n");
            response = execution.execute(request, body);
        } catch (Exception e) {
            sb.append(" == RESPONSE ERROR ====\n");
            sb.append(" * ERROR = ").append(e).append("\n");
            sb.append(" * DURATION = ").append(Duration.between(start, LocalDateTime.now()).toMillis()).append(" (ms) \n");
            sb.append("==== API CALL END ====\n");
            // Exception 발생시 로깅을 마무리 하고 rethrow 한다.
            log.warn(sb.toString());
            throw e;
        }

        sb.append(" == RESPONSE  ====\n");
        sb.append(" * STATUS = ").append(response.getStatusCode()).append("\n");
        sb.append(" * BODY = ").append(Objects.nonNull(response.getBody()) ? MvcUtils.inputStreamToString(response.getBody()) : "EMPTY");
        sb.append(" * DURATION = ").append(Duration.between(start, LocalDateTime.now()).toMillis()).append(" (ms) \n");
        sb.append("==== API CALL END ====\n");

        log.info(sb.toString());
        return response;
    }
}
반응형