Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand All @@ -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;
Expand All @@ -108,6 +110,7 @@ public PaymentService(
this.ticketReservationPaymentFacade = ticketReservationPaymentFacade;
this.reservationSessionRetriever = reservationSessionRetriever;
this.redisManager = redisManager;
this.reservationSessionRemover = reservationSessionRemover;
}


Expand Down Expand Up @@ -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: 결제는 됐는데, 티켓 발급 과정에서 실패했으므로, 따로 알림 구축해놔야될듯
Expand Down Expand Up @@ -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<Ticket> ticketList) {
for (final Ticket ticket : ticketList) {
switch (ticket.getStatus()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

import java.util.List;

import org.springframework.transaction.annotation.Transactional;

@Component
@RequiredArgsConstructor
public class ReservationSessionRemover {
Expand All @@ -15,4 +17,9 @@ public class ReservationSessionRemover {
public void deleteAllInBatch(final List<ReservationSessionEntity> reservationSessionEntities) {
reservationSessionRepository.deleteAllInBatch(reservationSessionEntities);
}

@Transactional
public void deleteByOrderId(final String orderId) {
reservationSessionRepository.deleteByOrderId(orderId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,6 @@ Optional<ReservationSessionEntity> findValidSessionByUserIdAndSessionKeyAndValid
List<ReservationSessionEntity> findAllBySuccessfulTrue();

List<ReservationSessionEntity> findAllBySuccessfulFalseAndCreatedAtBefore(final LocalDateTime time);

void deleteByOrderId(final String orderId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@ public class DiscordErrorLogAppender extends UnsynchronizedAppenderBase<ILogging

private int readTimeout = 3000;

private static final DateTimeFormatter KST_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
.withZone(ZoneId.of("Asia/Seoul"));
private static final DateTimeFormatter KST_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
.withZone(ZoneId.of("Asia/Seoul"));

public void setUrl(final String url) {
this.url = url;
Expand All @@ -50,25 +49,19 @@ protected void append(final ILoggingEvent event) {
return;
}

final String content = buildContent(event);
if (content == null || content.isBlank()) {
final String jsonBody = buildEmbedJson(event);
if (jsonBody == null || jsonBody.isBlank()) {
return;
}

final String jsonBody = """
{
"content": "%s"
}
""".formatted(escapeForJson(content));

send(jsonBody);

} catch (final Exception e) {
addError("Failed to send error log to Discord", e);
}
}

private String buildContent(final ILoggingEvent event) {
private String buildEmbedJson(final ILoggingEvent event) {
final Map<String, String> mdc = event.getMDCPropertyMap();

final String traceId = safe(mdc.get("trace_id"));
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,6 +21,7 @@

@RequiredArgsConstructor
@Component
@Slf4j
public class ExceptionHandlerFilter extends OncePerRequestFilter { //필터 내부 전체 예외
private final ObjectMapper objectMapper;

Expand All @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down