๐์ด๊ฑธ ์?
์๋ฒ๋ฅผ ๋ฐฐํฌํ๊ฒ ๋๋ฉด ๋ก๊ทธ๋ฅผ ์ผ๋ณด์ง ์๋ ์ด์ ์๋ฌ ๋ฐ์ ์ฌ๋ถ์ ๊ดํด ์๊ธฐ ํ๋ค๋ค
๋ก๊ทธ๋ฅผ ๋ณด๋ ๊ฒ๋ณด๋ค ๋ ํธํ๊ฒ ์ ์ ์๋ ๋ฐฉ๋ฒ์ด ์์๊น?
์๋ฌ๋ฅผ ์ฌ๋์ด๋ ๋์ค์ฝ๋ ์๋ฆผ์ฒ๋ผ ๋ณผ ์ ์์ผ๋ฉด ์ด๋จ๊น?
์๋ฌ๊ฐ ๋ฐ์ํ๋ฉด ์ฌ๋ ์๋์ด ์ค๋๋ก ๊ตฌํํด ๋ณด๋๋ก ํ์
๐ ์๋ฆผ ๊ตฌํํ๊ธฐ
๐ต ์ด๋ค ๋ฐฉ์์ผ๋ก ๊ตฌํํ๋?
Global Exception Handler์์ 500๋ฒ๋ ์๋ฌ๋ฅผ ์ฒ๋ฆฌํ๋ ๊ณณ์ slack ๋ฉ์์ง๋ฅผ ๋ณด๋ด๋ ๋ก์ง์ ์ถ๊ฐํ๋ค.
์ด๋ฅผ ํตํด 500๋ฒ๋ error๊ฐ ๋ฐ์ํ๋ฉด, ์๋ฌ ๋ด์ฉ๊ณผ request ์ ๋ณด์ ๊ดํด slack ๋ฉ์์ง๋ก ๋ณด๋ด์ง๋๋ก ํ๋ค.
๋ํ dev์ prod๋ฅผ ๊ตฌ๋ถํ๋ค.
์ฌ์ค ์ค ์๋ฒ์์๋ง ์๋ํ๋๋ก ๊ตฌํํด๋ ๋๋ค.
ํ์ง๋ง ๊ฐ๋ฐ ์ํฉ์์๋ 500๋ฒ ์๋ฌ๋ฅผ ๋ฐ๊ฒฌํ๊ธฐ ์ด๋ ต๋ค๊ณ ์๊ฐํด์, ๋ ์ํฉ์ ๊ตฌ๋ถ ์ง์ด ์๋์ ๋ณด๋ผ ์ ์๋๋ก ํ๋ค.
๐ต GlobalExceptionHandler
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
// ... ์๋ต
// ์์์ ๋ฐ๋ก ์ฒ๋ฆฌํ์ง ์์ ์๋ฌ๋ฅผ ๋ชจ๋ ์ฒ๋ฆฌํด์ค๋๋ค.
@ExceptionHandler(Exception.class)
protected ResponseEntity<ErrorResponse> handleException(Exception exception, HttpServletRequest httpServletRequest) {
log.error("handleException", exception);
final ContentCachingRequestWrapper contentCachingRequestWrapper
= new ContentCachingRequestWrapper(httpServletRequest);
Event.raise(SlackErrorMessage.of(exception, contentCachingRequestWrapper));
return new ResponseEntity<>(ErrorResponse.onFailure(ErrorCode._INTERNAL_SERVER_ERROR),
null, INTERNAL_SERVER_ERROR);
}
}
๊ธฐ์กด ๋ก์ง์ Exception class๋ฅผ ํตํด ์๋ฌ๊ฐ ์ฒ๋ฆฌ๋ ๊ฒฝ์ฐ 500๋ฒ ์๋ฌ๊ฐ ๋ฐ์ํ๋๋ก ๊ตฌํํ๋ค.
์ด ๋ถ๋ถ์ Slack ์๋ฆผ์ ๋ณด๋ผ ์ ์๋ event๋ฅผ ๋ฐ์ํ ์ ์๋๋ก ํ๋ค.
HttpServletRequest ๊ฐ์ ์ฝ์ ๋, ๊ฐ์ ๊ฐ์ ์ฌ๋ฌ ๋ฒ ์กฐํํ ์ ์๋ค.
๊ทธ๋์ HttpServletRequest๋ฅผ ์บ์ฑํด์ค ์ ์๋ ContentCachingRequestWrapper๋ฅผ ์ด์ฉํ๋ค.
๐ต SlackSendMessageHandler
@Component
@RequiredArgsConstructor
@Slf4j
public class SlackSendMessageHandler {
private final SlackMessageGenerater slackMessageGenerater;
private final SlackHelper slackHelper;
@Async
@EventListener(SlackErrorMessage.class)
public void Handle(SlackErrorMessage slackErrorMessage) throws IOException {
final Exception exception = slackErrorMessage.getException();
final ContentCachingRequestWrapper contentCachingRequestWrapper
= slackErrorMessage.getContentCachingRequestWrapper();
final Payload payload = slackMessageGenerater.generate(exception, contentCachingRequestWrapper);
slackHelper.sendNotification(payload);
}
}
event๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ ์ ํด๋์ค์ handle ํจ์๊ฐ ์๋๋๋ค.
slackMessageGenerater๋ฅผ ํตํด ๋ฉ์์ง๋ฅผ ๋ง๋ ํ, slackHelper๋ฅผ ํตํด ๋ฉ์์ง๋ฅผ ์ ์กํ๋ค.
๐ต SlackMessageGenerator
@Component
@AllArgsConstructor
@Slf4j
public class SlackMessageGenerater {
private final int MaxLen = 500;
private final ObjectMapper objectMapper;
public List<LayoutBlock> generate(Exception e, ContentCachingRequestWrapper cachedRequest) throws IOException {
List<LayoutBlock> layoutBlocks = new ArrayList<>();
// ์ ๋ชฉ
layoutBlocks.add(HeaderBlock.builder().text(plainText("์๋ฌ ์๋ฆผ")).build());
// ๊ตฌ๋ถ์
layoutBlocks.add(new DividerBlock());
// IP + Method, Addr
layoutBlocks.add(makeSection(getIP(cachedRequest), getAddr(cachedRequest)));
// RequestBody + RequestParam
layoutBlocks.add(makeSection(getBody(cachedRequest), getParam(cachedRequest)));
// ๊ตฌ๋ถ์
layoutBlocks.add(new DividerBlock());
// IP + Method, Addr
layoutBlocks.add(makeSection(getErrMessage(e), getErrStack(e)));
return layoutBlocks;
}
private LayoutBlock makeSection(TextObject first, TextObject second ) {
return Blocks.section(section -> section.fields(List.of(first, second)));
}
private MarkdownTextObject getIP(ContentCachingRequestWrapper c) {
final String errorIP = c.getRemoteAddr();
return MarkdownTextObject.builder().text("* User IP :*\n" + errorIP).build();
}
private MarkdownTextObject getAddr(ContentCachingRequestWrapper c) {
final String method = c.getMethod();
final String url = c.getRequestURL().toString();
return MarkdownTextObject.builder()
.text("* Request Addr :*\n" + method + " : " + url)
.build();
}
private MarkdownTextObject getBody(ContentCachingRequestWrapper c) throws IOException {
final String body = objectMapper.readTree(c.getContentAsByteArray()).toString();
return MarkdownTextObject.builder().text("* Request Body :*\n" + body).build();
}
private MarkdownTextObject getParam(ContentCachingRequestWrapper c) {
final String queryString = c.getQueryString();
return MarkdownTextObject.builder().text("* Request Param :*\n" + queryString).build();
}
private MarkdownTextObject getErrMessage(Exception e) {
final String errorMessage = e.getMessage();
return MarkdownTextObject.builder().text("* Message :*\n" + errorMessage).build();
}
private MarkdownTextObject getErrStack(Throwable throwable) {
String exceptionAsString = Arrays.toString(throwable.getStackTrace());
int cutLength = Math.min(exceptionAsString.length(), MaxLen);
String errorStack = exceptionAsString.substring(0, cutLength);
return MarkdownTextObject.builder().text("* Stack Trace :*\n" + errorStack).build();
}
}
์ด๊ฒ์ ํตํด ๋ฉ์ธ์ง๋ฅผ ๋ง๋ค๊ฒ ๋๋ค.
๊ฐ์ฌํ๊ฒ๋ Slack API ํจํค์ง์ ์ด๋ฏธ Object๊ฐ ๋ค ๊ตฌํ๋์ด ์์ด์,
์ฐ๋ฆฐ ์ฑ์ ๋ฃ๊ธฐ๋ง ํ๋ฉด ๋๋ค!
์ ์ฝ๋๋๋ก ํ๋ฉด ์ด๋ฐ ์๋ฆผ์ ๋ง๋ค ์ ์๋ค.
๐ต SlackHelper
@Component
@RequiredArgsConstructor
public class SlackHelper {
private final SpringEnvironmentHelper springEnvironmentHelper;
@Value("${slack.webhook.dev_url}")
String devUrl;
@Value("${slack.webhook.prod_url}")
String prodUrl;
public void sendNotification(Payload payload) {
final Slack slack = Slack.getInstance();
try {
if (springEnvironmentHelper.isProdProfile()) {
slack.send(prodUrl, payload);
}
else {
slack.send(devUrl, payload);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
์ฌ๊ธฐ์ ์ฌ๋ ๋ฉ์ธ์ง๋ฅผ ๋ณด๋ด๊ฒ ๋๋ค.
์์์ ๋งํ ๊ฒ๊ณผ ๊ฐ์ด ์ฐ๋ฆฐ dev์ prod๋ฅผ ๊ตฌ๋ถํด์ผ ํ๋ค.
์ปค์คํ ์ ํธ ํด๋์ค์ธ springEnvironmentHelper์ด ์ด๋ฅผ ๊ตฌ๋ถํด ์ค๋ค.
์๋๋ SpringEnvironmentHelper ์ฝ๋์ด๋ค.
@Component
@RequiredArgsConstructor
public class SpringEnvironmentHelper {
private final Environment environment;
private final String PROD = "prod";
private final String DEV = "dev";
public Boolean isProdProfile() {
String[] activeProfiles = environment.getActiveProfiles();
List<String> currentProfile = Arrays.stream(activeProfiles).collect(Collectors.toList());
return currentProfile.contains(PROD);
}
public Boolean isDevProfile() {
String[] activeProfiles = environment.getActiveProfiles();
List<String> currentProfile = Arrays.stream(activeProfiles).collect(Collectors.toList());
return currentProfile.contains(DEV);
}
}
ํ์ฌ ํ๊ฒฝ์ ๋ฐ๋ผ url์ ๊ตฌ๋ถํ์ฌ ๋ฉ์์ง๋ฅผ ์ ์กํ๋๋ก ๊ตฌํํ๋ค.
slack.send์ url๊ณผ message๋ฅผ ๋ฃ๊ณ ํธ์ถํ๋ฉด, ์๋์ด ์ ์ก๋๋ค!
'Infra' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
<Infra> ubuntu ํ๊ฒฝ์์ ์๋ฌธ์ฌํ docker container ์ฌ์ธ ๊ท๋ช (0) | 2023.10.05 |
---|---|
<Infra> Spring Slack ๋ฉ์ธ์ง ์ ์ก (Incoming Webhook) (0) | 2023.03.27 |