모르지 않다는 것은 아는것과 다르다.

Spring Cloud

MDC (Mapped Diagnostic Context)

채마스 2022. 2. 28. 19:49

MDC 란?

  • MDC 는 java.util.Map 형식을 이용하여 클라이언트 특징적인 데이타를 저장하기 위한 메카니즘이다.
  • 요즘 웹 어플리케이션은 멀티 스레드로 동작한다. 그렇기 때문에 서블릿은 어떤 클라이언트에 호출되더라도 같은 로그 기록을 남기는데, 이렇게 할 경우에 오류가 발생했을 때 어떤 클라이언트를 담당하는 스레드에서 오류가 발생했는지 알기 힘듭니다.
  • 예를들어 서버 C에서 요청이 시작돼서 A에서 오류가 발생했다. 오류를 추적하기 어렵다. -> 그래서 requestID 등을 부여해서 로그를 적재시켜나가면 오류를 찾을 수 있을것이다.
  • 현재 log4j 및 logback 만 MDC기능을 제공하고 있다.
  • springboot 환경에서는 spring-boot-starter-web을 의존성으로 추가하면 logback이 기본으로 포함 되어있다.




MDC 클래스 메소드

// key 와 연관된 Object 를 얻음
public static Object get(String key); 

// key 와 연관된 Object 를 저장
public static void put(String key, Object value); 

// key 와 연관된 Object 매핑을 제거
public static void remove(String key);



구현

@Configuration
public class WebConfig {
    @Bean
    public FilterRegistrationBean getFilterRegistrationBean() {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean(new MDCLogFilter());
        registrationBean.setUrlPatterns(Arrays.asList("/v1/*"));
        return registrationBean;
    }
}
  • 위와 같이 WebConfig 파일에 MDCLogFilter 에 대한 설정을 한다.
    • /v1 하위로 오는 모든 요청에 MDCLogFilter 를 적용하도록 설정하였다.
@Slf4j
public class MDCLogFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;

        String requestId = UUID.randomUUID().toString();
        String requestUrl = req.getRequestURI();
        String userAgent = req.getHeader("User-Agent");
        String clientIP = getClientIP(req);

        MDC.put("requestId", requestId);
        MDC.put("requestUrl", requestUrl);
        MDC.put("userAgent", userAgent);
        MDC.put("clientIP", clientIP);

        chain.doFilter(req, response);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        LOG.info("MDC filter initialize");
    }

    @Override
    public void destroy() {
        MDC.clear();
    }


    private String getClientIP(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        LOG.info("ip: {}", ip);

        if(!StringUtils.hasLength(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}
  • 위와 같이 doFilter에서 UUID 로 requestId 를 할당받는다.
  • destroy()에서 MDC 로그를 clear 해준다.
public enum MDCKeys {
    /**
     *  MDC.put("requestId", requestId);
     *         MDC.put("requestUrl", requestUrl);
     *         MDC.put("userAgent", userAgent);
     *         MDC.put("clientIP", clientIP);
     */

    REQUEST_ID("requestId"),
    REQUEST_URL("requestUrl"),
    USER_AGENT("userAgent"),
    CLIENT_IP("clientIP");

    MDCKeys(String propertyKey) {
        this.propertyKey = propertyKey;
    }

    public String getPropertyKey() {
        return propertyKey;
    }

    private String propertyKey;
}
  • 위와 같이 MDCKeys 에 대한 enum 클래스를 정의한다.
@Slf4j
@RestController
public class MdcController {
    @GetMapping("/v1/mdc")
    public ResponseEntity getData() {
        String requestId = MDC.get(MDCKeys.REQUEST_ID.getPropertyKey());
        String clientIP = MDC.get(MDCKeys.CLIENT_IP.getPropertyKey());
        String userAgent = MDC.get(MDCKeys.USER_AGENT.getPropertyKey());

        LOG.info("requestId: {} clientIP: {} userAgent {}", requestId, clientIP, userAgent);

        return ResponseEntity.ok("ok");
    }
}



정리

  • 트래픽이 많고 별도 쓰레드를 만들어서 요청 건을 가져오는 경우에는 쓰레드 번호가 중복되어 로그를 찾기 어려운 점이 있고, 하드코딩으로 특정 키 값을 로그에 남기기에는 꽤 공수가 든다.
  • 하지만 위와 같이 MDC 를 적용한다면, 로그 추적의 효율성을 높일 수 있다.
  • 위에서는 Filter로 구현을 했지만, 당연히 Interceptor를 이용해서 구현할 수도 있다.




REFERENCES

'Spring Cloud' 카테고리의 다른 글

Retrofit  (0) 2022.04.09