Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

코딩블로그

[Spring] 프로젝트 협업 툴 디스코드 연동해서 에러 메세지 받아보기 (Feat. feign, logback.xml) 본문

PopcornMate

[Spring] 프로젝트 협업 툴 디스코드 연동해서 에러 메세지 받아보기 (Feat. feign, logback.xml)

_hanbxx_ 2024. 4. 3. 20:46
728x90

이번에 팝콘메이트 하면서 통신할 때 에러 코드나 짤막한 에러 원인을 받아 볼 수 있게 하였는데, 그 정보들만 가지고 원인을 해결하지 못할 때가 있다 보니 리팩토링 할 겸 팀 공용 디스코드에 로그 전체를 볼 수 있는 시스템을 만들어 보려고 한다

 

먼저 팀이 사용하는 디스코드에서 웹후크를 만들려면 채널 설정 -> 연동 들어가서 웹후크 만들기를 누르면 된다! 간단한 작업이다

 

본격적으로 코드 구현에 대해 알아보자

(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

 

Discord로 Spring Logback 설정하기

Logback이란 log4j 이후에 출시된 Java 기반 Logging Framework 중 하나로 가장 널리 사용되고 있다. SLF4j 의 구현체이며 Spring Boot 환경이라면 별도의 dependency 추가 없이 기본적으로 포함되어 있다.application.

velog.io


+ Feign으로 디스코드 연동하는 부분은 이 분 블로그 참고하면서 도움 많이 되었다

https://velog.io/@eomgerm/AvAb-Spring-Discord-Webhook%EC%9C%BC%EB%A1%9C-%EC%97%90%EB%9F%AC-%EC%83%81%ED%99%A9-%EC%95%8C%EB%A6%BC-%EB%B0%9B%EA%B8%B0

 

[AvAb] Spring + Discord Webhook으로 에러 상황 알림 받기

아브아브의 데모데이도 얼마 남지 않았습니다. 서버는 얼추 기능 개발이 종료되고 리팩터링할 것들을 찾아보고 있는데요. 그 중에서 프런트엔드와 소통하면서 겪었던 문제 상황을 자동화해 보

velog.io