Spring 프로젝트 시작하기 - 로그편

로그를 개조해보겠다.
1. log4j2 라이브러리 추가

dependencies {
	...
    // log4j2
	implementation 'org.springframework.boot:spring-boot-starter-log4j2'
	testImplementation 'org.springframework.boot:spring-boot-starter-log4j2'
}

 

 

2. log4j2 로그 양식규정

해당 xml 파일은 /src/main/resources/log4j2.xml 경로에 생성했다.

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO">
    <!--    해당 설정파일에서 사용하는 프로퍼티-->
    <Properties>
        <Property name="logName">stock</Property>
        <Property name="layoutPattern">%d{yyyy-MM-dd HH:mm:ss.SSSS} [%t] %highlight{%-6p}{FATAL=bg_red, ERROR=red, WARN=yellow, INFO=green, DEBUG=blue} [%-10X{X-USER-ID}] [%logger{0}:%line] %msg%n</Property>
    </Properties>
    <!--    LogEvent를 전달해주는 Appender-->
    <Appenders>
        <Console name="Console_Appender" target="SYSTEM_OUT">
            <PatternLayout pattern="${layoutPattern}"/>
        </Console>
        <RollingFile name="File_Appender" fileName="logs/${logName}.log" filePattern="logs/${logName}_%d{yyyy-MM-dd}_%i.log.gz">
            <PatternLayout pattern="${layoutPattern}"/>
            <Policies>
                <SizeBasedTriggeringPolicy size="200KB"/>
                <TimeBasedTriggeringPolicy interval="1"/>
            </Policies>
            <DefaultRolloverStrategy max="10" fileIndex="min"/>
        </RollingFile>
    </Appenders>

    <!--    실제 Logger-->
    <Loggers>
        <Root level="INFO" additivity="false">
            <AppenderRef ref="Console_Appender"/>
            <AppenderRef ref="File_Appender"/>
        </Root>
        <Logger name="org.springframework" level="DEBUG"
                additivity="false">
            <AppenderRef ref="Console_Appender" />
            <AppenderRef ref="File_Appender"/>
        </Logger>
        <Logger name="com.fucct" level="INFO" additivity="false">
            <AppenderRef ref="Console_Appender" />
            <AppenderRef ref="File_Appender"/>
        </Logger>
        <Logger name="com.fucct.springlog4j2.loggertest" level="TRACE" additivity="false">
            <AppenderRef ref="Console_Appender" />
        </Logger>
    </Loggers>
</Configuration>

 

로그 형식은 layoutPattern라는 이름의 프로퍼티로 지정해주었다.

한행에 로그를 어떻게 출력해줄지 구성하는 부분이다.

%d{yyyy-MM-dd HH:mm:ss.SSSS} [%t] %highlight{%-6p}{FATAL=bg_red, ERROR=red, WARN=yellow, INFO=green, DEBUG=blue} [%-10X{X-USER-ID}] [%logger{0}:%line] %msg%n

 

 

- %d{yyyy-MM-dd HH:mm:ss.SSSS}  :  로그 생성 date/time

- [%t] : thred 이름을 찍었다.

- %highlight{%-6p}{FATAL=bg_red, ERROR=red, WARN=yellow, INFO=green, DEBUG=blue}  :  위험도 표시

- [%-10X{X-USER-ID}]  :  유저 ID 표시

- [%logger{0}:%line]  :  로그가 찍힌 위치 표시

- %msg  :  메세지 내용

 

나머지는 변수로 맵핑해서 호출이 가능한데,

유저 ID와 같은 데이터는 따로 지정을 해주어야한다.

 

3. MDC 데이터 저장

import org.slf4j.MDC;


@Slf4j
@Component
public class TokenAuthInterceptor implements HandlerInterceptor {
    public static final String USER_ID = "X-USER-ID";
    
    ...
    
    
	MDC.put(USER_ID, tokenUser.getUId()); // 로그 출력시에 USER_ID값을 사용할 수 있도록 저장한다.
    ...
    
}

이런식으로 slf4j의 MDC를 import해서 데이터를 넣어주면 Log단에서 값을 잘 뽑아서 쓴다.

 

4. LogFilter 생성

API가 호출될때마다 일정한 로그를 출력한다.

package org.debugggggger.stock.common.middleware.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;

import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Optional;
import java.util.UUID;

@Component
@Slf4j
public class LogFilter extends OncePerRequestFilter {
    public static String getClientIp(HttpServletRequest request) {
        String[] IP_HEADERS = {
                "X-Forwarded-For",
                "Proxy-Client-IP",
                "WL-Proxy-Client-IP",
                "HTTP_CLIENT_IP",
                "HTTP_X_FORWARDED_FOR",
                "X-Real-IP",
                "X-RealIP",
                "REMOTE_ADDR"
        };

        for (String header : IP_HEADERS) {
            String ip = request.getHeader(header);
            if ((ip != null) && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
                return ip;
            }
        }

        return request.getRemoteAddr();
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
        ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
        try {
            // API 호출마다 Thred의 이름을 6글자의 랜덤키로 변환
            Thread.currentThread().setName(generateRandomKey());
            String requestURIWithQueryString = Optional.ofNullable(requestWrapper.getRequestURI())
                    .map(uri -> uri + Optional.ofNullable(requestWrapper.getQueryString()).map(qs -> "?" + qs).orElse(""))
                    .orElse("");

            // API 호출의 도입부
            log.info("--> {} {}", requestWrapper.getMethod(), requestURIWithQueryString);
            log.debug("Request IP : {}", getClientIp(requestWrapper));
            String requestBody = new String(requestWrapper.getContentAsByteArray(), StandardCharsets.UTF_8);
            log.info("request body: {}", requestBody);

            filterChain.doFilter(requestWrapper, response);

        } catch (Exception e) {
            log.error("doFilter() : ", e);
        }
    }

    private String generateRandomKey(){
        String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
        int LENGTH = 6;
        SecureRandom random = new SecureRandom();
        StringBuilder key = new StringBuilder(LENGTH);
        for (int i = 0; i < LENGTH; i++) {
            int index = random.nextInt(CHARACTERS.length());
            key.append(CHARACTERS.charAt(index));
        }
        return key.toString();
    }
}

 

thread 이름은 6자리의 랜덤한 값으로 키값을 만들어주었다.

API가 호출될때에도 요청 URI, Method, request body, IP등을 출력해서 확인이 가능하도록 했다.