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

[Feature/#243] 디스코드 알림 연동 #247

Merged
merged 11 commits into from
Oct 24, 2024
9 changes: 9 additions & 0 deletions linkmind/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ dependencies {

implementation 'io.sentry:sentry-spring-boot-starter:5.7.0'

// openfeign
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'

}

dependencyManagement {
imports {
mavenBom("org.springframework.cloud:spring-cloud-dependencies:2023.0.3")
}
}
//sourceSets {
// main {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cglib.core.Local;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

import javax.annotation.PostConstruct;
import java.time.LocalTime;
import java.util.TimeZone;

@SpringBootApplication
@EnableFeignClients
@EnableJpaAuditing
public class ToasterApplication {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.method.annotation.HandlerMethodValidationException;

import com.app.toaster.common.dto.ApiResponse;
import com.app.toaster.exception.Error;
import com.app.toaster.exception.model.CustomException;
import com.app.toaster.external.client.discord.DiscordMessageProvider;
import com.app.toaster.external.client.discord.NotificationType;
import com.app.toaster.external.client.slack.SlackApi;

import io.sentry.Sentry;
Expand All @@ -39,87 +42,88 @@
@RequiredArgsConstructor
public class ControllerExceptionAdvice {
private final SlackApi slackApi;
private final DiscordMessageProvider discordMessageProvider;

/**
* custom error
*/
@ExceptionHandler(CustomException.class)
protected ResponseEntity<ApiResponse> handleCustomException(CustomException e) {
protected ResponseEntity<ApiResponse> handleCustomException(CustomException e , WebRequest request) {
Sentry.captureException(e);
return ResponseEntity.status(e.getHttpStatus())
.body(ApiResponse.error(e.getError(), e.getMessage()));
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
protected ResponseEntity<ApiResponse> handleConstraintDefinitionException(final MethodArgumentNotValidException e) {
protected ResponseEntity<ApiResponse> handleConstraintDefinitionException(final MethodArgumentNotValidException e, WebRequest request) {
FieldError fieldError = e.getBindingResult().getFieldError();
Sentry.captureException(e);
return ResponseEntity.status(e.getStatusCode())
.body(ApiResponse.error(Error.BAD_REQUEST_VALIDATION, fieldError.getDefaultMessage()));
}

@ExceptionHandler(MalformedURLException.class)
protected ResponseEntity<ApiResponse> handleConstraintDefinitionException(final MalformedURLException e) {
protected ResponseEntity<ApiResponse> handleConstraintDefinitionException(final MalformedURLException e, WebRequest request) {
Sentry.captureException(e);
return ResponseEntity.status(Error.MALFORMED_URL_EXEPTION.getErrorCode())
.body(ApiResponse.error(Error.MALFORMED_URL_EXEPTION, Error.MALFORMED_URL_EXEPTION.getMessage()));
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(DateTimeParseException.class)
protected ResponseEntity<ApiResponse> handleDateTimeParseException(final DateTimeParseException e) {
protected ResponseEntity<ApiResponse> handleDateTimeParseException(final DateTimeParseException e, WebRequest request) {
return ResponseEntity.status(Error.BAD_REQUEST_REMIND_TIME.getErrorCode())
.body(ApiResponse.error(Error.BAD_REQUEST_REMIND_TIME, Error.BAD_REQUEST_REMIND_TIME.getMessage()));
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
protected ResponseEntity<ApiResponse> handleHttpRequestMethodNotSupportedException(final HttpRequestMethodNotSupportedException e) {
protected ResponseEntity<ApiResponse> handleHttpRequestMethodNotSupportedException(final HttpRequestMethodNotSupportedException e, WebRequest request) {
return ResponseEntity.status(e.getStatusCode())
.body(ApiResponse.error(Error.REQUEST_METHOD_VALIDATION_EXCEPTION, Error.REQUEST_METHOD_VALIDATION_EXCEPTION.getMessage()));
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
protected ResponseEntity<ApiResponse> handleHttpMediaTypeNotSupportedException(final HttpMediaTypeNotSupportedException e) {
protected ResponseEntity<ApiResponse> handleHttpMediaTypeNotSupportedException(final HttpMediaTypeNotSupportedException e, WebRequest request) {
return ResponseEntity.status(e.getStatusCode())
.body(ApiResponse.error(Error.REQUEST_MEDIA_TYPE_VALIDATION_EXCEPTION, Error.REQUEST_MEDIA_TYPE_VALIDATION_EXCEPTION.getMessage()));
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MissingServletRequestParameterException.class)
protected ResponseEntity<ApiResponse> MissingServletRequestParameterException(final MissingServletRequestParameterException e) {
protected ResponseEntity<ApiResponse> MissingServletRequestParameterException(final MissingServletRequestParameterException e, WebRequest request) {
return ResponseEntity.status(e.getStatusCode())
.body(ApiResponse.error(Error.BAD_REQUEST_VALIDATION, Error.BAD_REQUEST_VALIDATION.getMessage()));
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(HttpMessageNotReadableException.class)
protected ResponseEntity<ApiResponse> MissingServletRequestParameterException(final HttpMessageNotReadableException e) {
protected ResponseEntity<ApiResponse> MissingServletRequestParameterException(final HttpMessageNotReadableException e, WebRequest request) {
return ResponseEntity.status(Error.BAD_REQUEST_VALIDATION.getErrorCode())
.body(ApiResponse.error(Error.BAD_REQUEST_VALIDATION, Error.BAD_REQUEST_VALIDATION.getMessage()));
}

@ResponseStatus(HttpStatus.BAD_REQUEST) // 커스텀 validation 에러 핸들링.
@ExceptionHandler(HandlerMethodValidationException.class)
protected ResponseEntity<ApiResponse> ConstraintViolationException(final HandlerMethodValidationException e) {
protected ResponseEntity<ApiResponse> ConstraintViolationException(final HandlerMethodValidationException e, WebRequest request) {
Sentry.captureException(e);
return ResponseEntity.status(Error.BAD_REQUEST_VALIDATION.getErrorCode())
.body(ApiResponse.error(Error.BAD_REQUEST_VALIDATION, Error.BAD_REQUEST_VALIDATION.getMessage()));
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(UnknownHostException.class)
protected ResponseEntity<ApiResponse> UnknownHostException(final UnknownHostException e) {
protected ResponseEntity<ApiResponse> UnknownHostException(final UnknownHostException e, WebRequest request) {
Sentry.captureException(e);
return ResponseEntity.status(Error.BAD_REQUEST_VALIDATION.getErrorCode())
.body(ApiResponse.error(Error.BAD_REQUEST_VALIDATION, Error.BAD_REQUEST_VALIDATION.getMessage()));
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MissingRequestHeaderException.class)
protected ResponseEntity<ApiResponse> MissingRequestHeaderException(final MissingRequestHeaderException e){
protected ResponseEntity<ApiResponse> MissingRequestHeaderException(final MissingRequestHeaderException e, WebRequest request){
Sentry.captureException(e);
return ResponseEntity.status(Error.BAD_REQUEST_VALIDATION.getErrorCode())
.body(ApiResponse.error(Error.BAD_REQUEST_VALIDATION, Error.BAD_REQUEST_VALIDATION.getMessage()));
Expand All @@ -134,8 +138,9 @@ protected ResponseEntity<ApiResponse> MissingRequestHeaderException(final Missin
@ExceptionHandler(Exception.class)
protected ApiResponse<Object> handleException(final Exception error, final HttpServletRequest request) throws
IOException {
slackApi.sendAlert(error, request);
// slackApi.sendAlert(error, request);
Sentry.captureException(error);
discordMessageProvider.sendNotification(NotificationType.ERROR,error,request.getRequestURI());
return ApiResponse.error(Error.INTERNAL_SERVER_ERROR);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
package com.app.toaster.controller;

import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import com.app.toaster.controller.request.toast.IsReadDto;
import com.app.toaster.controller.response.toast.IsReadResponse;
import com.app.toaster.domain.User;
import com.app.toaster.exception.Error;
import com.app.toaster.exception.model.NotFoundException;
import com.app.toaster.infrastructure.UserRepository;

import lombok.RequiredArgsConstructor;

@RestController
public class HealthCheckController {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public enum Error {
CREATE_PUBLIC_KEY_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "publickey 생성 과정 중 문제가 발생했습니다."),
FAIL_TO_SEND_PUSH_ALARM(HttpStatus.INTERNAL_SERVER_ERROR, "다수기기 푸시메시지 전송 실패"),
FAIL_TO_SEND_SQS(HttpStatus.INTERNAL_SERVER_ERROR, "sqs 전송 실패"),
INVALID_DISCORD_MESSAGE(HttpStatus.INTERNAL_SERVER_ERROR, "디스코드 알림 전송 실패"),

CREATE_TOAST_PROCCESS_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "토스트 저장 중 문제가 발생했습니다. 카테고리 또는 s3 관련 문제로 예상됩니다.")
;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.app.toaster.external.client.discord;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@FeignClient(name = "${discord.sign-name}", url = "${discord.webhook-url-error}")
public interface DiscordErrorClient {
@PostMapping(produces = MediaType.APPLICATION_JSON_VALUE)
void sendMessage(@RequestBody DiscordMessage discordMessage);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.app.toaster.external.client.discord;

import java.util.List;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class DiscordMessage {

private String content;
private List<Embed> embeds;

@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public static class Embed {

private String title;
private String description;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.app.toaster.external.client.discord;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContextException;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;

import com.app.toaster.exception.Error;
import com.app.toaster.exception.model.CustomException;
import com.app.toaster.infrastructure.UserRepository;

import feign.FeignException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Component
public class DiscordMessageProvider {
private final DiscordSingUpClient discordSingUpClient;
private final UserRepository userRepository;
private final Environment environment;

public void sendNotification(NotificationType type, Exception e, String request) {
if (!Arrays.asList(environment.getActiveProfiles()).contains("local")) {
try {
switch (type){
case ERROR -> discordSingUpClient.sendMessage(createErrorMessage(e,request));
case SINGUP -> discordSingUpClient.sendMessage(createSingUpMessage());
}
} catch (FeignException error) {
throw new CustomException(Error.INVALID_DISCORD_MESSAGE,
Error.INVALID_APPLE_IDENTITY_TOKEN.getMessage());
}
}
}

private DiscordMessage createSingUpMessage() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

옹 좋은데 SignUpMessage()로 오타정도만 나중에 수정해주면 좋을거같네용☺️

return DiscordMessage.builder()
.content("# 😍 회원가입 이벤트가 발생했습니다.")
.embeds(
List.of(
DiscordMessage.Embed.builder()
.title("ℹ️ 회원가입 정보")
.description(
"### 🕖 발생 시간\n"
+ LocalDateTime.now()
+ "\n"
+ "### 📜 유저 가입 정보\n"
+ "토스터의 " + userRepository.count() + "번째 유저가 생성되었습니다!! ❤️"
+ "\n")
.build()
)
)
.build();
}


private DiscordMessage createErrorMessage(Exception e, String requestUrl) {
return DiscordMessage.builder()
.content("# 🚨 삐용삐용 에러났어요 에러났어요")
.embeds(
List.of(
DiscordMessage.Embed.builder()
.title("ℹ️ 에러 정보")
.description(
"### 🕖 발생 시간\n"
+ LocalDateTime.now()
+ "\n"
+ "### 🔗 요청 URL\n"
+ requestUrl
+ "\n"
+ "### 📄 Stack Trace\n"
+ "```\n"
+ getStackTrace(e).substring(0, 1000)
+ "\n```")
.build()
)
)
.build();
}

private String createRequestFullPath(WebRequest webRequest) {
HttpServletRequest request = ((ServletWebRequest) webRequest).getRequest();
String fullPath = request.getMethod() + " " + request.getRequestURL();

String queryString = request.getQueryString();
if (queryString != null) {
fullPath += "?" + queryString;
}

return fullPath;
}

private String getStackTrace(Exception e) {
StringWriter stringWriter = new StringWriter();
e.printStackTrace(new PrintWriter(stringWriter));
return stringWriter.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.app.toaster.external.client.discord;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@FeignClient(name = "${discord.error-name}", url = "${discord.webhook-url-sign}")
public interface DiscordSingUpClient {
@PostMapping(produces = MediaType.APPLICATION_JSON_VALUE)
void sendMessage(@RequestBody DiscordMessage discordMessage);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.app.toaster.external.client.discord;

public enum NotificationType {
ERROR,
SINGUP
}
Loading