-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
239 additions
and
0 deletions.
There are no files selected for viewing
28 changes: 28 additions & 0 deletions
28
src/main/java/org/kakaoshare/backend/logging/ApiQueryCounterAspect.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
); | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
src/main/java/org/kakaoshare/backend/logging/config/LoggingConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
129
src/main/java/org/kakaoshare/backend/logging/filter/LoggingFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
28 changes: 28 additions & 0 deletions
28
src/main/java/org/kakaoshare/backend/logging/handler/ConnectionInvocationHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
23 changes: 23 additions & 0 deletions
23
src/main/java/org/kakaoshare/backend/logging/handler/PreparedStatementInvocationHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
16 changes: 16 additions & 0 deletions
16
src/main/java/org/kakaoshare/backend/logging/util/ApiQueryCounter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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++; | ||
} | ||
} |