2021/개발

WebFlux Log Tracing

mjin.park 2021. 12. 4. 17:40

WebFlux를 처음 사용해보면서 로깅하는데 며칠을 애먹었다.

Spring MVC와는 다르게 레퍼런싱할 수 있는 자료들이 거의 없었다. 그래서 다른 분들의 시간을 단축시켜주고자 글을 적어본다.

(좀 더 좋은 자료가 있는 곳이 있다면 댓글로 공유부탁드립니다 🙇)

 

결론부터 말하자면,

WebFlux에서 MDC Context로 로깅을 '잘' 하려면 아래와 같이 HttpHandlerDecoratorFactory의 구현체를 하나 만들면 된다.

package com.tistory.mjin1220.decorator;

import org.slf4j.MDC;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.HttpHandlerDecoratorFactory;
import org.springframework.stereotype.Component;
import reactor.util.context.Context;

import java.util.UUID;

@Component
public class CustomHttpHandlerDecorator implements HttpHandlerDecoratorFactory {

    private static final String MDC_KEY_TRACE_ID = "traceId";

    @Override
    public HttpHandler apply(HttpHandler httpHandler) {
        return (request, response) ->
                httpHandler.handle(request, response)
                           .contextWrite(context -> {
                               final String traceId = getTraceId();
                               MDC.put(MDC_KEY_TRACE_ID, traceId);
                               return Context.of(MDC_KEY_TRACE_ID, traceId);
                           });
    }

    private String getTraceId() {
        return UUID.randomUUID()
                   .toString();
    }
}

위와 같이 설정을 해주면 WebFilter에서 로직을 처리하다가 Exception이 발생하는 케이스에서도 커버가 된다.

WebFlux에서 AbstractErrorWebExceptionHandler를 이용해서 Exception을 처리하게 되는데, 이 때 WebFilter에서 아래와 같이 처리해주는 케이스로는 커버가 되지 않았다.

        return chain.filter(exchange)
                    .contextWrite(context -> {
                        final String traceId = getTraceId();
                        MDC.put(MDC_KEY_TRACE_ID, traceId);
                        return Context.of(MDC_KEY_TRACE_ID, traceId);
                    });

여기에서 내부적으로 WebFlux를 단일 ThreadLocal에서만 처리하는 경우는 드물다.

그러면 매번 컨텍스트 스위칭이 일어날 때마다 MDC에 있는 데이터들을 Context로 넘겨주는 것은 매우 귀찮은 일이다. (또는 Context에 있는 데이터들을 MDC로)

package com.tistory.mjin1220.config;

import lombok.RequiredArgsConstructor;
import org.reactivestreams.Subscription;
import org.slf4j.MDC;
import org.springframework.context.annotation.Configuration;
import reactor.core.CoreSubscriber;
import reactor.core.publisher.Hooks;
import reactor.core.publisher.Operators;
import reactor.util.context.Context;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @link https://www.novatec-gmbh.de/en/blog/how-can-the-mdc-context-be-used-in-the-reactive-spring-applications/
 */
@Configuration
public class MdcContextLifterConfig {

    public static final String MDC_CONTEXT_REACTOR_KEY = MdcContextLifterConfig.class.getName();

    @PostConstruct
    @SuppressWarnings({"unchecked", "rawtypes"})
    public void contextOperatorHook() {
        Hooks.onEachOperator(MDC_CONTEXT_REACTOR_KEY, Operators.lift((scannable, subscriber) -> new MdcContextLifter(subscriber)));
    }

    @PreDestroy
    public void cleanupHook() {
        Hooks.resetOnEachOperator(MDC_CONTEXT_REACTOR_KEY);
    }

    /**
     * Helper that copies the state of Reactor [Context] to MDC on the #onNext function.
     */
    @RequiredArgsConstructor
    public static class MdcContextLifter<T> implements CoreSubscriber<T> {

        private final CoreSubscriber<T> coreSubscriber;

        @Override
        public void onSubscribe(Subscription subscription) {
            coreSubscriber.onSubscribe(subscription);
        }

        @Override
        public void onNext(T t) {
            copyToMdc(coreSubscriber.currentContext());
            coreSubscriber.onNext(t);
        }

        @Override
        public void onError(Throwable throwable) {
            copyToMdc(coreSubscriber.currentContext());
            coreSubscriber.onError(throwable);
        }

        @Override
        public void onComplete() {
            coreSubscriber.onComplete();
        }

        @Override
        public Context currentContext() {
            return coreSubscriber.currentContext();
        }

        /**
         * Extension function for the Reactor [Context]. Copies the current context to the MDC, if context is empty clears the MDC.
         * State of the MDC after calling this method should be same as Reactor [Context] state.
         * One thread-local access only.
         */
        void copyToMdc(Context context) {
            if (context != null && !context.isEmpty()) {
                Map<String, String> map = context.stream()
                                                 .collect(Collectors.toMap(e -> e.getKey()
                                                                                 .toString(), e -> e.getValue()
                                                                                                    .toString()));

                MDC.setContextMap(map);
            } else {
                MDC.clear();
            }
        }
    }
}

※ Reference

반응형

'2021 > 개발' 카테고리의 다른 글

SSO  (0) 2021.10.10
OAuth 2.0  (0) 2021.10.10
Spring Batch - 기본 프로젝트 만들기  (0) 2021.05.01
ELK - Elasticsearch 설치  (0) 2021.04.27
Spring Boot - An illegal reflective access operation has occurred  (0) 2021.04.24