Skip to content

Commit

Permalink
Merge pull request #247 from Link-MIND/feature/#243
Browse files Browse the repository at this point in the history
[Feature/#243] λ””μŠ€μ½”λ“œ μ•Œλ¦Ό 연동
  • Loading branch information
mmihye authored Oct 24, 2024
2 parents c89b4b5 + 898fa9d commit 23837ad
Show file tree
Hide file tree
Showing 11 changed files with 231 additions and 13 deletions.
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,15 @@
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.NotificationDto;
import com.app.toaster.external.client.discord.NotificationType;
import com.app.toaster.external.client.slack.SlackApi;

import io.sentry.Sentry;
Expand All @@ -39,87 +43,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 +139,10 @@ 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(
new NotificationDto(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,10 +1,20 @@
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.external.client.discord.DiscordMessageProvider;
import com.app.toaster.external.client.discord.NotificationDto;
import com.app.toaster.external.client.discord.NotificationType;
import com.app.toaster.infrastructure.UserRepository;

import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
public class HealthCheckController {
private final DiscordMessageProvider discordMessageProvider;

@GetMapping("/health")
public String healthCheck() {
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,14 @@
package com.app.toaster.external.client.discord;

import java.net.URI;

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-feign-client", url = "URI")
public interface DiscordClient {
@PostMapping(produces = MediaType.APPLICATION_JSON_VALUE)
void sendMessage(URI uri, @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,118 @@
package com.app.toaster.external.client.discord;

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

import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
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;
import lombok.extern.slf4j.Slf4j;

@RequiredArgsConstructor
@Component
@Slf4j
public class DiscordMessageProvider {
private final DiscordClient discordClient;
private final UserRepository userRepository;
private final Environment environment;

@Value("${discord.webhook-url-error}")
private String webhookUrlError;


@Value("${discord.webhook-url-sign}")
private String webhookUrlSign;

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void sendNotification(NotificationDto notification) {
if (!Arrays.asList(environment.getActiveProfiles()).contains("local")) {
try {
switch (notification.type()){
case ERROR -> discordClient.sendMessage(URI.create(webhookUrlError), createErrorMessage(notification.e(), notification.request()));
case SIGNUP -> discordClient.sendMessage(URI.create(webhookUrlSign), createSignUpMessage());
}
} catch (Exception error) {
log.warn("discord notification fail : " + error);
}
}
}

private DiscordMessage createSignUpMessage() {
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,9 @@
package com.app.toaster.external.client.discord;


public record NotificationDto(
NotificationType type,
Exception e,
String request
) {
}
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,
SIGNUP
}
Loading

0 comments on commit 23837ad

Please sign in to comment.