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..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 @@ -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,15 @@ public void cancelPayment(final long userId, final String orderId) { } } + private void deleteReservationSessionByOrderId(final String orderId) { + try { + reservationSessionRemover.deleteByOrderId(orderId); + } catch (Exception e) { + log.error("[Payment] 결제 실패 후, Reservation Session 삭제 실패(중복 redis rollback 가능성) orderId={}", orderId, e); + } + + } + 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); } 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..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,8 +67,16 @@ 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={}", + request.getHeader("User-Agent"), + e + ); throw new FilterException(ErrorCode.INTERNAL_SERVER_ERROR); } finally { MDC.remove(USER_ID_MDC_KEY);