Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API 요청 로깅 #84

Merged
merged 5 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ dependencies {

// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// IOUtils
implementation 'commons-io:commons-io:2.7'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,19 @@
public class GlobalExceptionHandler {

@ExceptionHandler(BuzException.class)
public ResponseEntity<?> handle(BuzException ex) {
public ResponseEntity<?> handleBuzException(BuzException ex) {
log.warn("{}({}) - {}", ex.getClass().getSimpleName(), ex.statusCode.getStatusCode(), ex.getMessage());
return ResponseEntity
.status(ex.statusCode.getHttpCode())
.body(ApiResponse.error(ex.statusCode.getStatusCode(), ex.statusCode.getMessage()));
}

@ExceptionHandler(AuthException.class) //분리 이유: SignToken
public ResponseEntity<?> handle(AuthException ex) {
public ResponseEntity<?> handleAuthException(AuthException ex) {
log.warn("{}({}) - {}", ex.getClass().getSimpleName(), ex.statusCode.getStatusCode(), ex.getMessage());
return ResponseEntity
.status(ex.statusCode.getHttpCode())
.body(ApiResponse.error(ex.statusCode.getStatusCode(), ex.data, ex.statusCode.getMessage()));

}

/**
Expand All @@ -51,7 +50,7 @@ public ResponseEntity<?> handle(AuthException ex) {
// AWS S3 버킷 정책에 맞지 않는 요청
@ExceptionHandler(AwsS3Exception.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
protected ApiResponse<?> handleAwsS3Error(final AwsS3Exception ex) {
protected ApiResponse<?> handleAwsS3Exception(final AwsS3Exception ex) {
log.warn("{} - {}", ex.getClass().getSimpleName(), ex.getMessage());
return error(ex.getAwsS3ErrorCode().getStatusCode(), ex.getMessage());
}
Expand Down
51 changes: 51 additions & 0 deletions src/main/java/bokjak/bokjakserver/web/log/LoggerAspect.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package bokjak.bokjakserver.web.log;

import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.json.simple.JSONObject;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.Enumeration;
import java.util.Objects;

@Component
@Aspect
@Slf4j
public class LoggerAspect {
@Pointcut("execution(* bokjak.bokjakserver..*Controller.*(..)) || execution(* bokjak.bokjakserver..*GlobalExceptionHandler.*(..))")
// 이런 패턴이 실행될 경우 수행
public void loggerPointCut() {
}

@Around("loggerPointCut()")
public Object logRequestUri(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object result = proceedingJoinPoint.proceed();
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

if (Objects.nonNull(requestAttributes)) {
HttpServletRequest request = requestAttributes.getRequest(); // request 정보를 가져온다.
String controllerName = proceedingJoinPoint.getSignature().getDeclaringType().getSimpleName();
String methodName = proceedingJoinPoint.getSignature().getName();
log.info("{}.{}: {} {} PARAM={}", controllerName, methodName, request.getMethod(), request.getRequestURI(), extractParams(request)); // param에 담긴 정보들을 한번에 로깅한다.
}

return result;
}

private static JSONObject extractParams(HttpServletRequest request) { // request로부터 param 추출, JSONObject로 변환
JSONObject jsonObject = new JSONObject();
Enumeration<String> params = request.getParameterNames();
while (params.hasMoreElements()) {
String param = params.nextElement();
String replaceParam = param.replaceAll("\\.", "-");
jsonObject.put(replaceParam, request.getParameter(param));
}
return jsonObject;
}
}
143 changes: 143 additions & 0 deletions src/main/java/bokjak/bokjakserver/web/log/ReadableRequestWrapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package bokjak.bokjakserver.web.log;

import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.entity.ContentType;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

@Slf4j
public class ReadableRequestWrapper extends HttpServletRequestWrapper {
private final Charset encoding;
private byte[] rawData;
private final Map<String, String[]> params = new HashMap<>();

public ReadableRequestWrapper(HttpServletRequest request) {
super(request);
this.params.putAll(request.getParameterMap()); // 오리지널 요청의 파라미터들 저장

String charEncoding = request.getCharacterEncoding(); // 인코딩 설정
this.encoding = StringUtils.isBlank(charEncoding) ? StandardCharsets.UTF_8 : Charset.forName(charEncoding);

try {
// 중요: body가 유실되지 않도록 함. getInputStream -> rawData에 저장 -> getReader() 에서 새 스트림으로 생성
InputStream inputStream = request.getInputStream();
this.rawData = IOUtils.toByteArray(inputStream);

// body 파싱
String collect = this.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
if (StringUtils.isEmpty(collect)) { // body 가 없을경우 로깅 제외 TODO: 이 경우에도 로깅해야 하지 않나?
return;
}
if (request.getContentType() != null && request.getContentType().contains(
ContentType.MULTIPART_FORM_DATA.getMimeType())) { // 파일 업로드시 로깅 제외 TODO: 이 경우에도 로깅해야 하지 않나?
return;
}

JSONParser jsonParser = new JSONParser();
Object parse = jsonParser.parse(collect);
if (parse instanceof JSONArray) {
JSONArray jsonArray = (JSONArray) jsonParser.parse(collect);
setParameter("requestBody", jsonArray.toJSONString());
} else {
JSONObject jsonObject = (JSONObject) jsonParser.parse(collect);
for (Object key : jsonObject.keySet()) {
setParameter(key.toString(), jsonObject.get(key.toString()).toString().replace("\"", "\\\""));
}
}
} catch (Exception e) {
log.error("ReadableRequestWrapper init error", e);
}
}

@Override
public String getParameter(String name) {
String[] paramArray = getParameterValues(name);
if (paramArray != null && paramArray.length > 0) {
return paramArray[0];
} else {
return null;
}
}

@Override
public Map<String, String[]> getParameterMap() {
return Collections.unmodifiableMap(params);
}

@Override
public Enumeration<String> getParameterNames() {
return Collections.enumeration(params.keySet());
}

@Override
public String[] getParameterValues(String name) {
String[] result = null;
String[] dummyParamValue = params.get(name);

if (dummyParamValue != null) {
result = new String[dummyParamValue.length];
System.arraycopy(dummyParamValue, 0, result, 0, dummyParamValue.length);
}
return result;
}

public void setParameter(String name, String value) {
String[] param = {value};
setParameter(name, param);
}

public void setParameter(String name, String[] values) {
params.put(name, values);
}

@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.rawData);

return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}

@Override
public boolean isReady() {
return false;
}

@Override
public void setReadListener(ReadListener readListener) {
// Do nothing
}

public int read() {
return byteArrayInputStream.read();
}
};
}

@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(this.getInputStream(), this.encoding));
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package bokjak.bokjakserver.web.log;

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;

import java.io.IOException;

@WebFilter(urlPatterns = "/*") // 대상: 전체 URI
public class ReadableRequestWrapperFilter implements Filter { // TODO: 4xx 응답을 캐치 못한다?

@Override
public void init(FilterConfig filterConfig) {
// Do nothing
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
ReadableRequestWrapper wrapper = new ReadableRequestWrapper((HttpServletRequest) request);
filterChain.doFilter(wrapper, response); // 필터 체인에 Wrapper 추가
}

@Override
public void destroy() {
// Do nothing
}

}