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

[#414] 실시간 에러감지를 위한 메신져 알림 로직 구현 #415

Merged
merged 6 commits into from
Dec 11, 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
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ dependencies {
// Embedded Redis
testImplementation ('it.ozimov:embedded-redis:0.7.2') { exclude group: "org.slf4j", module: "slf4j-simple" }
implementation group: 'it.ozimov', name: 'embedded-redis', version: '0.7.2'

// Slack
implementation 'com.slack.api:slack-api-client:1.29.0'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.festival.common.exception;

import com.festival.common.infra.Alert.discord.DiscordService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
Expand All @@ -14,25 +17,29 @@

@Slf4j
@RestControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionHandler {

@Value("${spring.servlet.multipart.max-file-size}")
private String maxFileSize;

// private final SlackService slackService;
private final DiscordService discordService;

/**
* 서비스 로직 도중 발생하는 에러들을 커스텀하여 응답값을 내려줍니다.
*/
@ExceptionHandler(CustomException.class)
public ResponseEntity<ErrorResponse> handleCustomException(CustomException e){
public ResponseEntity<ErrorResponse> handleCustomException(CustomException e, HttpServletRequest request){
ErrorCode errorCode = e.getErrorCode();
discordService.sendDiscordAlertLog(e, request);
log.error(errorCode.getMessage());
return ResponseEntity.status(errorCode.getStatus()).body( new ErrorResponse(errorCode.getMessage()));
}

@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<ErrorResponse> handleMaxSizeException() {
String errorMessage = maxFileSize + " 이내 용량의 파일들만 업로드 할 수 있습니다.";
log.error(errorMessage);
return ResponseEntity.badRequest()
.body(new ErrorResponse(errorMessage));
}
Expand All @@ -46,7 +53,6 @@ public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgu
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors()
.forEach(c -> errors.put(((FieldError) c).getField(), c.getDefaultMessage()));
log.error(ex.getMessage());
return ResponseEntity.badRequest().body(errors);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.festival.common.infra.Alert.discord;

import com.festival.common.exception.CustomException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.awt.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Slf4j
@Component
@RequiredArgsConstructor
public class DiscordService {

@Value("${webhook.discord.url}")
private String discordUrl;

public void sendDiscordAlertLog(CustomException ex, HttpServletRequest request) {
try {
DiscordUtil discordUtil = new DiscordUtil(discordUrl);
String registeredTimeFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now());

discordUtil.setUsername("Millicon");
discordUtil.setAvatarUrl("https://raw.githubusercontent.com/angelSooho/InfraTest/main/images/millicon-black.png");

DiscordUtil.EmbedObject embedObject = new DiscordUtil.EmbedObject()
.setTitle("\uD83D\uDEA8 서버에 에러가 감지되었습니다. 즉시 확인이 필요합니다. \uD83D\uDEA8")
.setColor(Color.RED)
// .setFooter("여기는 footer 입니다 ", "https://i.imgur.com/Hv0xNBm.jpeg") // 푸터
// .setThumbnail("https://i.imgur.com/oBPXx0D.png") // 썸네일 이미지
// .setImage("https://i.imgur.com/8nLFCVP.png") // 메인 이미지
.addField("Request IP", request.getRemoteAddr(), true)
.addField("Request URL", request.getRequestURL() + " " + request.getMethod(), true)
.addField("Error Code", ex.getErrorCode().getStatus().toString(), false)
.addField("Error Message", ex.getErrorCode().getMessage(), true)
.addField("발생 시간", registeredTimeFormat, false);

discordUtil.addEmbed(embedObject);
discordUtil.execute();
} catch (Exception e) {
log.debug("Discord 통신 과정에 예외 발생");
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
package com.festival.common.infra.Alert.discord;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

import javax.net.ssl.HttpsURLConnection;
import java.awt.*;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.net.URL;
import java.util.List;
import java.util.*;

@Setter
public class DiscordUtil {

private final String url;
private String content;
private String username;
private String avatarUrl;
private boolean tts;
private List<EmbedObject> embedObjects = new ArrayList<>();

public DiscordUtil(String url) {
this.url = url;
}
public void addEmbed(EmbedObject embed) {
this.embedObjects.add(embed);
}

public void execute() throws IOException {
if (this.content == null && this.embedObjects.isEmpty()) {
throw new IllegalArgumentException("Set content or add at least one EmbedObject");
}

JSONObject json = new JSONObject();

json.put("content", this.content);
json.put("username", this.username);
json.put("avatar_url", this.avatarUrl);
json.put("tts", this.tts);

if (!this.embedObjects.isEmpty()) {
List<JSONObject> embedObjects = new ArrayList<>();

for (EmbedObject embed : this.embedObjects) {
JSONObject jsonEmbed = new JSONObject();

jsonEmbed.put("title", embed.getTitle());
jsonEmbed.put("description", embed.getDescription());
jsonEmbed.put("url", embed.getUrl());

if (embed.getColor() != null) {
Color color = embed.getColor();
int rgb = color.getRed();
rgb = (rgb << 8) + color.getGreen();
rgb = (rgb << 8) + color.getBlue();

jsonEmbed.put("color", rgb);
}

EmbedObject.Footer footer = embed.getFooter();
EmbedObject.Image image = embed.getImage();
EmbedObject.Thumbnail thumbnail = embed.getThumbnail();
EmbedObject.Author author = embed.getAuthor();
List<EmbedObject.Field> fields = embed.getFields();

if (footer != null) {
JSONObject jsonFooter = new JSONObject();

jsonFooter.put("text", footer.getText());
jsonFooter.put("icon_url", footer.getIconUrl());
jsonEmbed.put("footer", jsonFooter);
}

if (image != null) {
JSONObject jsonImage = new JSONObject();

jsonImage.put("url", image.getUrl());
jsonEmbed.put("image", jsonImage);
}

if (thumbnail != null) {
JSONObject jsonThumbnail = new JSONObject();

jsonThumbnail.put("url", thumbnail.getUrl());
jsonEmbed.put("thumbnail", jsonThumbnail);
}

if (author != null) {
JSONObject jsonAuthor = new JSONObject();

jsonAuthor.put("name", author.getName());
jsonAuthor.put("url", author.getUrl());
jsonAuthor.put("icon_url", author.getIconUrl());
jsonEmbed.put("author", jsonAuthor);
}

List<JSONObject> jsonFields = new ArrayList<>();
for (EmbedObject.Field field : fields) {
JSONObject jsonField = new JSONObject();

jsonField.put("name", field.getName());
jsonField.put("value", field.getValue());
jsonField.put("inline", field.isInline());

jsonFields.add(jsonField);
}

jsonEmbed.put("fields", jsonFields.toArray());
embedObjects.add(jsonEmbed);
}

json.put("embeds", embedObjects.toArray());
}

URL url = new URL(this.url);
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.addRequestProperty("Content-Type", "application/json");
connection.addRequestProperty("User-Agent", "Java-DiscordWebhook-BY-Gelox_");
connection.setDoOutput(true);
connection.setRequestMethod("POST");

OutputStream stream = connection.getOutputStream();
stream.write(json.toString().getBytes());
stream.flush();
stream.close();

connection.getInputStream().close();
connection.disconnect();
}

@Getter
public static class EmbedObject {
private String title;
private String description;
private String url;
private Color color;

private Footer footer;
private Thumbnail thumbnail;
private Image image;
private Author author;
private List<Field> fields = new ArrayList<>();

public EmbedObject setTitle(String title) {
this.title = title;
return this;
}

public EmbedObject setDescription(String description) {
this.description = description;
return this;
}

public EmbedObject setUrl(String url) {
this.url = url;
return this;
}

public EmbedObject setColor(Color color) {
this.color = color;
return this;
}

public EmbedObject setFooter(String text, String icon) {
this.footer = new Footer(text, icon);
return this;
}

public EmbedObject setThumbnail(String url) {
this.thumbnail = new Thumbnail(url);
return this;
}

public EmbedObject setImage(String url) {
this.image = new Image(url);
return this;
}

public EmbedObject setAuthor(String name, String url, String icon) {
this.author = new Author(name, url, icon);
return this;
}

public EmbedObject addField(String name, String value, boolean inline) {
this.fields.add(new Field(name, value, inline));
return this;
}

@Getter
@AllArgsConstructor
private class Footer {
private String text;
private String iconUrl;
}

@Getter
@AllArgsConstructor
private class Thumbnail {
private String url;
}

@Getter
@AllArgsConstructor
private class Image {
private String url;
}

@Getter
@AllArgsConstructor
private class Author {
private String name;
private String url;
private String iconUrl;
}

@Getter
@AllArgsConstructor
private class Field {
private String name;
private String value;
private boolean inline;
}
}

private class JSONObject {

private final HashMap<String, Object> map = new HashMap<>();

void put(String key, Object value) {
if (value != null) {
map.put(key, value);
}
}

@Override
public String toString() {
StringBuilder builder = new StringBuilder();
Set<Map.Entry<String, Object>> entrySet = map.entrySet();
builder.append("{");

int i = 0;
for (Map.Entry<String, Object> entry : entrySet) {
Object val = entry.getValue();
builder.append(quote(entry.getKey())).append(":");

if (val instanceof String) {
builder.append(quote(String.valueOf(val)));
} else if (val instanceof Integer) {
builder.append(Integer.valueOf(String.valueOf(val)));
} else if (val instanceof Boolean) {
builder.append(val);
} else if (val instanceof JSONObject) {
builder.append(val.toString());
} else if (val.getClass().isArray()) {
builder.append("[");
int len = Array.getLength(val);
for (int j = 0; j < len; j++) {
builder.append(Array.get(val, j).toString()).append(j != len - 1 ? "," : "");
}
builder.append("]");
}

builder.append(++i == entrySet.size() ? "}" : ",");
}

return builder.toString();
}

private String quote(String string) {
return "\"" + string + "\"";
}
}
}
Loading