[Spring] 로깅 정책

이열음·2022년 10월 1일
0

우아한 테크코스 프로젝트 "터놓고"에서 로깅을 맡았다.
무작정 구현은 시간낭비니까 정책 수립부터 가보자ㅎㅅㅎ

설계시 고려사항

고려사항은 다음과 같다.

  • 서버 메모리 용량와 서비스 운영 도중 발생하는 에러 디버깅의 편의성 두개의 적절한 중간점 찾기
  • 요청정보까지 같이 찍기
    - 여러 요청이 엮여서 나는 에러를 디버깅할때 편하기 위함

그래서 전략이 뭔데?

PROD

  • SpringFrameWork Exception: INFO 레벨까지 로깅
  • Application Exception : DEBUG 레벨까지 로깅
  • JDBC Exception : INFO 레벨까지 로깅
  • Binding Params 출력 : TRACE 레벨까지 출력

기본 로그

파일 저장, 콘솔 출력

/logs/local.log 에 저장

이전 로그 관리

하루 지나면./logs/backup/rollingfile.날짜.gz로 압축해서 저장

  • 용량을 줄이고자 압축해서 저장 → 확인할땐 풀어서 확인하면 됨

삭제 조건

  • 수정을 하지 않은지 5일이 넘어가면 삭제
  • 저장되는 파일이 31개가 넘어가면 삭제
  • 10MB 넘어가면 삭제

DEV

  • SpringFrameWork Exception: INFO 레벨까지 로깅
  • Application Exception : Debug 레벨까지 로깅
  • SQL 출력 : Debug 레벨까지 출력

기본 로그

파일 저장, 콘솔 출력

/logs/local.log 에 저장

이전 로그 관리

하루 지나면./logs/backup/rollingfile.날짜.gz로 압축해서 저장

삭제 조건

  • 수정을 하지 않은지 5일이 넘어가면 삭제
  • 저장되는 파일이 31개가 넘어가면 삭제
  • 5MB 넘어가면 삭제

LOCAL

  • SpringFrameWork Exception: INFO 레벨까지 로깅
  • Application Exception : Debug 레벨까지 로깅
  • SQL 출력 : Debug 레벨까지 출력
  • JDBC Exception : INFO 레벨까지 로깅
  • Binding Params 출력 : TRACE 레벨까지 출력

기본 로그

콘솔 출력

이전 로그 관리

없음

구현은 어케했나?

일단 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();
    }
}

GlobalControllerAdvice

실패한 요청일 경우 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");
        }
    }

LogForm

로깅 프린트 폼

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

0개의 댓글