diff --git a/build.gradle b/build.gradle index 3378ed41..ab0f60da 100644 --- a/build.gradle +++ b/build.gradle @@ -48,6 +48,12 @@ dependencies { implementation 'io.springfox:springfox-boot-starter:3.0.0' implementation 'io.springfox:springfox-swagger-ui:3.0.0' + + // slack 알림 + implementation 'com.slack.api:slack-api-client:1.27.2' + + // slack DTO json Formatting + implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.0' } tasks.named('test') { diff --git a/src/main/java/com/gam/api/common/ApiResponse.java b/src/main/java/com/gam/api/common/ApiResponse.java index 77b984ca..1d1c7e76 100644 --- a/src/main/java/com/gam/api/common/ApiResponse.java +++ b/src/main/java/com/gam/api/common/ApiResponse.java @@ -27,4 +27,9 @@ public static ApiResponse fail(String message){ .build(); } + public static ApiResponse serverError(Object object){ + return ApiResponse.builder() + .data(object) + .build(); + } } diff --git a/src/main/java/com/gam/api/common/ErrorHandler.java b/src/main/java/com/gam/api/common/ErrorHandler.java index e276fc1b..eca7e2e0 100644 --- a/src/main/java/com/gam/api/common/ErrorHandler.java +++ b/src/main/java/com/gam/api/common/ErrorHandler.java @@ -7,6 +7,13 @@ import com.gam.api.common.exception.ReportException; import com.gam.api.common.exception.ScrapException; import com.gam.api.common.exception.WorkException; +import com.gam.api.common.message.ExceptionMessage; +import com.gam.api.config.SlackConfig; +import com.slack.api.Slack; +import java.time.LocalDateTime; +import javax.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.val; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -15,11 +22,34 @@ import javax.persistence.EntityNotFoundException; -import static com.gam.api.common.message.ExceptionMessage.EMPTY_METHOD_ARGUMENT; - @RestControllerAdvice +@RequiredArgsConstructor public class ErrorHandler { + private final Slack slack; + private final SlackConfig slackConfig; + + /** Internal Server Error + Slack Alert **/ + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception exception, HttpServletRequest request) { + val errorDTO = requestToDTO(exception, request); + sendSlackAlarm(errorDTO.toString()); + + ApiResponse response = ApiResponse.serverError(errorDTO); + return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); + } + + @ExceptionHandler(RuntimeException.class) + public ResponseEntity handleRuntimeException(RuntimeException exception, HttpServletRequest request) { + val errorDTO = requestToDTO(exception, request); + sendSlackAlarm(errorDTO.toString()); + + ApiResponse response = ApiResponse.serverError(errorDTO); + return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); + } + + + /** Custom Error + 4__ Error Handler **/ @ExceptionHandler(EntityNotFoundException.class) public ResponseEntity handleEntityNotFoundException(EntityNotFoundException exception) { ApiResponse response = ApiResponse.fail(exception.getMessage()); @@ -73,4 +103,23 @@ public ResponseEntity ReportException(ReportException exception){ ApiResponse response = ApiResponse.fail(exception.getMessage()); return new ResponseEntity<>(response, exception.getStatusCode()); } + + private InternalServerErrorDTO requestToDTO(Exception exception, HttpServletRequest request) { + val header = InternalServerErrorDTO.extractHeaders(request); + return InternalServerErrorDTO.of(header, request.getMethod(), request.getRequestURL().toString(), + exception.getMessage(), exception.getClass().getName(), LocalDateTime.now()); + } + + private void sendSlackAlarm(String dto) { + try{ + val slackResponse = slack.send(slackConfig.getUrl(), SlackErrorPayload.of(dto)).getBody(); + + if(!slackResponse.equals("ok")) { + throw new Exception(ExceptionMessage.NOT_POST_SLACK_ALARM.getMessage());// todo log -> 슬랙알림 안감 + } + }catch (Exception exception) { + System.out.println(exception.toString()); // todo log + } + } + } \ No newline at end of file diff --git a/src/main/java/com/gam/api/common/InternalServerErrorDTO.java b/src/main/java/com/gam/api/common/InternalServerErrorDTO.java new file mode 100644 index 00000000..9ca484c3 --- /dev/null +++ b/src/main/java/com/gam/api/common/InternalServerErrorDTO.java @@ -0,0 +1,82 @@ +package com.gam.api.common; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Enumeration; +import java.util.Objects; +import javax.servlet.http.HttpServletRequest; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class InternalServerErrorDTO { + private String header = null; + private String httpMethod = null; + private String URL = null ; + private String message = null; + + private String errorClass = null; + + private String dateTime = ""; + + @Builder + private InternalServerErrorDTO(String header, String httpMethod, String URL, String message, + String errorClass, String dateTime) { + this.header = header; + this.httpMethod = httpMethod; + this.URL = URL; + this.message = message; + this.errorClass = errorClass; + this.dateTime = dateTime; + } + + public static InternalServerErrorDTO of(String header, String httpMethod, String URL, String message, + String errorClass, LocalDateTime nowDateTime) { + if(Objects.isNull(nowDateTime)) { + nowDateTime = LocalDateTime.now(); + } + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + String now = nowDateTime.format(formatter); + + return InternalServerErrorDTO.builder() + .header(header) + .httpMethod(httpMethod) + .URL(URL) + .message(message) + .errorClass(errorClass) + .dateTime(now) + .build(); + } + + public static String extractHeaders(HttpServletRequest request) { + StringBuilder headers = new StringBuilder(); + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String headerName = headerNames.nextElement(); + String headerValue = request.getHeader(headerName); + headers.append(headerName).append(": ").append(headerValue); + } + return headers.toString(); + } + + @Override + public String toString() { + ObjectMapper objectMapper = new ObjectMapper(); + ObjectWriter writer = objectMapper.writerWithDefaultPrettyPrinter(); + String jsonString = null; + try { + jsonString = writer.writeValueAsString(this); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); //todo Log + } + return jsonString; + } + +} diff --git a/src/main/java/com/gam/api/common/SlackErrorPayload.java b/src/main/java/com/gam/api/common/SlackErrorPayload.java new file mode 100644 index 00000000..d04137f3 --- /dev/null +++ b/src/main/java/com/gam/api/common/SlackErrorPayload.java @@ -0,0 +1,35 @@ +package com.gam.api.common; + +import static com.slack.api.model.block.Blocks.section; +import static com.slack.api.model.block.composition.BlockCompositions.markdownText; +import static java.util.Arrays.asList; + +import com.slack.api.webhook.Payload; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class SlackErrorPayload { + + private Payload payload; + + @Builder + private SlackErrorPayload(Payload payload) { + this.payload = payload; + } + + public static Payload of(String errorDto) { + // Create Payload with a code block + String codeBlock = "```\n" +errorDto + "```"; + + return Payload.builder() + .blocks(asList( + section(s -> s.text(markdownText(mt -> mt.text(codeBlock)))) + )) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/gam/api/common/message/ExceptionMessage.java b/src/main/java/com/gam/api/common/message/ExceptionMessage.java index 71be522a..b61d90f0 100644 --- a/src/main/java/com/gam/api/common/message/ExceptionMessage.java +++ b/src/main/java/com/gam/api/common/message/ExceptionMessage.java @@ -54,7 +54,10 @@ public enum ExceptionMessage { /** Report **/ ALREADY_REPORTED_USER("이미 신고 한 유저입니다. 신고 처리 진행중"), - NOT_MATCH_DB_BLOCK_STATUS("DB의 userScrap 상태와, 보낸 userScrap 상태가 같지 않습니다."); + NOT_MATCH_DB_BLOCK_STATUS("DB의 userScrap 상태와, 보낸 userScrap 상태가 같지 않습니다."), + + /** slack **/ + NOT_POST_SLACK_ALARM("슬랙 알림이 전송되지 않았습니다."); private final String message; } diff --git a/src/main/java/com/gam/api/config/SlackConfig.java b/src/main/java/com/gam/api/config/SlackConfig.java new file mode 100644 index 00000000..17eb4708 --- /dev/null +++ b/src/main/java/com/gam/api/config/SlackConfig.java @@ -0,0 +1,20 @@ +package com.gam.api.config; + +import com.slack.api.Slack; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Getter +@Configuration +public class SlackConfig { + + @Value("${slack.url}") + private String url; + + @Bean + public Slack slack() { + return Slack.getInstance(); + } +}