ASAP 프로젝트에서는 Slack Webhook과 연동해서 예외 처리를 해놓지 않은 서버 내부 오류 발생 시 Slack에 알림을 보내도록 설정했었다.
코드는 👉 참고
그런데 슬랙은 평가판 90일이 지나면 오래된 메시지는 볼 수 없어서 났었던 에러 사항을 볼 수 없다는 점이 아쉬웠다.
새롭게 들어가는 프로젝트에서는 디스코드에 에러 사항을 로깅하기로 결정했고, 그에 대한 글을 남겨보려 한다.
만약에 서버를 로컬에서 돌리고 있다고 생각해보자. 그러면 우리는 콘솔에서 바로 예외를 확인할 수 있을 것이다.
그런데 가상 머신에 서버를 nohup 명령어를 사용해 배포를 해두었다면, 우리는 매번 에러가 발생할 때마다 nohup.out 파일에 들어가서 에러 로그를 확인해야 할 것이다.
그래서 에러 로깅을 할 때 서버 내에 로깅 파일을 두어 확인하거나, 슬랙이나 디스코드 채널을 통해 더 읽기 쉽게 관리한다.
디스코드 서버에 들어가 채널을 만들면, 채널 옆 설정 아이콘을 누른다.
설정에 들어간 후
연동 창에서 웹후크를 만든다. (이름은 자유)
repositories {
mavenCentral()
maven { url 'https://jitpack.io' }
}
dependencies {
... 생략...
implementation('com.github.napstr:logback-discord-appender:1.0.0')
}
의존성을 위처럼 추가해준 후 application.yml 을 설정해준다.
logging:
discord:
webhook-uri: https://discord.com/api/webhooks/1186554772329857085/NiDo5TL7FLdsp9FOurVGp-cqczLSi6QPbBM8X9Z6zY6_nFfKqHfgeztIosCCMxTIhgwH
config: classpath:logback-spring.xml
기존 로깅 관련 글에서 작성했던 LoggingAspect 클래스이다.
@Aspect
@Component
@Slf4j
public class LoggingAspect {
private final ObjectMapper objectMapper = new ObjectMapper();
@Pointcut("execution(* com.laboratory.auth.controller..*(..)) || ( execution(* com.laboratory.auth.common.advice..*(..)) && !execution(* com.laboratory.auth.common.advice.ControllerExceptionAdvice.handleException*(..)))")
public void controllerInfoLevelExecute() {
}
@Pointcut("execution(* com.laboratory.auth.common.advice.ControllerExceptionAdvice.handleException*(..))")
public void controllerErrorLevelExecute() {
}
@Around("com.laboratory.auth.common.aspect.LoggingAspect.controllerInfoLevelExecute()")
public Object requestInfoLevelLogging(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
final ContentCachingRequestWrapper cachingRequest = (ContentCachingRequestWrapper) request;
long startAt = System.currentTimeMillis();
Object returnValue = proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
long endAt = System.currentTimeMillis();
log.info("================================================NEW===============================================");
log.info("====> Request: {} {} ({}ms)\n *Header = {}", request.getMethod(), request.getRequestURL(), endAt - startAt, getHeaders(request));
if ("POST".equalsIgnoreCase(request.getMethod())) {
log.info("====> Body: {}", objectMapper.readTree(cachingRequest.getContentAsByteArray()));
}
if (returnValue != null) {
log.info("====> Response: {}", returnValue);
}
log.info("================================================END===============================================");
return returnValue;
}
@Around("com.laboratory.auth.common.aspect.LoggingAspect.controllerErrorLevelExecute()")
public Object requestErrorLevelLogging(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
final ContentCachingRequestWrapper cachingRequest = (ContentCachingRequestWrapper) request;
long startAt = System.currentTimeMillis();
Object returnValue = proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
long endAt = System.currentTimeMillis();
log.error("====> Request: {} {} ({}ms)\n *Header = {}", request.getMethod(), request.getRequestURL(), endAt - startAt, getHeaders(request));
if ("POST".equalsIgnoreCase(request.getMethod())) {
log.error("====> Body: {}", objectMapper.readTree(cachingRequest.getContentAsByteArray()));
}
if (returnValue != null) {
log.error("====> Response: {}", returnValue);
}
log.error("================================================END===============================================");
return returnValue;
}
private Map<String, Object> getHeaders(HttpServletRequest request) {
Map<String, Object> headerMap = new HashMap<>();
Enumeration<String> headerArray = request.getHeaderNames();
while (headerArray.hasMoreElements()) {
String headerName = headerArray.nextElement();
headerMap.put(headerName, request.getHeader(headerName));
}
return headerMap;
}
}
이 LoggingAspect 파일을 통해서 로깅 상황과, 로깅 방식, 로깅 포맷을 작성해주었다. (이전 블로그 글에 리퀘스트를 Wrapping 하는 내용이 담겨있으니 참고하세요!)
이제 기존에 사용하던 logback-spring.xml 파일에서 discord-appender를 사용해야 한다.
<configuration>
<property name="LOG_PATTERN"
value="[%d{yyyy-MM-dd HH:mm:ss}:%-4relative] %green([%thread]) %highlight(%-5level) %boldWhite([%C.%M:%yellow(%L)]) - %msg%n"/>
<springProperty name="DISCORD_WEBHOOK_URI" source="logging.discord.webhook-uri"/>
<springProfile name="local">
<include resource="console-appender.xml"/>
<include resource="discord-appender.xml"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_DISCORD" />
</root>
</springProfile>
</configuration>
기존에 작성했던 logback-spring.xml에서 discord-appender.xml을 include한다.
<include resource="discord-appender.xml"/>
<!-- 또 아래처럼 appender-ref에 discord-appender에 지정해준 appender를 넣어줘야한다. -->
<appender-ref ref="ASYNC_DISCORD" />
<included>
<appender name="DISCORD" class="com.github.napstr.logback.DiscordAppender">
<webhookUri>${DISCORD_WEBHOOK_URI}</webhookUri>
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %green([%thread]) %highlight(%-5level) %boldWhite([%C.%M:%yellow(%L)]) - %msg%n</pattern>
</layout>
<username>에러났대...</username>
<avatarUrl>https://www.greenart.co.kr/upimage/new_editor/20212/20210201112021.jpg</avatarUrl>
<tts>false</tts>
</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>
</included>
이렇게 xml 파일은 모두 끝났다. 마지막으로 컨트롤러에 RuntimeException을 던지는 테스트 메서드를 만들고 테스트해보자!
@RestController
@RequestMapping("/test")
public class ErrorLoggingTestController {
@GetMapping("")
public void testThrowInternalServerError() {
throw new RuntimeException();
}
}
그리고 RestContollerAdvice를 통해 에러를 잡을 때 다음과 같이 처리해주면,
@Slf4j
@RestControllerAdvice
@RequiredArgsConstructor
public class ControllerExceptionAdvice {
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
protected ErrorResponse handleException(final Exception error, final HttpServletRequest request) throws IOException {
log.error("================================================NEW===============================================");
log.error(error.getMessage(), error);
return ErrorResponse.of(ErrorMessage.INTERNAL_SERVER_ERROR);
}
}
/test
api를 호출하면 아래와 같이 console에도 에러가 나오고, 디스코드에도 에러가 전달되는 것을 볼 수 있다.!
이렇게 서버 에러 로깅을 디스코드에서 해봤다.
기존에 구현했던 파일로 저장하는 방식보다 간편하고 빠르게 확인해볼 수 있어졌다!
다음은 회원 가입같은 이벤트 발생 시 디스코드에 알림을 보내는 것을 구현해보겠다!🔥