코딩블로그
[Spring] 프로젝트 협업 툴 디스코드 연동해서 에러 메세지 받아보기 (Feat. feign, logback.xml) 본문
[Spring] 프로젝트 협업 툴 디스코드 연동해서 에러 메세지 받아보기 (Feat. feign, logback.xml)
_hanbxx_ 2024. 4. 3. 20:46이번에 팝콘메이트 하면서 통신할 때 에러 코드나 짤막한 에러 원인을 받아 볼 수 있게 하였는데, 그 정보들만 가지고 원인을 해결하지 못할 때가 있다 보니 리팩토링 할 겸 팀 공용 디스코드에 로그 전체를 볼 수 있는 시스템을 만들어 보려고 한다
먼저 팀이 사용하는 디스코드에서 웹후크를 만들려면 채널 설정 -> 연동 들어가서 웹후크 만들기를 누르면 된다! 간단한 작업이다
본격적으로 코드 구현에 대해 알아보자
(1) FeignClient 이용하는 방식
먼저 DiscordClient 클래스에 Controller 클래스에 메서드 짜주는 것처럼 인터셉터를 위한 메서드를 써준다
@FeignClient(
name = "discord-client",
url = "웹후크 URL",
configuration = DiscordFeignConfig.class)
public interface DiscordClient {
@PostMapping
void sendAlarm(@RequestBody DiscordMessage message);
}
그다음에 DiscordFeignConfig에다가 intercept하기 위한 Bean을 등록해 준다
public class DiscordFeignConfig{
@Bean
public RequestInterceptor requestInterceptor() {
return template -> template.header("Content-Type", "application/json;charset=UTF-8");
}
}
Discord 공식 문서에 나와있는 요청 바디는
{
"content": "string"
"embeds": [
{
"title": "string"
"description": "string"
},
...
]
}
이런 식으로 되어있어서 DTO 또한 똑같이 만들어 주었다
@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;
}
}
나는 따로 ErrorHandlerResponse를 만들어서 쓰고 있기 때문에 GlobalExceptionHandler이라는 파일을 따로 만들어서 사용하고 있어 통신할 때 처리해 주는 메서드에 "sendDiscordAlarm"메서드를 만들어 호출해 주었다
@ExceptionHandler(BaseErrorException.class)
public ResponseEntity<ErrorResponse> handleBaseErrorException(
BaseErrorException e, HttpServletRequest request) {
log.error("BaseErrorException", e);
final ErrorReason errorReason = e.getErrorCode().getErrorReason();
final ErrorResponse errorResponse = ErrorResponse.from(errorReason);
if (!Arrays.asList(environment.getActiveProfiles()).contains("local")) {
sendDiscordAlarm(e, request);
}
return ResponseEntity.status(HttpStatus.valueOf(errorReason.getStatus()))
.body(errorResponse);
}
private void sendDiscordAlarm(Exception e, WebRequest request) {
discordClient.sendAlarm(createMessage(e, request));
}
private DiscordMessage createMessage(Exception e, WebRequest request) {
return DiscordMessage.builder()
.content("# 🚨 에러 발생 🚨")
.embeds(
List.of(
DiscordMessage.Embed.builder()
.title("ℹ️ 정보")
.description(
"### 🕖 발생 시간\n"
+ LocalDateTime.now()
+ "\n"
+ "### 🔗 요청 URL\n"
+ createRequestFullPath(request)
+ "\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();
}
여기서 중요한 점은 spring Profile을 사용하고 있다면 local에서 테스트하는 에러 로그들을 굳이 모두가 보는 디스코드 채널에 보낼 필요가 없으니 꼭 local profile이 아닐 때를 고려해서 코드를 짜줘야 한다!!
if (!Arrays.asList(environment.getActiveProfiles()).contains("local")) {
sendDiscordAlarm(e, request);
}
+ 디스코드 채널의 HTTP 연결 허용을 꼭 해줘야 한다. 안 한다면 401 에러가 난다!
하지만 이렇게 Intercept방식으로 하면 통신할 때 쓰는 에러 Response까지 바뀌게 된다..
그래서 그냥 Discord LogBack 형식으로 바꿔서 해보았다
(2) LogBack 방식
위에 디스코드 웹훅 성정은 그대로지만 이번엔 LogBack.xml파일을 설정해주어야 한다
Api 모듈의 Resource 디렉터리에 logback.xml파일을 만들어 주었다
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<springProfile name="local">
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
<springProfile name="dev">
<property resource="application.yml"/>
<springProperty name="DISCORD_WEBHOOK_URL" source="logging.discord.webhook-url"/>
<appender name="DISCORD" class="com.github.napstr.logback.DiscordAppender">
<webhookUri>웹후크URL</webhookUri>
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{HH:mm:ss} [%thread] [%-5level] %logger{36} - %msg%n```%ex{full}```</pattern>
</layout>
<username>에러 비상 비상</username>
<avatarUrl>https://velog.velcdn.com/images/sangyoung23/post/d0e62103-e8d8-4a2e-955f-d5a27137044e/image.png</avatarUrl>
<tts>false</tts>
</appender>
<appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
<charset>utf8</charset>
</encoder>
</appender>
<appender name="ASYNC_DISCORD" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="DISCORD" />
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
<root level="INFO">
<appender-ref ref="ASYNC_DISCORD"/>
<appender-ref ref="Console"/>
</root>
</springProfile>
</configuration>
나는 특히 Spring Profile을 "애용"하고 있는 개발자로서 logback에 spring profile을 지원해 주는 점이 너무 좋았다
이 파일만 추가해 주면
디스코드에 이런 식으로 자세한 로그까지 오는 것을 볼 수 있다
결론
Feign을 이용하는 방법보다 디스코드의 logback형식이 훨씬 더 리소스를 잡아먹지도 않고 나처럼 디스코드 연동 처음 해보는 사람한테 엄청 편할 것 같다
하지만 Feign의 장점으로는 굳이 gradle library를 사용할 필요 없이 api로 통신하면 되는 것이라서 케이스 바이 케이스라는 말이 항상 맞는 것 같다!
대규모 팀 프로젝트나 인원이 많을 때 특히 이러한 협업 툴 연동이 정말 유용할 것 같아 적용해 보았다
출처
https://velog.io/@qwe916/Discord%EB%A1%9C-Spring-Logback-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0
+ Feign으로 디스코드 연동하는 부분은 이 분 블로그 참고하면서 도움 많이 되었다
'PopcornMate' 카테고리의 다른 글
[리팩토링] 멀티모듈에 Spotless 적용하면서 테스트 코드 컨벤션 유지하기 (0) | 2024.04.16 |
---|---|
[Spring]Junit5 Controller 단위 테스트 @AutoConfigureMockMvc, @MockBean, @MockMvc 트러블슈팅 (0) | 2024.04.06 |
멀티 모듈 환경에서 내가 만든 api 테스트해보기 Feat. MockMvc, profileResolver, WithCustomUser (0) | 2024.03.24 |
[Spring] FCM 구축하기 & FCM과 @Scheduled을 이용하여 특정 시간대에 알림 보내기 (4) | 2024.03.11 |
[Spring] Amazons S3 Presigned Url 구현하기 (0) | 2024.03.11 |