로그를 개조해보겠다.
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등을 출력해서 확인이 가능하도록 했다.
'Back-End > JAVA' 카테고리의 다른 글
JwtToken 심화과정 (0) | 2024.12.12 |
---|---|
[JAVA] SXSSF 엑셀파일 전송 API (1) | 2024.11.20 |
Spring 프로젝트 시작하기 - 로그인편 (2) | 2024.11.19 |
Spring 프로젝트 시작하기 - 보안 ) Access Token과 Refresh Token (1) | 2024.11.18 |
Spring 프로젝트 시작하기 - 보안 ) Jwt Token 쿠키 예외처리 (1) | 2024.11.15 |