우아한 테크코스 프로젝트 "터놓고"에서 로깅을 맡았다.
무작정 구현은 시간낭비니까 정책 수립부터 가보자ㅎㅅㅎ
고려사항은 다음과 같다.
파일 저장, 콘솔 출력
/logs/local.log 에 저장
하루 지나면./logs/backup/rollingfile.날짜.gz로 압축해서 저장
삭제 조건
파일 저장, 콘솔 출력
/logs/local.log 에 저장
하루 지나면./logs/backup/rollingfile.날짜.gz로 압축해서 저장
삭제 조건
콘솔 출력
없음
일단 xml 파일에 전략대로 설정해주었다. 이건 시크릿파일이라 게시하기가 좀 그럼..ㅎ
InputStream 은 한번 읽으면 다시 읽을 수 없음.
근데 우리는 요청정보 찍고 , 컨트롤러로 요청을 넘겨 수행할때
총 두번 요청을 읽을 수 있어야함.
그래서 필터에서 꺼내서 캐싱하고 이 캐싱본을 활용하게끔 함.
package com.woowacourse.ternoko.common.log;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
@Component
public class CustomLoggingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(@NotNull final HttpServletRequest request,
@NotNull final HttpServletResponse response,
final FilterChain filterChain)
throws ServletException, IOException {
ContentCachingRequestWrapper wrappingRequest = new ContentCachingRequestWrapper(request);
filterChain.doFilter(wrappingRequest, response);
}
}
상태코드가 성공일 경우 캐싱한 요청을 캐스팅한 후 출력함.
package com.woowacourse.ternoko.common.log;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.util.ContentCachingRequestWrapper;
@Slf4j
@Component
@RequiredArgsConstructor
public class LogInterceptor implements HandlerInterceptor {
private final ObjectMapper objectMapper;
@Override
public void afterCompletion(@NotNull final HttpServletRequest request, final HttpServletResponse response,
@NotNull final Object handler, final Exception ex) throws IOException {
if (isSuccess(response.getStatus())) {
ContentCachingRequestWrapper cachingRequest;
try{
cachingRequest = (ContentCachingRequestWrapper) request;
} catch (ClassCastException e){
log.info("로깅 필터가 정상적으로 동작하지 않았습니다.");
return;
}
log.info(LogForm.SUCCESS_LOGGING_FORM, request.getMethod(), request.getRequestURI(),
StringUtils.hasText(request.getHeader("Authorization")),
objectMapper.readTree(cachingRequest.getContentAsByteArray()));
}
}
private boolean isSuccess(int responseStatus) {
return !HttpStatus.valueOf(responseStatus).is4xxClientError() && !HttpStatus.valueOf(responseStatus)
.is5xxServerError();
}
}
실패한 요청일 경우 controllerAdvice 에서 잡은 Exception정보를 통해 실패 로그 출력함
private void printFailedLog(final HttpServletRequest request,
final Exception e,
final ContentCachingRequestWrapper cachingRequest) {
try {
log.info(FAILED_LOGGING_FORM,
request.getMethod(),
request.getRequestURI(),
StringUtils.hasText(request.getHeader("Authorization")),
objectMapper.readTree(cachingRequest.getContentAsByteArray()),
e.getMessage());
log.debug("Stack trace: ", e);
} catch (IOException ex) {
log.debug("log error");
}
}
로깅 프린트 폼
package com.woowacourse.ternoko.common.log;
public class LogForm {
private static final String NEWLINE = System.lineSeparator();
public static final String SUCCESS_LOGGING_FORM =
NEWLINE + "HTTP Method : {} "
+ NEWLINE + "Request URI : {} "
+ NEWLINE + "AccessToken is exist : {} "
+ NEWLINE + "Request Body : {}";
public static final String FAILED_LOGGING_FORM =
SUCCESS_LOGGING_FORM
+ NEWLINE + "MESSAGE : {}";
}
테스트 리팩토링을 한 팀원이 MockMvc를 사용했는데 MockMvc는 커스텀한 필터를 따로 등록해줘야 정상적으로 동작함. 트러블 슈팅 기록은 여기ㅎ
트러블슈팅 : https://necessary-sundial-178.notion.site/d575a364448440de9d4fd278f8fbcf52
해결 PR : https://github.com/woowacourse-teams/2022-ternoko/issues/431