일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- MySQL
- springboot
- tool
- javascript
- ReactJS
- IntelliJ
- Spring Batch
- 맛집
- jsp
- Design Patterns
- Gradle
- Spring
- java
- laravel
- jenkins
- Spring Boot
- AWS
- JVM
- devops
- php
- Git
- linux
- db
- redis
- 요리
- Web Server
- elasticsearch
- Oracle
- ubuntu
- it
Archives
- Today
- Total
아무거나
[Spring boot] MDC 를 활용한 Logback 에 Request Payload 항목 추가하여 로그 세분화 본문
Java & Kotlin/Spring
[Spring boot] MDC 를 활용한 Logback 에 Request Payload 항목 추가하여 로그 세분화
전봉근 2022. 11. 16. 13:03반응형
MDC(Mapped Diagnostic Context)는 현재 실행중인 쓰레드에 메타 정보를 넣고 관리하는 공간이다. MDC는 내부적으로 Map을 관리하고 있어 (Key, Value) 형태로 값을 저장할 수 있다.
Application 에서 로그를 남길시에 Request 관련 값들을 좀 더 상세하게 다루고 싶어 해당 포스팅을 작성하게 되었다. (Ex: url, parameter 등)
하기 코드만 그대로 사용하면 되므로 활용해보자.
https://github.com/bkjeon1614/java-example-code/tree/develop/bkjeon-mybatis-codebase/base-api
- 의존성 추가
[build.gradle]... // LoggingFilter (Spring Version 에 따라 기본으로 제공해주는 경우도 있음) implementation group: 'javax.ws.rs', name: 'javax.ws.rs-api', version: '2.0' implementation group: 'javax.annotation', name: 'javax.annotation-api', version: '1.3.2' ...
- 로깅 필터 클래스 작성
[LoggingFilter.java]package com.example.bkjeon.base.filter; import static javax.ws.rs.core.MediaType.*; import java.io.IOException; import java.util.Arrays; import java.util.List; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.MDC; import org.springframework.http.MediaType; import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; import org.springframework.util.StreamUtils; import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.util.ContentCachingResponseWrapper; import lombok.extern.slf4j.Slf4j; /** * 로깅 필터 * [MDC] * 실행 쓰레드들에 공통값을 주입하여 의미있는 정보를 추가해 로깅 할 수 있도록 제공 * ( Ex: 멀티 스레딩 환경시 실행되는 task 는 로그가 섞여 제대로 확인하기 힘들어서 * 스레드로컬 변수에 값을 할당하여 트래킹에 용이하게 만드나 매번 해당 값을 주입하기는 번거로워 logback, log4j 등 MDC 를 제공) * * [doFilterInternal] * doFilter 와 동일하지만 단일 요청 스레드 내에서 요청당 한 번만 호출되도록 보장된다. */ @Slf4j @Component public class LoggingFilter extends OncePerRequestFilter { @Override protected void doFilterInternal( HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable FilterChain filterChain ) throws ServletException, IOException { MDC.put("method", request.getMethod()); MDC.put("uri", request.getQueryString() == null ? request.getRequestURI() : request.getRequestURI() + "?" + request.getQueryString()); if (filterChain != null) { doFilterWrapped(new RequestWrapper(request), new ResponseWrapper(response), filterChain); } MDC.clear(); } protected void doFilterWrapped( RequestWrapper request, ContentCachingResponseWrapper response, FilterChain filterChain ) throws ServletException, IOException { try { logRequest(request); filterChain.doFilter(request, response); } finally { response.copyBodyToResponse(); } } private static void logRequest(RequestWrapper request) throws IOException { boolean mediaTypeChk = isMediaType(MediaType.valueOf(request.getContentType() == null ? APPLICATION_JSON : request.getContentType())); // inputStream 을 byte 배열로 반환 byte[] content = StreamUtils.copyToByteArray(request.getInputStream()); if (mediaTypeChk && content.length > 0) { MDC.put("payload", new String(content)); } } private static boolean isMediaType(MediaType mediaType) { final List<MediaType> mediaTypeList = Arrays.asList( MediaType.valueOf("text/*"), MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.valueOf("application/*+json"), MediaType.valueOf("application/*+xml"), MediaType.MULTIPART_FORM_DATA ); return mediaTypeList.stream().anyMatch(visibleType -> visibleType.includes(mediaType)); } }
- 요청된 HTTP 접근 클래스 작성
[RequestWrapper.java]package com.example.bkjeon.base.filter; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import org.springframework.util.StreamUtils; /** * 요청된 HTTP 접근 * [HttpServletRequestWrapper] * Servlet 관련 인터페이스 제공 */ public class RequestWrapper extends HttpServletRequestWrapper { private final byte[] cachedInputStream; public RequestWrapper(HttpServletRequest request) throws IOException { super(request); InputStream requestInputStream = request.getInputStream(); this.cachedInputStream = StreamUtils.copyToByteArray(requestInputStream); } /** * binary data로 Request Body 정보를 담은 ServletInputStream(inputstream)을 반환한다. * @return */ @Override public ServletInputStream getInputStream() { return new ServletInputStream() { private final InputStream cachedBodyInputStream = new ByteArrayInputStream(cachedInputStream); @Override public boolean isFinished() { try { // 더 이상 읽을 byte 가 없을 때 return return cachedBodyInputStream.available() == 0; } catch (IOException e) { e.printStackTrace(); } return false; } @Override public boolean isReady() { return true; } @Override public void setReadListener(ReadListener readListener) { throw new UnsupportedOperationException(); } @Override public int read() throws IOException { return cachedBodyInputStream.read(); } }; } }
- HTTP 응답 캐싱 클래스 작성
[ResponseWrapper.java]package com.example.bkjeon.base.filter; import javax.servlet.http.HttpServletResponse; import org.springframework.web.util.ContentCachingResponseWrapper; /** * HTTP 응답 캐싱 * [ContentCachingResponseWrapper] * httpServletRequest의 getInputStream()은 한번 밖에 사용 못하므로 ContentCachingResponseWrapper 를 사용해야함 * ContentCachingResponseWrapper 는 출력 스트림 및 기록된 모든 콘텐츠를 캐시하고 바이트 배열을 통해 이 콘텐츠를 검색할 수 있도록 하는 * HttpServletResponse Wrapper 이다. */ public class ResponseWrapper extends ContentCachingResponseWrapper { public ResponseWrapper(HttpServletResponse response) { super(response); } }
- MDC 에 적용된 logback pattern 항목값 추가 ([Request: %X{method} uri=%X{uri} payload=%X{payload}])
[logback-spring.xml]... <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender"> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern> %d{yyyy-MM-dd HH:mm:ss} [%thread] [Request: %X{method} uri=%X{uri} payload=%X{payload}] %-5level %logger{36} - %msg %n </Pattern> </layout> </appender> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <encoder> <pattern>[%d{yyyy-MM-dd HH:mm:ss}:%-3relative][%thread]64 [Request: %X{method} uri=%X{uri} payload=%X{payload}] %-5level %logger{35} - %msg%n</pattern> </encoder> <file>${LOG_FILE_APPLICATION}</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${LOG_FILE_APPLICATION}.%d{yyyyMMdd}</fileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> </appender> ...
끝.
반응형
'Java & Kotlin > Spring' 카테고리의 다른 글
SpringBoot + Zipkin 을 활용한 트레이스 환경 구성 (0) | 2023.01.09 |
---|---|
Spring Boot 에서 FeignClient 를 사용하여 Rest API 를 간편하게 사용하자 (0) | 2022.12.26 |
Spring Batch 5편 - Scope 와 Job Parameter (0) | 2022.11.02 |
SpEL(Spring Expression Language) (0) | 2022.10.11 |
POJO 란 (0) | 2022.09.06 |
Comments