-
Notifications
You must be signed in to change notification settings - Fork 2
faet: dev log stacktrace 추가 - #159 #161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,24 +22,38 @@ public class DevRequestResponseLoggingFilter implements Filter { | |
|
|
||
| private final DiscordSender discordSender; | ||
|
|
||
| private final static String HEALTH_CHECK_URL = "/actuator/health"; | ||
|
|
||
| @Override | ||
| public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { | ||
|
|
||
| // 요청,응답을 여러 번 읽을 수 있도록 래핑 | ||
| final ContentCachingRequestWrapper req = new ContentCachingRequestWrapper((HttpServletRequest) request); | ||
| final ContentCachingResponseWrapper res = new ContentCachingResponseWrapper((HttpServletResponse) response); | ||
|
|
||
| Exception exception = null; | ||
|
|
||
| final long start = System.currentTimeMillis(); | ||
| try { | ||
| chain.doFilter(req, res); | ||
| } finally { | ||
| } catch (Exception e) { | ||
| exception = e; | ||
| log.error("🚨 [DevRequestResponseLoggingFilter] 요청 중 예외 발생: {}", e.getMessage(), e); | ||
| res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); | ||
| } | ||
| finally { | ||
| final long duration = System.currentTimeMillis() - start; | ||
| final String uri = req.getRequestURI(); | ||
| if (!uri.startsWith("/actuator/health")) { | ||
| final String jsonLog = HttpReqResLogJsonBuilder.buildJsonLog(req, res, duration); | ||
| if (!uri.startsWith(HEALTH_CHECK_URL)) { | ||
| final String jsonLog = HttpReqResLogJsonBuilder.buildJsonLog(req, res, duration, exception); | ||
| discordSender.send(jsonLog); | ||
| } | ||
| res.copyBodyToResponse(); | ||
| } | ||
|
|
||
| if (exception != null) { | ||
| if (exception instanceof ServletException se) throw se; | ||
| if (exception instanceof IOException ioe) throw ioe; | ||
| throw new ServletException(exception); | ||
| } | ||
|
Comment on lines
+38
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 예외 재던지기(rethrow) 전에 응답을 커밋함 → double‑commit 위험 catch에서 상태코드 500 설정 후 finally에서 권장 패치: - } catch (Exception e) {
+ } catch (Exception e) {
exception = e;
log.error("🚨 [DevRequestResponseLoggingFilter] 요청 중 예외 발생: {}", e.getMessage(), e);
- res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
- }
- finally {
+ }
+ finally {
final long duration = System.currentTimeMillis() - start;
final String uri = req.getRequestURI();
if (!uri.startsWith(HEALTH_CHECK_URL)) {
final String jsonLog = HttpReqResLogJsonBuilder.buildJsonLog(req, res, duration, exception);
- discordSender.send(jsonLog);
+ try {
+ discordSender.send(jsonLog);
+ } catch (Exception sendEx) {
+ log.warn("Discord 전송 실패: {}", sendEx.toString());
+ }
}
- res.copyBodyToResponse();
+ if (exception == null) {
+ res.copyBodyToResponse();
+ }
}
if (exception != null) {
if (exception instanceof ServletException se) throw se;
if (exception instanceof IOException ioe) throw ioe;
throw new ServletException(exception);
}🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,11 +29,14 @@ public final class HttpReqResLogJsonBuilder { | |
| private static final String FIELD_DURATION = "duration_ms"; | ||
| private static final String FIELD_REQUEST_BODY = "request_body"; | ||
| private static final String FIELD_RESPONSE_BODY = "response_body"; | ||
| private static final String FIELD_EXCEPTION = "exception"; | ||
| private static final String FIELD_STACKTRACE = "stacktrace"; | ||
| private static final String LOG_TYPE_HTTP = "HTTP"; | ||
|
|
||
| public static String buildJsonLog(final ContentCachingRequestWrapper request, | ||
| final ContentCachingResponseWrapper response, | ||
| final long duration) { | ||
| final long duration, | ||
| final Exception exception) { | ||
| try { | ||
| final Map<String, Object> logMap = new LinkedHashMap<>(); | ||
|
|
||
|
|
@@ -47,6 +50,12 @@ public static String buildJsonLog(final ContentCachingRequestWrapper request, | |
| logMap.put(FIELD_REQUEST_BODY, extractBody(request.getContentAsByteArray())); | ||
| logMap.put(FIELD_RESPONSE_BODY, extractBody(response.getContentAsByteArray())); | ||
|
|
||
|
|
||
| if (exception != null) { | ||
| logMap.put(FIELD_EXCEPTION, exception.getClass().getSimpleName() + ": " + exception.getMessage()); | ||
| logMap.put(FIELD_STACKTRACE, getStackTraceAsString(exception)); | ||
| } | ||
|
Comment on lines
+53
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 예외/스택트레이스 로그 추가는 좋습니다. 다만 민감정보 마스킹이 필요합니다. 요청/응답 바디, 예외 메시지에 비밀번호·토큰·이메일 등 PII/시크릿이 포함될 수 있습니다. Discord로 외부 전송되는 만큼 기본 마스킹을 추가해 주세요(키 기반 마스킹: password, pass, pwd, token, accessToken, refreshToken, authorization, email, phone 등). 예시(핵심 아이디어):
원하는 경우 보조 헬퍼 |
||
|
|
||
| return OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(logMap); | ||
| } catch (Exception e) { | ||
| return "{\"error\":\"failed to format log\"}"; | ||
|
|
@@ -60,4 +69,16 @@ private static String extractBody(byte[] arr) { | |
| ? body.substring(0, MAX_LENGTH) + OVER_MAX_LENGTH | ||
| : body; | ||
| } | ||
|
|
||
| private static String getStackTraceAsString(final Exception e) { | ||
| final StringBuilder sb = new StringBuilder(); | ||
| for (StackTraceElement el : e.getStackTrace()) { | ||
| sb.append(el.toString()).append("\n"); | ||
| if (sb.length() > 1500) { // Discord 메시지 길이 제한 방지 | ||
| sb.append("...more"); | ||
| break; | ||
| } | ||
| } | ||
| return sb.toString(); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
쿠폰 사용 시 각 티켓 가격을 예약 총액으로 설정 — 과금 오류 위험
hasCoupon일 때 개별 티켓의ticketPrice가reservation.getTotalAmount()(예약 전체 금액)로 설정됩니다. 여러 장 생성 시 각 티켓이 총액을 개별적으로 갖게 되어 과금/정산 오류가 발생할 수 있습니다.빠른 안전조치(권장):
정확한 할인 배분이 필요하면, 전체 원가 합계 대비
reservation.getTotalAmount()비율로 티켓 단가를 비례 배분(반올림 규칙 포함)하는 로직을 별도 단계에서 계산해 주입하는 방식을 제안드립니다. 비즈니스 룰 확인 부탁드립니다.🏁 Script executed:
Length of output: 2618
🏁 Script executed:
Length of output: 18145
🏁 Script executed:
Length of output: 13848
🏁 Script executed:
Length of output: 10511
🏁 Script executed:
Length of output: 7105
🏁 Script executed:
Length of output: 8937
쿠폰 적용 시 각 티켓 가격이 예약 총액으로 설정됨 — 중복 청구 위험 (치명적)
hasCoupon일 때IntStream.range(0, reservationTicket.getCount())를 통해 생성되는 각 티켓이reservation.getTotalAmount()(예약 전체 금액)로 설정됩니다. 별도 검증으로 쿠폰 구매는 티켓 1장으로 제한되어 있지만, 제너레이터가 이를 강제하지 않아 다음과 같은 위험이 있습니다:ticketPrice는 DB 저장 및 정산 쿼리(sumTicketPriceByTicketTypeIdAndStatuses)에 사용되므로 회계 데이터 오염권장 조치:
검증 실패 시에도 안전하도록 제너레이터 내부에 방어 로직 추가:
또는 쿠폰 할인을 별도 단계에서 명시적으로 처리(금액 비례 배분, 반올림 규칙 포함)하고 검증 계층에서만 쿠폰 복수 구매 방지.
📝 Committable suggestion
🤖 Prompt for AI Agents