Skip to content

Commit

Permalink
slack logger에 access log 정보를 포함한다. #501 (#530)
Browse files Browse the repository at this point in the history
* feat: 커스텀 slackAppender 뼈대 제작

* feat: ServletRequest 캐싱 로직 구현

* refactor: filter 전략 변경

* feat: Slack 알람 관련 interceptor 등록

* chore: webClient를 위한 webflux 의존성 추가.

* feat: 슬랙으로 메세지 전송 기능 구현

* refactor: spring에서 제공해주는 wrapper로 변경

* test: 필요 없는 테스트 제거

* feat: AoP, ThreadLocal 기반으로 변경

* feat: Slack 알림 등록

* refactor: 기존 SlackAppender 제거

* refactor: 사용하지 않는 Appender 제거

* fix: SlackAlarm이 test에서도 전송되는 문제 해결

* refactor: SlackMessage 이름 변경 및 빈등록

* refactor: logger 위치 변경

* refactor: thread local 제거후 request scope 사용

* refactor: 잘못된 token에 대한 에러 핸들링

* refactor: 중복된 flayway 파일 제거

* refactor: objectmapper 주입받아 사용하도록 변경
  • Loading branch information
bperhaps committed Oct 21, 2021
1 parent 8703807 commit 5ee50c0
Show file tree
Hide file tree
Showing 13 changed files with 325 additions and 51 deletions.
4 changes: 3 additions & 1 deletion backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ dependencies {

// log
implementation 'net.rakugakibox.spring.boot:logback-access-spring-boot-starter:2.7.1'
implementation "com.github.maricn:logback-slack-appender:1.4.0"

// jwt
implementation 'io.jsonwebtoken:jjwt:0.9.1'
Expand Down Expand Up @@ -83,6 +82,9 @@ dependencies {
//flyway
implementation 'org.flywaydb:flyway-core:6.4.2'

// webclient
implementation 'org.springframework.boot:spring-boot-starter-webflux'

runtimeOnly 'com.h2database:h2'
runtimeOnly 'mysql:mysql-connector-java'
}
Expand Down
10 changes: 10 additions & 0 deletions backend/src/main/java/wooteco/prolog/common/WebConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.data.web.config.PageableHandlerMethodArgumentResolverCustomizer;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.util.ContentCachingRequestWrapper;
import wooteco.prolog.common.slacklogger.RequestStorage;
import wooteco.prolog.studylog.application.dto.search.SearchArgumentResolver;
import wooteco.support.performance.PerformanceLogger;
import wooteco.support.performance.RequestApiExtractor;
Expand Down Expand Up @@ -38,4 +42,10 @@ public PageableHandlerMethodArgumentResolverCustomizer customize() {
public PerformanceLogger performanceLogger(ObjectMapper objectMapper) {
return new PerformanceLogger(objectMapper, new RequestApiExtractor());
}

@Bean
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public RequestStorage requestStorage() {
return new RequestStorage();
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
package wooteco.prolog.common.exception;

import static wooteco.prolog.common.slacklogger.SlackAlarmErrorLevel.ERROR;
import static wooteco.prolog.common.slacklogger.SlackAlarmErrorLevel.WARN;

import java.util.Arrays;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import wooteco.prolog.common.slacklogger.SlackAlarm;

@Slf4j
@RestControllerAdvice
public class ExceptionController {

@SlackAlarm(level = WARN)
@ExceptionHandler(BadRequestException.class)
public ResponseEntity<ExceptionDto> loginExceptionHandler(BadRequestException e) {
log.warn(e.getMessage());
return ResponseEntity.badRequest()
.body(new ExceptionDto(e.getCode(), e.getMessage()));
}

@SlackAlarm(level = ERROR)
@ExceptionHandler(Exception.class)
public ResponseEntity<ExceptionDto> runtimeExceptionHandler(Exception e) {
if (e.getMessage() == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package wooteco.prolog.common.filter;

import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import wooteco.prolog.common.slacklogger.RequestStorage;

@Component
public class ServletWrappingFilter extends OncePerRequestFilter {

private final RequestStorage requestStorage;

public ServletWrappingFilter(RequestStorage requestStorage) {
this.requestStorage = requestStorage;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {

ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
requestStorage.set(wrappedRequest);

filterChain.doFilter(wrappedRequest, response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package wooteco.prolog.common.slacklogger;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.boot.autoconfigure.AutoConfigurationPackage;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
@Profile({"prod", "dev"})
@AutoConfigurationPackage
public class ExceptionAppender {

private static final String SLACK_ALARM_FORMAT = "[SlackAlarm] %s";

private final RequestStorage requestStorage;
private final SlackMessageGenerator slackMessageGenerator;
private final PrologSlack prologSlack;

public ExceptionAppender(RequestStorage requestStorage,
SlackMessageGenerator slackMessageGenerator,
PrologSlack prologSlack) {
this.requestStorage = requestStorage;
this.slackMessageGenerator = slackMessageGenerator;
this.prologSlack = prologSlack;
}

@Before("@annotation(wooteco.prolog.common.slacklogger.SlackAlarm)")
public void appendExceptionToResponseBody(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
if (!validateHasOneArgument(args)) {
return;
}

if (!validateIsException(args)) {
return;
}

MethodSignature signature = (MethodSignature) joinPoint.getSignature();
SlackAlarm annotation = signature.getMethod().getAnnotation(SlackAlarm.class);
SlackAlarmErrorLevel level = annotation.level();

String message = slackMessageGenerator
.generate(requestStorage.get(), (Exception) args[0], level);
prologSlack.send(message);
}

private boolean validateIsException(Object[] args) {
if (!(args[0] instanceof Exception)) {
log.warn("[SlackAlarm] argument is not Exception");
return false;
}

return true;
}

private boolean validateHasOneArgument(Object[] args) {
if (args.length != 1) {
log.warn(String
.format(SLACK_ALARM_FORMAT, "ambiguous exceptions! require just only one Exception"));
return false;
}

return true;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package wooteco.prolog.common.slacklogger;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.HashMap;
import java.util.Map;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;

@Component
public class PrologSlack {

private static final String SLACK_LOGGER_WEBHOOK_URI =
System.getenv("SLACK_LOGGER_WEBHOOK_URI");

public final ObjectMapper objectMapper;

public PrologSlack(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}

public void send(String message) {
WebClient.create(SLACK_LOGGER_WEBHOOK_URI)
.post()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(toJson(message))
.retrieve()
.bodyToMono(String.class)
.block();
}

private String toJson(String message) {
try {
Map<String, String> values = new HashMap<>();
values.put("text", message);

return objectMapper.writeValueAsString(values);
} catch (JsonProcessingException ignored) {
}
return "{\"text\" : \"슬랙으로 보낼 데이터를 제이슨으로 변경하는데 에러가 발생함.\"}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package wooteco.prolog.common.slacklogger;

import org.springframework.web.util.ContentCachingRequestWrapper;

public class RequestStorage {

private ContentCachingRequestWrapper request;

public void set(ContentCachingRequestWrapper request) {
this.request = request;
}

public ContentCachingRequestWrapper get() {
return request;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package wooteco.prolog.common.slacklogger;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SlackAlarm {

SlackAlarmErrorLevel level() default SlackAlarmErrorLevel.WARN;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package wooteco.prolog.common.slacklogger;

public enum SlackAlarmErrorLevel {
TRACE,
DEBUG,
INFO,
WARN,
ERROR
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package wooteco.prolog.common.slacklogger;

import static java.util.stream.Collectors.joining;

import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import org.springframework.web.util.ContentCachingRequestWrapper;
import wooteco.prolog.login.application.AuthorizationExtractor;
import wooteco.prolog.login.application.JwtTokenProvider;
import wooteco.prolog.login.excetpion.TokenNotValidException;

@Component
public class SlackMessageGenerator {

private static final String EXTRACTION_ERROR_MESSAGE = "메세지를 추출하는데 오류가 생겼습니다.\nmessagee : %s";
private static final String EXCEPTION_MESSAGE_FORMAT = "_%s_ %s.%s:%d - %s";
private static final String SLACK_MESSAGE_FORMAT = "*[%s]* %s\n*[요청한 멤버 id]* %s\n\n*[ERROR LOG]*\n%s\n\n*[REQUEST_INFORMATION]*\n%s %s\n%s\n\n%s";
private static final String EMPTY_BODY_MESSAGE = "{BODY IS EMPTY}";

private final Environment environment;
private final JwtTokenProvider jwtTokenProvider;

public SlackMessageGenerator(Environment environment,
JwtTokenProvider jwtTokenProvider) {
this.environment = environment;
this.jwtTokenProvider = jwtTokenProvider;
}

public String generate(ContentCachingRequestWrapper request,
Exception exception,
SlackAlarmErrorLevel level) {
try {
String token = AuthorizationExtractor.extract(request);
String profile = getProfile();
String currentTime = getCurrentTime();
String method = request.getMethod();
String userId = getUserId(token);
String requestURI = request.getRequestURI();
String headers = extractHeaders(request);
String body = getBody(request);
String exceptionMessage = extractExceptionMessage(exception, level);

return toMessage(profile, currentTime, userId,
exceptionMessage, method, requestURI, headers, body);
} catch (Exception e) {
return String.format(EXTRACTION_ERROR_MESSAGE, e.getMessage());
}
}

private String getProfile() {
return String.join(",", environment.getActiveProfiles()).toUpperCase();
}

private String getCurrentTime() {
return LocalDateTime.now().toString();
}

private String getUserId(String token) {
try {
return jwtTokenProvider.extractSubject(token);
} catch (TokenNotValidException e) {
return "Guest";
}
}

private String extractHeaders(ContentCachingRequestWrapper request) {
Enumeration<String> headerNames = request.getHeaderNames();

Map<String, String> values = new HashMap<>();

while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
values.put(headerName, request.getHeader(headerName));
}

return values.entrySet().stream()
.map(e -> e.getKey() + ":" + e.getValue())
.collect(joining("\n"));
}

private String getBody(ContentCachingRequestWrapper request) {
String body = new String(request.getContentAsByteArray());
if (body.isEmpty()) {
body = EMPTY_BODY_MESSAGE;
}
return body;
}

private String extractExceptionMessage(Exception e, SlackAlarmErrorLevel level) {
StackTraceElement stackTrace = e.getStackTrace()[0];
String className = stackTrace.getClassName();
int lineNumber = stackTrace.getLineNumber();
String methodName = stackTrace.getMethodName();

String message = e.getMessage();

if (Objects.isNull(message)) {
return Arrays.stream(e.getStackTrace())
.map(StackTraceElement::toString)
.collect(joining("\n"));
}

return String
.format(EXCEPTION_MESSAGE_FORMAT, level.name(), className, methodName, lineNumber,
message);
}


private String toMessage(String profile, String currentTime, String userId, String errorMessage,
String method, String requestURI, String headers, String body) {
return String.format(
SLACK_MESSAGE_FORMAT, profile, currentTime, userId,
errorMessage, method, requestURI, headers, body
);
}
}

This file was deleted.

0 comments on commit 5ee50c0

Please sign in to comment.