Skip to content

Commit 5fbbb75

Browse files
authored
Merge pull request #136 from YAPP-Github/feat/PRODUCT-201
[Feat] 로깅 전략 수립 및 적용
2 parents 7097a34 + fca80c6 commit 5fbbb75

File tree

7 files changed

+395
-15
lines changed

7 files changed

+395
-15
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package eatda.config;
2+
3+
import jakarta.servlet.http.HttpServletRequest;
4+
import jakarta.servlet.http.HttpServletResponse;
5+
import java.util.UUID;
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
import org.slf4j.MDC;
9+
import org.springframework.stereotype.Component;
10+
import org.springframework.web.servlet.HandlerInterceptor;
11+
12+
@Component
13+
public class LoggingInterceptor implements HandlerInterceptor {
14+
15+
private static final Logger log = LoggerFactory.getLogger(LoggingInterceptor.class);
16+
private static final String START_TIME = "startTime";
17+
private static final String REQUEST_ID = "requestId";
18+
19+
@Override
20+
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
21+
request.setAttribute(START_TIME, System.currentTimeMillis());
22+
String requestId = UUID.randomUUID().toString().substring(0, 8);
23+
MDC.put(REQUEST_ID, requestId);
24+
25+
log.info("[Request] {} {}", request.getMethod(), request.getRequestURI());
26+
return true;
27+
}
28+
29+
@Override
30+
public void afterCompletion(
31+
HttpServletRequest request,
32+
HttpServletResponse response,
33+
Object handler,
34+
Exception ex
35+
) {
36+
Long startTime = (Long) request.getAttribute(START_TIME);
37+
if (startTime == null) {
38+
log.warn("[Response] {} {} (duration unknown - preHandle not called)",
39+
request.getMethod(), request.getRequestURI());
40+
MDC.clear();
41+
return;
42+
}
43+
44+
long duration = System.currentTimeMillis() - startTime;
45+
log.info("[Response] {} {} ({}ms)", request.getMethod(), request.getRequestURI(), duration);
46+
MDC.clear();
47+
}
48+
}

src/main/java/eatda/config/WebConfig.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,24 @@
66
import lombok.RequiredArgsConstructor;
77
import org.springframework.context.annotation.Configuration;
88
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
9+
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
910
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
1011

1112
@Configuration
1213
@RequiredArgsConstructor
1314
public class WebConfig implements WebMvcConfigurer {
1415

1516
private final JwtManager jwtManager;
17+
private final LoggingInterceptor loggingInterceptor;
1618

1719
@Override
1820
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
1921
argumentResolvers.add(new AuthMemberArgumentResolver(jwtManager));
2022
}
23+
24+
@Override
25+
public void addInterceptors(InterceptorRegistry registry) {
26+
registry.addInterceptor(loggingInterceptor)
27+
.addPathPatterns("/**");
28+
}
2129
}

src/main/java/eatda/exception/GlobalExceptionHandler.java

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,69 +24,70 @@ public class GlobalExceptionHandler {
2424

2525
@ExceptionHandler(BindException.class)
2626
public ResponseEntity<ErrorResponse> handleBindingException(BindException exception) {
27-
return toErrorResponse(EtcErrorCode.CLIENT_REQUEST_ERROR);
27+
return toErrorResponse(EtcErrorCode.CLIENT_REQUEST_ERROR, exception);
2828
}
2929

3030
@ExceptionHandler(ConstraintViolationException.class)
3131
public ResponseEntity<ErrorResponse> handleConstraintViolationException(ConstraintViolationException exception) {
32-
return toErrorResponse(EtcErrorCode.CLIENT_REQUEST_ERROR);
32+
return toErrorResponse(EtcErrorCode.CLIENT_REQUEST_ERROR, exception);
3333
}
3434

3535
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
3636
public ResponseEntity<ErrorResponse> handleMethodArgumentTypeMismatchException(
3737
MethodArgumentTypeMismatchException exception) {
38-
return toErrorResponse(EtcErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH);
38+
return toErrorResponse(EtcErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH, exception);
3939
}
4040

4141
@ExceptionHandler(ClientAbortException.class)
4242
public ResponseEntity<ErrorResponse> handleClientAbortException(ClientAbortException exception) {
43-
return toErrorResponse(EtcErrorCode.ALREADY_DISCONNECTED);
43+
log.warn("[ClientAbortException] {}: {}", exception.getClass().getSimpleName(), exception.getMessage());
44+
return toErrorResponse(EtcErrorCode.ALREADY_DISCONNECTED, null);
4445
}
4546

4647
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
4748
public ResponseEntity<ErrorResponse> handleHttpRequestMethodNotSupportedException(
4849
HttpRequestMethodNotSupportedException exception
4950
) {
50-
return toErrorResponse(EtcErrorCode.METHOD_NOT_SUPPORTED);
51+
return toErrorResponse(EtcErrorCode.METHOD_NOT_SUPPORTED, exception);
5152
}
5253

5354
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
5455
public ResponseEntity<ErrorResponse> handleHttpMediaTypeNotSupportedException(
5556
HttpMediaTypeNotSupportedException exception
5657
) {
57-
return toErrorResponse(EtcErrorCode.MEDIA_TYPE_NOT_SUPPORTED);
58+
return toErrorResponse(EtcErrorCode.MEDIA_TYPE_NOT_SUPPORTED, exception);
5859
}
5960

6061
@ExceptionHandler(NoResourceFoundException.class)
6162
public ResponseEntity<ErrorResponse> handleNoResourceFoundException(NoResourceFoundException exception) {
62-
return toErrorResponse(EtcErrorCode.NO_RESOURCE_FOUND);
63+
return toErrorResponse(EtcErrorCode.NO_RESOURCE_FOUND, exception);
6364
}
6465

6566
@ExceptionHandler(MissingRequestCookieException.class)
6667
public ResponseEntity<ErrorResponse> handleMissingRequestCookieException(MissingRequestCookieException exception) {
67-
return toErrorResponse(EtcErrorCode.NO_COOKIE_FOUND);
68+
return toErrorResponse(EtcErrorCode.NO_COOKIE_FOUND, exception);
6869
}
6970

7071
@ExceptionHandler(MissingRequestHeaderException.class)
7172
public ResponseEntity<ErrorResponse> handleMissingRequestHeaderException(MissingRequestHeaderException exception) {
72-
return toErrorResponse(EtcErrorCode.NO_HEADER_FOUND);
73+
return toErrorResponse(EtcErrorCode.NO_HEADER_FOUND, exception);
7374
}
7475

7576
@ExceptionHandler(MissingServletRequestParameterException.class)
7677
public ResponseEntity<ErrorResponse> handleMissingServletRequestParameterException(
7778
MissingServletRequestParameterException exception) {
78-
return toErrorResponse(EtcErrorCode.NO_PARAMETER_FOUND);
79+
return toErrorResponse(EtcErrorCode.NO_PARAMETER_FOUND, exception);
7980
}
8081

8182
@ExceptionHandler(HandlerMethodValidationException.class)
8283
public ResponseEntity<ErrorResponse> handleHandlerMethodValidationException(
8384
HandlerMethodValidationException exception) {
84-
return toErrorResponse(EtcErrorCode.VALIDATION_ERROR);
85+
return toErrorResponse(EtcErrorCode.VALIDATION_ERROR, exception);
8586
}
8687

8788
@ExceptionHandler(BusinessException.class)
8889
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException exception) {
89-
log.error("[BusinessException] handled: {}", exception.getErrorCode());
90+
log.warn("[BusinessException] Code: {}, Message: {}", exception.getErrorCode(), exception.getMessage());
9091
ErrorResponse response = new ErrorResponse(exception.getErrorCode());
9192
return ResponseEntity.status(exception.getStatus())
9293
.body(response);
@@ -95,10 +96,17 @@ public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e
9596
@ExceptionHandler(Exception.class)
9697
public ResponseEntity<ErrorResponse> handleException(Exception exception) {
9798
log.error("[Unhandled Exception] {}: {}", exception.getClass().getSimpleName(), exception.getMessage(), exception);
98-
return toErrorResponse(EtcErrorCode.INTERNAL_SERVER_ERROR);
99+
return toErrorResponse(EtcErrorCode.INTERNAL_SERVER_ERROR, null);
99100
}
100101

101-
private ResponseEntity<ErrorResponse> toErrorResponse(EtcErrorCode errorCode) {
102+
private ResponseEntity<ErrorResponse> toErrorResponse(EtcErrorCode errorCode, Exception exception) {
103+
if (exception != null) {
104+
if (errorCode.getStatus().is4xxClientError()) {
105+
log.warn("[Client Error] {}: {}", exception.getClass().getSimpleName(), exception.getMessage());
106+
} else {
107+
log.error("[Server Error] {}: {}", exception.getClass().getSimpleName(), exception.getMessage(), exception);
108+
}
109+
}
102110
return ResponseEntity.status(errorCode.getStatus())
103111
.body(new ErrorResponse(errorCode));
104112
}

src/main/resources/db/seed/dev/V2__dev_init_data.sql

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ VALUES (1, 1, 1, '정말 맛있어요! 강추합니다!', 'cheer/dummy/1.jpg', t
3434
(7, 7, 7, '패스트푸드가 빠르고 맛있어요!', 'cheer/dummy/7.jpg', false);
3535

3636
INSERT INTO article (id, title, subtitle, article_url, image_key)
37-
VALUES (1, '첫 번째 기사', '서브타이틀 1', 'https://example.com/article1', 'article/dummy/1.jpg'),
37+
VALUES (1, '미식가를 위한 수제 아이스크림 가게 🍨', '센프란시스코에서 영감을 얻은 펠엔콜 사장님의 이야기',
38+
'https://ultra-wallet-037.notion.site/240b6292d5398127a630fabaa9dcd80d?pvs=74',
39+
'article/dummy/1.jpg'),
3840
(2, '두 번째 기사', '서브타이틀 2', 'https://example.com/article2', 'article/dummy/2.jpg'),
3941
(3, '세 번째 기사', '서브타이틀 3', 'https://example.com/article3', 'article/dummy/3.jpg');
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<configuration>
3+
4+
<conversionRule conversionWord="clr" class="org.springframework.boot.logging.logback.ColorConverter"/>
5+
6+
<property name="CONSOLE_PATTERN"
7+
value="%d{yyyy-MM-dd HH:mm:ss.SSS} %white([%thread]) %clr([%p]) %magenta([%X{requestId}]) %blue(%logger{5}) - %msg %n"/>
8+
<property name="ROLLING_PATTERN"
9+
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-5level] [%X{requestId}] %logger{5} - %msg %n"/>
10+
11+
<property name="LOG_DIR" value="${LOG_DIR:-${user.home}/logs/eatda}"/>
12+
<property name="FILE_PATH_NAME" value="${LOG_DIR}/eatda.log"/>
13+
<property name="LOG_NAME_PATTERN" value="${LOG_DIR}/eatda-%d{yyyy-MM-dd}.%i.log"/>
14+
<property name="MAX_FILE_SIZE" value="10MB"/>
15+
<property name="TOTAL_SIZE" value="100MB"/>
16+
<property name="MAX_HISTORY" value="7"/>
17+
18+
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
19+
<encoder>
20+
<pattern>${CONSOLE_PATTERN}</pattern>
21+
</encoder>
22+
</appender>
23+
24+
<appender name="ROLLING_LOG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
25+
<encoder>
26+
<pattern>${ROLLING_PATTERN}</pattern>
27+
</encoder>
28+
<file>${FILE_PATH_NAME}</file>
29+
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
30+
<fileNamePattern>${LOG_NAME_PATTERN}</fileNamePattern>
31+
<maxHistory>${MAX_HISTORY}</maxHistory>
32+
<maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
33+
<totalSizeCap>${TOTAL_SIZE}</totalSizeCap>
34+
</rollingPolicy>
35+
</appender>
36+
37+
<springProfile name="local, dev">
38+
<root level="INFO">
39+
<appender-ref ref="CONSOLE"/>
40+
<appender-ref ref="ROLLING_LOG_FILE"/>
41+
</root>
42+
</springProfile>
43+
44+
<springProfile name="prod">
45+
<property name="LOG_NAME_PATTERN" value="${LOG_DIR}/eatda-%d{yyyy-MM-dd}.%i.log.gz"/>
46+
47+
<root level="INFO">
48+
<appender-ref ref="CONSOLE"/>
49+
<appender-ref ref="ROLLING_LOG_FILE"/>
50+
</root>
51+
</springProfile>
52+
53+
</configuration>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package eatda.config;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import ch.qos.logback.classic.Level;
6+
import ch.qos.logback.classic.Logger;
7+
import ch.qos.logback.classic.spi.ILoggingEvent;
8+
import ch.qos.logback.core.read.ListAppender;
9+
import org.junit.jupiter.api.Nested;
10+
import org.junit.jupiter.api.Test;
11+
import org.slf4j.LoggerFactory;
12+
import org.springframework.mock.web.MockHttpServletRequest;
13+
import org.springframework.mock.web.MockHttpServletResponse;
14+
15+
class LoggingInterceptorTest {
16+
17+
private final LoggingInterceptor interceptor = new LoggingInterceptor();
18+
19+
@Nested
20+
class afterCompletion {
21+
22+
@Test
23+
void preHandle_없이_afterCompletion만_호출되면_duration_unknown_로그가_남는다() throws Exception {
24+
MockHttpServletRequest request = new MockHttpServletRequest();
25+
MockHttpServletResponse response = new MockHttpServletResponse();
26+
27+
Logger logger = (Logger) LoggerFactory.getLogger(LoggingInterceptor.class);
28+
ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
29+
listAppender.start();
30+
logger.addAppender(listAppender);
31+
32+
interceptor.afterCompletion(request, response, new Object(), null);
33+
34+
assertThat(listAppender.list).anySatisfy(event -> {
35+
assertThat(event.getLevel()).isEqualTo(Level.WARN);
36+
assertThat(event.getFormattedMessage()).contains("duration unknown - preHandle not called");
37+
});
38+
39+
listAppender.stop();
40+
logger.detachAppender(listAppender);
41+
}
42+
}
43+
}

0 commit comments

Comments
 (0)