Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] 에러 핸들링 + Slack 연동 기능 추가 #2

Merged
merged 1 commit into from
Jul 3, 2023
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
17 changes: 12 additions & 5 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,28 @@ repositories {
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

// mysql --version
// Health Check
implementation 'org.springframework.boot:spring-boot-starter-actuator'

// JPA & Database
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'mysql:mysql-connector-java:8.0.32'
implementation 'org.springframework.boot:spring-boot-starter-validation'

//JSON
implementation 'com.googlecode.json-simple:json-simple:1.1.1'

// Health Check
implementation 'org.springframework.boot:spring-boot-starter-actuator'
// Slack Webhook
implementation 'com.slack.api:slack-api-client:1.28.0'
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
implementation 'com.slack.api:slack-app-backend:1.28.0'
implementation 'com.slack.api:slack-api-model:1.28.0'
}

tasks.named('test') {
Expand Down
34 changes: 34 additions & 0 deletions src/main/java/sopt/org/umbbaServer/error/ApiResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package sopt.org.umbbaServer.error;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@JsonInclude(JsonInclude.Include.NON_NULL)
@Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class ApiResponse<T> {

private final int code;
private final String message;
private T data;

public static ApiResponse success(SuccessType successType) {
return new ApiResponse<>(successType.getHttpStatusCode(), successType.getMessage());
}

public static <T> ApiResponse<T> success(SuccessType successType, T data) {
return new ApiResponse<T>(successType.getHttpStatusCode(), successType.getMessage(), data);
}

public static ApiResponse error(ErrorType errorType) {
return new ApiResponse<>(errorType.getHttpStatusCode(), errorType.getMessage());
}

public static ApiResponse error(ErrorType errorType, String message) {
return new ApiResponse<>(errorType.getHttpStatusCode(), message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package sopt.org.umbbaServer.error;

import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import sopt.org.umbbaServer.util.slack.SlackApi;

import javax.validation.UnexpectedTypeException;
import java.util.Objects;

@RestControllerAdvice
@Component
@RequiredArgsConstructor
public class ControllerExceptionAdvice {

private final SlackApi slackApi;

/**
* 400 BAD_REQUEST
*/

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
protected ApiResponse handleMethodArgumentNotValidException(final MethodArgumentNotValidException e) {
FieldError fieldError = Objects.requireNonNull(e.getFieldError());
return ApiResponse.error(ErrorType.REQUEST_VALIDATION_EXCEPTION, String.format("%s. (%s)", fieldError.getDefaultMessage(), fieldError.getField()));
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(UnexpectedTypeException.class)
protected ApiResponse handleUnexpectedTypeException(final UnexpectedTypeException e) {
return ApiResponse.error(ErrorType.VALIDATION_WRONG_TYPE_EXCEPTION);
}

/**
* 500 INTERNEL_SERVER
*/
// @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
// @ExceptionHandler(Exception.class)
// protected ApiResponse<Object> handleException(final Exception e, final HttpServletRequest request) throws IOException {
// //slackApi.sendAlert(e, request);
// return ApiResponse.error(ErrorType.INTERNAL_SERVER_ERROR);
// }

/**
* CUSTOM_ERROR
*/
@ExceptionHandler(CustomException.class)
protected ResponseEntity<ApiResponse> handleSoptException(CustomException e) {
return ResponseEntity.status(e.getHttpStatus())
.body(ApiResponse.error(e.getErrorType(), e.getMessage()));
}
}
18 changes: 18 additions & 0 deletions src/main/java/sopt/org/umbbaServer/error/CustomException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package sopt.org.umbbaServer.error;

import lombok.Getter;

@Getter
public class CustomException extends RuntimeException {

private final ErrorType errorType;

public CustomException(ErrorType errorType) {
super(errorType.getMessage());
this.errorType = errorType;
}

public int getHttpStatus() {
return errorType.getHttpStatusCode();
}
}
51 changes: 51 additions & 0 deletions src/main/java/sopt/org/umbbaServer/error/ErrorType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package sopt.org.umbbaServer.error;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public enum ErrorType {

/**
* 400 BAD REQUEST
*/
REQUEST_VALIDATION_EXCEPTION(HttpStatus.BAD_REQUEST, "잘못된 요청입니다"),
VALIDATION_WRONG_TYPE_EXCEPTION(HttpStatus.BAD_REQUEST, "잘못된 타입이 입력되었습니다"),
EMPTY_PRINCIPLE(HttpStatus.BAD_REQUEST, "Principle 객체가 없습니다. (null)"),

/**
* 401 UNAUTHORIZED
*/
INVALID_SOCIAL_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 소셜 엑세스 토큰입니다."),
INVALID_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 엑세스 토큰입니다, 엑세스 토큰을 재발급 받아주세요."),
INVALID_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 리프레시 토큰입니다, 다시 로그인을 해주세요."),
NOTMATCH_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "일치하지 않는 리프레시 토큰입니다."),

/**
* 404 NOT FOUND
*/
INVALID_USER(HttpStatus.NOT_FOUND, "유효하지 않은 회원입니다."),


/**
* 409 CONFLICT
*/
ALREADY_EXIST_USER_EXCEPTION(HttpStatus.CONFLICT, "이미 존재하는 유저입니다"),


/**
* 500 INTERNAL SERVER ERROR
*/
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "알 수 없는 서버 에러가 발생했습니다"),
;

private final HttpStatus httpStatus;
private final String message;

public int getHttpStatusCode() {
return httpStatus.value();
}
}
32 changes: 32 additions & 0 deletions src/main/java/sopt/org/umbbaServer/error/SuccessType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package sopt.org.umbbaServer.error;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public enum SuccessType {

/**
* 200 OK
*/
LOGIN_SUCCESS(HttpStatus.OK, "로그인에 성공했습니다."),
REFRESH_SUCCESS(HttpStatus.OK, "Access 토큰 재발급에 성공했습니다."),
LOGOUT_SUCCESS(HttpStatus.OK, "로그아웃에 성공했습니다."),
KAKAO_ACCESS_TOKEN_SUCCESS(HttpStatus.OK, "카카오 엑세스 토큰을 가져오는데 성공했습니다"),

/**
* 201 CREATED
*/
CREATE_BOARD_SUCCESS(HttpStatus.CREATED, "게시물 생성이 완료됐습니다."),
;

private final HttpStatus httpStatus;
private final String message;

public int getHttpStatusCode() {
return httpStatus.value();
}
}
22 changes: 22 additions & 0 deletions src/main/java/sopt/org/umbbaServer/util/AuditingTimeEntity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package sopt.org.umbbaServer.util;

import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AuditingTimeEntity {

@CreatedDate
private LocalDateTime createdAt;

@LastModifiedDate
private LocalDateTime updatedAt;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package sopt.org.umbbaServer.controller;
package sopt.org.umbbaServer.util;

import lombok.RequiredArgsConstructor;
import org.springframework.core.env.Environment;
Expand All @@ -19,4 +19,4 @@ public String getProfile() {
.findFirst()
.orElse("");
}
}
}
107 changes: 107 additions & 0 deletions src/main/java/sopt/org/umbbaServer/util/slack/SlackApi.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package sopt.org.umbbaServer.util.slack;

import com.slack.api.Slack;
import com.slack.api.model.block.Blocks;
import com.slack.api.model.block.LayoutBlock;
import com.slack.api.model.block.composition.BlockCompositions;
import com.slack.api.webhook.WebhookPayloads;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Date;
import java.util.List;

import static com.slack.api.model.block.composition.BlockCompositions.plainText;

@Component
@RequiredArgsConstructor
@Slf4j
public class SlackApi {

//application.yml 에 등록해놓은 webhookUrl
@Value("${slack.webhook.url}")
private String webhookUrl;
private final static String NEW_LINE = "\n";
private final static String DOUBLE_NEW_LINE = "\n\n";

private StringBuilder sb = new StringBuilder();


// Slack으로 알림 보내기
public void sendAlert(Exception error, HttpServletRequest request) throws IOException {

// 현재 프로파일이 특정 프로파일이 아니면 알림보내지 않기
// if (!env.getActiveProfiles()[0].equals("set1")) {
// return;
// }

// 메시지 내용인 LayoutBlock List 생성
List<LayoutBlock> layoutBlocks = generateLayoutBlock(error, request);

// 슬랙의 send API과 webhookURL을 통해 생성한 메시지 내용 전송
Slack.getInstance().send(webhookUrl, WebhookPayloads
.payload(p ->
// 메시지 전송 유저명
p.username("Exception is detected 🚨")
// 메시지 전송 유저 아이콘 이미지 URL
.iconUrl("https://yt3.googleusercontent.com/ytc/AGIKgqMVUzRrhoo1gDQcqvPo0PxaJz7e0gqDXT0D78R5VQ=s900-c-k-c0x00ffffff-no-rj")
// 메시지 내용
.blocks(layoutBlocks)));
}

// 전체 메시지가 담긴 LayoutBlock 생성
private List<LayoutBlock> generateLayoutBlock(Exception error, HttpServletRequest request) {
return Blocks.asBlocks(
getHeader("서버 측 오류로 예상되는 예외 상황이 발생하였습니다."),
Blocks.divider(),
getSection(generateErrorMessage(error)),
Blocks.divider(),
getSection(generateErrorPointMessage(request)),
Blocks.divider(),
// 이슈 생성을 위해 프로젝트의 Issue URL을 입력하여 바로가기 링크를 생성
getSection("<https://github.com/Team-Umbba/Umbba-Server/issues|이슈 생성하러 가기>")
);
}

// 예외 정보 메시지 생성
private String generateErrorMessage(Exception error) {
sb.setLength(0);
sb.append("*[🔥 Exception]*" + NEW_LINE + error.toString() + DOUBLE_NEW_LINE);
sb.append("*[📩 From]*" + NEW_LINE + readRootStackTrace(error) + DOUBLE_NEW_LINE);

return sb.toString();
}

// HttpServletRequest를 사용하여 예외발생 요청에 대한 정보 메시지 생성
private String generateErrorPointMessage(HttpServletRequest request) {
sb.setLength(0);
sb.append("*[🧾세부정보]*" + NEW_LINE);
sb.append("Request URL : " + request.getRequestURL().toString() + NEW_LINE);
sb.append("Request Method : " + request.getMethod() + NEW_LINE);
sb.append("Request Time : " + new Date() + NEW_LINE);

return sb.toString();
}

// 예외발생 클래스 정보 return
private String readRootStackTrace(Exception error) {
return error.getStackTrace()[0].toString();
}

// 에러 로그 메시지의 제목 return
private LayoutBlock getHeader(String text) {
return Blocks.header(h -> h.text(
plainText(pt -> pt.emoji(true)
.text(text))));
}

// 에러 로그 메시지 내용 return
private LayoutBlock getSection(String message) {
return Blocks.section(s ->
s.text(BlockCompositions.markdownText(message)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package sopt.org.umbbaServer.util.slack;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import sopt.org.umbbaServer.error.ApiResponse;

@RestController
@RequestMapping("/test")
public class SlackTestController {

@GetMapping
@ResponseStatus(HttpStatus.OK)
public ApiResponse test() {
throw new IllegalArgumentException();
}
}