Skip to content

Commit

Permalink
[FEAT] 로깅 모듈 추가 (#295)
Browse files Browse the repository at this point in the history
  • Loading branch information
kmw2378 authored Jun 20, 2024
2 parents 11fb9c7 + 095e9f8 commit fa70671
Show file tree
Hide file tree
Showing 6 changed files with 239 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.kakaoshare.backend.logging;

import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.kakaoshare.backend.logging.handler.ConnectionInvocationHandler;
import org.kakaoshare.backend.logging.util.ApiQueryCounter;
import org.springframework.stereotype.Component;

import java.lang.reflect.Proxy;

@Aspect
@Component
@RequiredArgsConstructor
public class ApiQueryCounterAspect {
private final ApiQueryCounter apiQueryCounter;

@Around("execution(* javax.sql.DataSource.getConnection())")
public Object getConnection(final ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
final Object connection = proceedingJoinPoint.proceed();
return Proxy.newProxyInstance(
connection.getClass().getClassLoader(),
connection.getClass().getInterfaces(),
new ConnectionInvocationHandler(apiQueryCounter, connection)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.kakaoshare.backend.logging.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StopWatch;
import org.springframework.web.context.annotation.RequestScope;

@Configuration
public class LoggingConfig {
@Bean
@RequestScope
public StopWatch stopWatch() {
return new StopWatch();
}
}
129 changes: 129 additions & 0 deletions src/main/java/org/kakaoshare/backend/logging/filter/LoggingFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package org.kakaoshare.backend.logging.filter;

import com.querydsl.core.util.StringUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.kakaoshare.backend.logging.util.ApiQueryCounter;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

import static org.springframework.http.HttpHeaders.AUTHORIZATION;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

@Component
@Slf4j
@RequiredArgsConstructor
public class LoggingFilter extends OncePerRequestFilter {
private static final String REQUEST_LOG_NO_BODY_FORMAT = "REQUEST :: METHOD: {}, URL: {}, HAS_AUTHORIZATION: {}";
private static final String REQUEST_LOG_FORMAT = REQUEST_LOG_NO_BODY_FORMAT + ", BODY: {}";
private static final String RESPONSE_LOG_NO_BODY_FORMAT = "RESPONSE :: STATUS_CODE: {}, METHOD: {}, URL: {}, QUERY_COUNT: {}, TIME_TAKEN: {}ms";
private static final String QUERY_COUNT_WARNING_LOG_FORMAT = "쿼리가 {}번 이상 실행되었습니다.";

private static final int QUERY_COUNT_WARNING_STANDARD = 10;
private static final String START_OF_PARAMS = "?";
private static final String PARAM_DELIMITER = "&";
private static final String KEY_VALUE_DELIMITER = "=";

private final StopWatch apiTimer;
private final ApiQueryCounter apiQueryCounter;

@Override
protected void doFilterInternal(final HttpServletRequest request,
final HttpServletResponse response,
final FilterChain filterChain) throws ServletException, IOException {
final ContentCachingRequestWrapper cachingRequest = new ContentCachingRequestWrapper(request);
final ContentCachingResponseWrapper cachingResponse = new ContentCachingResponseWrapper(response);

apiTimer.start();
filterChain.doFilter(cachingRequest, cachingResponse);
apiTimer.stop();

logRequestAndResponse(cachingRequest, cachingResponse);
cachingResponse.copyBodyToResponse();
}

private void logRequestAndResponse(final ContentCachingRequestWrapper request,
final ContentCachingResponseWrapper response) {
logRequest(request);
logResponse(request, response);
warnByQueryCount();
}

private void logRequest(final ContentCachingRequestWrapper request) {
final String requestBody = new String(request.getContentAsByteArray());
final String requestURIWithParams = getRequestURIWithParams(request);

if (requestBody.isBlank()) {
log.info(REQUEST_LOG_NO_BODY_FORMAT, request.getMethod(), requestURIWithParams, StringUtils.isNullOrEmpty(request.getHeader(AUTHORIZATION)));
return;
}

log.info(REQUEST_LOG_FORMAT, request.getMethod(), requestURIWithParams, StringUtils.isNullOrEmpty(request.getHeader(AUTHORIZATION)), requestBody);
}

private void logResponse(final ContentCachingRequestWrapper request,
final ContentCachingResponseWrapper response) {
final int queryCount = apiQueryCounter.getCount();
final String requestURIWithParams = getRequestURIWithParams(request);
log.info(RESPONSE_LOG_NO_BODY_FORMAT, response.getStatus(), request.getMethod(),
requestURIWithParams, queryCount, apiTimer.getLastTaskTimeMillis());
}

private String getRequestURIWithParams(final ContentCachingRequestWrapper request) {
final String requestURI = request.getRequestURI();
final Map<String, String[]> params = request.getParameterMap();

if (params.isEmpty()) {
return requestURI;
}

final String parsedParams = parseParams(params);
return requestURI + parsedParams;
}

private String parseParams(final Map<String , String[]> params) {
final String everyParamStrings = params.entrySet().stream()
.map(this::toParamString)
.collect(Collectors.joining(PARAM_DELIMITER));

return START_OF_PARAMS + everyParamStrings;
}

private String toParamString(final Map.Entry<String, String[]> entry) {
final String key = entry.getKey();
final StringBuilder builder = new StringBuilder();

return Arrays.stream(entry.getValue())
.map(value -> builder.append(key).append(KEY_VALUE_DELIMITER).append(value))
.collect(Collectors.joining(PARAM_DELIMITER));
}

private Optional<String> getJsonResponseBody(final ContentCachingResponseWrapper response) {
if (Objects.equals(response.getContentType(), APPLICATION_JSON_VALUE)) {
return Optional.of(new String(response.getContentAsByteArray()));
}

return Optional.empty();
}

private void warnByQueryCount() {
final int queryCount = apiQueryCounter.getCount();
if (queryCount >= QUERY_COUNT_WARNING_STANDARD) {
log.warn(QUERY_COUNT_WARNING_LOG_FORMAT, QUERY_COUNT_WARNING_STANDARD);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.kakaoshare.backend.logging.handler;

import lombok.RequiredArgsConstructor;
import org.kakaoshare.backend.logging.util.ApiQueryCounter;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

@RequiredArgsConstructor
public class ConnectionInvocationHandler implements InvocationHandler {
private final ApiQueryCounter apiQueryCounter;
private final Object connection;

@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
final Object result = method.invoke(connection, args);
if (method.getName().equals("prepareStatement")) {
return Proxy.newProxyInstance(
result.getClass().getClassLoader(),
result.getClass().getInterfaces(),
new PreparedStatementInvocationHandler(apiQueryCounter, result)
);
}

return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.kakaoshare.backend.logging.handler;

import lombok.RequiredArgsConstructor;
import org.kakaoshare.backend.logging.util.ApiQueryCounter;
import org.springframework.web.context.request.RequestContextHolder;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

@RequiredArgsConstructor
public class PreparedStatementInvocationHandler implements InvocationHandler {
private final ApiQueryCounter apiQueryCounter;
private final Object preparedStatement;

@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
if (method.getName().contains("execute") && RequestContextHolder.getRequestAttributes() != null) {
apiQueryCounter.increaseCount();
}

return method.invoke(preparedStatement, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.kakaoshare.backend.logging.util;

import lombok.Getter;
import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.RequestScope;

@Component
@RequestScope
@Getter
public class ApiQueryCounter {
private int count;

public void increaseCount() {
count++;
}
}

0 comments on commit fa70671

Please sign in to comment.