UMC 7주차 시니어 미션입니다.
app:
notification:
discord:
webhook-url: ${DISCORD_WEBHOOK_URL}
enabled: true
slack:
webhook-url: ${SLACK_WEBHOOK_URL}
enabled: false
alert-env: prod
app:
notification:
discord:
webhook-url: ${DISCORD_WEBHOOK_URL}
enabled: true
slack:
webhook-url: ${SLACK_WEBHOOK_URL}
enabled: false
alert-env: dev
public void notifyError(String requestUrl, Exception exception, String method) {
// 현재 환경이 알림 대상 환경인지 확인
if (!isAlertTargetEnv()) {
log.debug("Alert is disabled for environment: {}", currentEnv);
return;
}
String errorMessage = formatErrorMessage(requestUrl, exception, method);
if (discordEnabled && !discordWebhookUrl.isBlank()) {
sendToDiscord(errorMessage);
}
if (slackEnabled && !slackWebhookUrl.isBlank()) {
sendToSlack(errorMessage);
}
}
/**
* Discord로 에러 메시지 전송
*/
private void sendToDiscord(String errorMessage) {
try {
Map<String, Object> payload = new HashMap<>();
payload.put("content", errorMessage);
restTemplate.postForObject(discordWebhookUrl, payload, String.class);
log.info("Discord notification sent successfully");
} catch (RestClientException e) {
log.error("Failed to send Discord notification", e);
} catch (Exception e) {
log.error("Unexpected error while sending Discord notification", e);
}
}
/**
* Slack으로 에러 메시지 전송
*/
private void sendToSlack(String errorMessage) {
try {
Map<String, Object> payload = new HashMap<>();
Map<String, Object> attachments = new HashMap<>();
attachments.put("color", "danger");
attachments.put("text", errorMessage);
payload.put("attachments", new Object[]{attachments});
restTemplate.postForObject(slackWebhookUrl, payload, String.class);
log.info("Slack notification sent successfully");
} catch (RestClientException e) {
log.error("Failed to send Slack notification", e);
} catch (Exception e) {
log.error("Unexpected error while sending Slack notification", e);
}
}
/**
* 에러 메시지 포맷팅
*/
private String formatErrorMessage(String requestUrl, Exception exception, String method) {
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String exceptionName = exception.getClass().getSimpleName();
String exceptionMessage = exception.getMessage() != null ? exception.getMessage() : "No message";
// 스택 트레이스의 첫 번째 라인 추출
String stackTrace = "";
if (exception.getStackTrace().length > 0) {
StackTraceElement element = exception.getStackTrace()[0];
stackTrace = String.format("%s.%s (Line %d)",
element.getClassName(), element.getMethodName(), element.getLineNumber());
}
return String.format(
"[500 ERROR ALERT]\n" +
"====================================\n" +
"Time: %s\n" +
"Environment: %s\n" +
"Request URL: %s\n" +
"Method: %s\n" +
"Exception: %s\n" +
"Message: %s\n" +
"Location: %s\n" +
"====================================",
now.format(formatter),
currentEnv.toUpperCase(),
requestUrl,
method,
exceptionName,
exceptionMessage,
stackTrace
);
}
/**
* 현재 환경이 알림 대상인지 확인
*/
private boolean isAlertTargetEnv() {
if (alertEnv == null || alertEnv.isBlank()) {
return false;
}
String[] envs = alertEnv.split(",");
for (String env : envs) {
if (env.trim().equals(currentEnv)) {
return true;
}
}
return false;
}
private final NotificationService notificationService;
500 에러를 처리하는 로직에서 알림 전송 로직을 추가한다.
// 모든 미처리 예외 → 500
@ExceptionHandler(Exception.class)
public ResponseEntity<Object> handleUnknownException(Exception e, HttpServletRequest request) {
log.error("Unhandled exception", e);
// 요청 정보 추출
String requestUrl = request.getRequestURI();
String method = request.getMethod();
// Discord/Slack으로 알림 전송
notificationService.notifyError(requestUrl, e, method);
ApiResponse<Object> body = ApiResponse.onFailure(ErrorCode.INTERNAL_SERVER_ERROR, null);
WebRequest webRequest = new ServletWebRequest(request);
return handleExceptionInternal(e, body, new HttpHeaders(),
ErrorCode.INTERNAL_SERVER_ERROR.getHttpStatus(), webRequest);
}