From 9b5627071d0475fa6d65030a584e5d878722c753 Mon Sep 17 00:00:00 2001 From: Kwak Seong Joon Date: Wed, 4 Feb 2026 19:31:40 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20filter=20=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=94=94=EC=8A=A4=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=ED=98=95=EC=8B=9D=20=EB=B3=80=EA=B2=BD=20-=20#238?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/DiscordErrorLogAppender.java | 130 ++++++++++++------ .../global/filter/ExceptionHandlerFilter.java | 10 ++ .../filter/JwtAuthenticationFilter.java | 4 + 3 files changed, 101 insertions(+), 43 deletions(-) diff --git a/src/main/java/com/permitseoul/permitserver/global/DiscordErrorLogAppender.java b/src/main/java/com/permitseoul/permitserver/global/DiscordErrorLogAppender.java index 5040254d..2aed555e 100644 --- a/src/main/java/com/permitseoul/permitserver/global/DiscordErrorLogAppender.java +++ b/src/main/java/com/permitseoul/permitserver/global/DiscordErrorLogAppender.java @@ -23,9 +23,8 @@ public class DiscordErrorLogAppender extends UnsynchronizedAppenderBase mdc = event.getMDCPropertyMap(); final String traceId = safe(mdc.get("trace_id")); @@ -77,49 +70,100 @@ private String buildContent(final ILoggingEvent event) { final String uri = safe(mdc.get("uri")); final String method = safe(mdc.get("method")); final String status = safe(mdc.get("status")); + final String statusText = status.isBlank() ? "-" : status; final String timestamp = KST_FORMATTER.format(Instant.ofEpochMilli(event.getTimeStamp())); + final String loggerShort = getShortLoggerName(event.getLoggerName()); - final StringBuilder sb = new StringBuilder(); - - sb.append("[PERMIT-PROD] ") - .append(event.getLevel()) - .append(" at ") - .append(timestamp) - .append(" (Asia/Seoul)") - .append("\n"); - - sb.append("logger=").append(event.getLoggerName()) - .append(" | thread=").append(event.getThreadName()) - .append("\n"); - - sb.append("user_id=").append(userIdText) - .append(" | trace_id=").append(traceId) - .append(" | uri=").append(uri) - .append(" | method=").append(method) - .append(" | status=").append(status) - .append("\n\n"); - - sb.append("message: ") - .append(event.getFormattedMessage()); + // 메시지 처리 (최대 1024자) + String message = event.getFormattedMessage(); + if (message.length() > 1024) { + message = message.substring(0, 1021) + "..."; + } + // 스택트레이스 처리 + String stackTraceField = ""; final IThrowableProxy throwableProxy = event.getThrowableProxy(); if (throwableProxy != null) { String stackTrace = ThrowableProxyUtil.asString(throwableProxy); - - final int maxStackLength = 1700; + final int maxStackLength = 1000; if (stackTrace.length() > maxStackLength) { stackTrace = stackTrace.substring(0, maxStackLength) + "\n... (truncated)"; } - - sb.append("\n\n") - .append("```") - .append("\n") - .append(stackTrace) - .append("\n```"); + stackTraceField = """ + ,{ + "name": "📋 Stack Trace", + "value": "```%s```", + "inline": false + }""".formatted(escapeForJson(stackTrace)); } - return sb.toString(); + return """ + { + "embeds": [{ + "title": "🚨 PERMIT-PROD ERROR", + "color": 16711680, + "fields": [ + { + "name": "🕐 Time", + "value": "`%s`", + "inline": true + }, + { + "name": "👤 User ID", + "value": "`%s`", + "inline": true + }, + { + "name": "🔗 Trace ID", + "value": "`%s`", + "inline": true + }, + { + "name": "📍 Endpoint", + "value": "`%s %s`", + "inline": true + }, + { + "name": "📊 Status", + "value": "`%s`", + "inline": true + }, + { + "name": "📦 Logger", + "value": "`%s`", + "inline": true + }, + { + "name": "💬 Message", + "value": "```%s```", + "inline": false + }%s + ], + "footer": { + "text": "Thread: %s" + } + }] + } + """.formatted( + escapeForJson(timestamp), + escapeForJson(userIdText), + escapeForJson(traceId), + escapeForJson(method), + escapeForJson(uri), + escapeForJson(statusText), + escapeForJson(loggerShort), + escapeForJson(message), + stackTraceField, + escapeForJson(event.getThreadName())); + } + + private String getShortLoggerName(final String loggerName) { + if (loggerName == null || loggerName.isBlank()) { + return "unknown"; + } + final int lastDot = loggerName.lastIndexOf('.'); + return lastDot >= 0 ? loggerName.substring(lastDot + 1) : loggerName; } private void send(final String body) throws Exception { diff --git a/src/main/java/com/permitseoul/permitserver/global/filter/ExceptionHandlerFilter.java b/src/main/java/com/permitseoul/permitserver/global/filter/ExceptionHandlerFilter.java index 8fe94f9a..4f40ec54 100644 --- a/src/main/java/com/permitseoul/permitserver/global/filter/ExceptionHandlerFilter.java +++ b/src/main/java/com/permitseoul/permitserver/global/filter/ExceptionHandlerFilter.java @@ -10,6 +10,7 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.NonNull; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; @@ -20,6 +21,7 @@ @RequiredArgsConstructor @Component +@Slf4j public class ExceptionHandlerFilter extends OncePerRequestFilter { //필터 내부 전체 예외 private final ObjectMapper objectMapper; @@ -30,9 +32,17 @@ protected void doFilterInternal(@NonNull final HttpServletRequest request, try { filterChain.doFilter(request, response); } catch (FilterException e) { + log.warn("[FilterException] code={}, ua={}", + e.getErrorCode().name(), + request.getHeader("User-Agent") + ); handleUnauthorizedException(response, e); } catch (Exception e) { + log.error("[UnhandledException in FilterChain] ua={}", + request.getHeader("User-Agent"), + e + ); handleException(response); } } diff --git a/src/main/java/com/permitseoul/permitserver/global/filter/JwtAuthenticationFilter.java b/src/main/java/com/permitseoul/permitserver/global/filter/JwtAuthenticationFilter.java index 8983eb7e..1f0e831d 100644 --- a/src/main/java/com/permitseoul/permitserver/global/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/permitseoul/permitserver/global/filter/JwtAuthenticationFilter.java @@ -69,6 +69,10 @@ protected void doFilterInternal(@NonNull final HttpServletRequest request, } catch (ServletException | IOException e) { throw new FilterException(ErrorCode.INTERNAL_FILTER_ERROR); } catch (Exception e) { + log.error("[JWT Filter] unexpected error. ua={}", + request.getHeader("User-Agent"), + e + ); throw new FilterException(ErrorCode.INTERNAL_SERVER_ERROR); } finally { MDC.remove(USER_ID_MDC_KEY); From a7066ef51d5f0a86004c72b62d987459751f7234 Mon Sep 17 00:00:00 2001 From: Kwak Seong Joon Date: Wed, 4 Feb 2026 23:21:13 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=EA=B2=B0=EC=A0=9C=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=EC=8B=9C=20redis=20=EC=A4=91=EB=B3=B5=20rollback=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20-=20#238?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../payment/api/service/PaymentService.java | 15 ++++++++++++++- .../core/component/ReservationSessionRemover.java | 7 +++++++ .../repository/ReservationSessionRepository.java | 2 ++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/permitseoul/permitserver/domain/payment/api/service/PaymentService.java b/src/main/java/com/permitseoul/permitserver/domain/payment/api/service/PaymentService.java index 18d9ea89..35966128 100644 --- a/src/main/java/com/permitseoul/permitserver/domain/payment/api/service/PaymentService.java +++ b/src/main/java/com/permitseoul/permitserver/domain/payment/api/service/PaymentService.java @@ -24,6 +24,7 @@ import com.permitseoul.permitserver.domain.reservation.core.domain.Reservation; import com.permitseoul.permitserver.domain.reservation.core.domain.ReservationStatus; import com.permitseoul.permitserver.domain.reservation.core.exception.ReservationNotFoundException; +import com.permitseoul.permitserver.domain.reservationsession.core.component.ReservationSessionRemover; import com.permitseoul.permitserver.domain.reservationsession.core.component.ReservationSessionRetriever; import com.permitseoul.permitserver.domain.reservationsession.core.domain.ReservationSession; import com.permitseoul.permitserver.domain.reservationsession.core.exception.ReservationSessionBadRequestException; @@ -83,6 +84,7 @@ public class PaymentService { private final TicketReservationPaymentFacade ticketReservationPaymentFacade; private final ReservationSessionRetriever reservationSessionRetriever; private final RedisManager redisManager; + private final ReservationSessionRemover reservationSessionRemover; public PaymentService( @@ -96,7 +98,7 @@ public PaymentService( TicketRetriever ticketRetriever, TicketReservationPaymentFacade ticketReservationPaymentFacade, ReservationSessionRetriever reservationSessionRetriever, - RedisManager redisManager) { + RedisManager redisManager, ReservationSessionRemover reservationSessionRemover) { this.reservationTicketRetriever = reservationTicketRetriever; this.eventRetriever = eventRetriever; this.tossPaymentClient = tossPaymentClient; @@ -108,6 +110,7 @@ public PaymentService( this.ticketReservationPaymentFacade = ticketReservationPaymentFacade; this.reservationSessionRetriever = reservationSessionRetriever; this.redisManager = redisManager; + this.reservationSessionRemover = reservationSessionRemover; } @@ -171,26 +174,32 @@ public PaymentConfirmResponse getPaymentConfirm(final long userId, } catch (ReservationNotFoundException e) { sessionRedisRollback(reservationTicketList, userId, reservationSessionKey, orderId, totalAmount, paymentKey); + deleteReservationSessionByOrderId(orderId); throw new NotFoundPaymentException(ErrorCode.NOT_FOUND_RESERVATION); } catch (EventNotfoundException e) { sessionRedisRollback(reservationTicketList, userId, reservationSessionKey, orderId, totalAmount, paymentKey); + deleteReservationSessionByOrderId(orderId); throw new NotFoundPaymentException(ErrorCode.NOT_FOUND_EVENT); } catch (TicketTypeNotfoundException e) { sessionRedisRollback(reservationTicketList, userId, reservationSessionKey, orderId, totalAmount, paymentKey); + deleteReservationSessionByOrderId(orderId); throw new NotFoundPaymentException(ErrorCode.NOT_FOUND_TICKET_TYPE); } catch (TicketTypeInsufficientCountException e) { sessionRedisRollback(reservationTicketList, userId, reservationSessionKey, orderId, totalAmount, paymentKey); + deleteReservationSessionByOrderId(orderId); throw new ConflictReservationException(ErrorCode.CONFLICT_INSUFFICIENT_TICKET); } catch (TicketTypeTicketZeroException e) { sessionRedisRollback(reservationTicketList, userId, reservationSessionKey, orderId, totalAmount, paymentKey); + deleteReservationSessionByOrderId(orderId); throw new PaymentBadRequestException(ErrorCode.BAD_REQUEST_TICKET_COUNT_ZERO); } catch(FeignException e) { handleFailedTossPayment(reservation, reservationTicketList, userId, reservationSessionKey, orderId, totalAmount, paymentKey); + deleteReservationSessionByOrderId(orderId); throw handleFeignException(e, orderId, userId); } catch (AlgorithmException e) { //todo: 결제는 됐는데, 티켓 발급 과정에서 실패했으므로, 따로 알림 구축해놔야될듯 @@ -254,6 +263,10 @@ public void cancelPayment(final long userId, final String orderId) { } } + private void deleteReservationSessionByOrderId(final String orderId) { + reservationSessionRemover.deleteByOrderId(orderId); + } + private void validateTicketStatusForCancel(final List ticketList) { for (final Ticket ticket : ticketList) { switch (ticket.getStatus()) { diff --git a/src/main/java/com/permitseoul/permitserver/domain/reservationsession/core/component/ReservationSessionRemover.java b/src/main/java/com/permitseoul/permitserver/domain/reservationsession/core/component/ReservationSessionRemover.java index a4039ba7..95b23ff0 100644 --- a/src/main/java/com/permitseoul/permitserver/domain/reservationsession/core/component/ReservationSessionRemover.java +++ b/src/main/java/com/permitseoul/permitserver/domain/reservationsession/core/component/ReservationSessionRemover.java @@ -7,6 +7,8 @@ import java.util.List; +import org.springframework.transaction.annotation.Transactional; + @Component @RequiredArgsConstructor public class ReservationSessionRemover { @@ -15,4 +17,9 @@ public class ReservationSessionRemover { public void deleteAllInBatch(final List reservationSessionEntities) { reservationSessionRepository.deleteAllInBatch(reservationSessionEntities); } + + @Transactional + public void deleteByOrderId(final String orderId) { + reservationSessionRepository.deleteByOrderId(orderId); + } } diff --git a/src/main/java/com/permitseoul/permitserver/domain/reservationsession/core/repository/ReservationSessionRepository.java b/src/main/java/com/permitseoul/permitserver/domain/reservationsession/core/repository/ReservationSessionRepository.java index 677995d4..a35d8651 100644 --- a/src/main/java/com/permitseoul/permitserver/domain/reservationsession/core/repository/ReservationSessionRepository.java +++ b/src/main/java/com/permitseoul/permitserver/domain/reservationsession/core/repository/ReservationSessionRepository.java @@ -30,4 +30,6 @@ Optional findValidSessionByUserIdAndSessionKeyAndValid List findAllBySuccessfulTrue(); List findAllBySuccessfulFalseAndCreatedAtBefore(final LocalDateTime time); + + void deleteByOrderId(final String orderId); } From db7438c394b96a34bdb4c001280a430f9983a6a5 Mon Sep 17 00:00:00 2001 From: Kwak Seong Joon Date: Wed, 4 Feb 2026 23:22:14 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20filter=20log=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20-=20#238?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../permitserver/global/filter/JwtAuthenticationFilter.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/permitseoul/permitserver/global/filter/JwtAuthenticationFilter.java b/src/main/java/com/permitseoul/permitserver/global/filter/JwtAuthenticationFilter.java index 1f0e831d..c64bdda8 100644 --- a/src/main/java/com/permitseoul/permitserver/global/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/permitseoul/permitserver/global/filter/JwtAuthenticationFilter.java @@ -67,6 +67,10 @@ protected void doFilterInternal(@NonNull final HttpServletRequest request, } catch (AuthWrongJwtException e) { throw new FilterException(ErrorCode.UNAUTHORIZED); } catch (ServletException | IOException e) { + log.error("[JWT Filter] unexpected error. ua={}", + request.getHeader("User-Agent"), + e + ); throw new FilterException(ErrorCode.INTERNAL_FILTER_ERROR); } catch (Exception e) { log.error("[JWT Filter] unexpected error. ua={}", From 67e6e9b43fc0409c91269b4e6eaaf692f022e690 Mon Sep 17 00:00:00 2001 From: Kwak Seong Joon Date: Wed, 4 Feb 2026 23:34:04 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20session=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=EC=8B=9C=20=EB=A1=9C=EA=B9=85=20-=20#238?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/payment/api/service/PaymentService.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/permitseoul/permitserver/domain/payment/api/service/PaymentService.java b/src/main/java/com/permitseoul/permitserver/domain/payment/api/service/PaymentService.java index 35966128..79e5b22a 100644 --- a/src/main/java/com/permitseoul/permitserver/domain/payment/api/service/PaymentService.java +++ b/src/main/java/com/permitseoul/permitserver/domain/payment/api/service/PaymentService.java @@ -264,7 +264,12 @@ public void cancelPayment(final long userId, final String orderId) { } private void deleteReservationSessionByOrderId(final String orderId) { - reservationSessionRemover.deleteByOrderId(orderId); + try { + reservationSessionRemover.deleteByOrderId(orderId); + } catch (Exception e) { + log.error("[Payment] 결제 실패 후, Reservation Session 삭제 실패(중복 redis rollback 가능성) orderId={}", orderId, e); + } + } private void validateTicketStatusForCancel(final List ticketList) {