Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,7 @@ public class GlobalExceptionHandler {

@ExceptionHandler(NoResourceFoundException.class)
public ResponseEntity<Void> handleNoResourceFound(NoResourceFoundException exception) {
if (exception.getResourcePath().contains("favicon")) {
return ResponseEntity.notFound().build();
}
log.error("No static resource {}", exception.getResourcePath(), exception);
log.debug("No static resource: {}", exception.getResourcePath());
return ResponseEntity.notFound().build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Set;
import java.util.regex.Pattern;

import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
Expand All @@ -18,39 +21,182 @@
@Component
public class RequestLoggingFilter extends OncePerRequestFilter {

private static final Set<String> SENSITIVE_HEADERS = Set.of(
"authorization",
"cookie",
"set-cookie",
"proxy-authorization",
"x-auth-token",
"x-api-key"
);

private static final Set<String> SENSITIVE_BODY_FIELDS = Set.of(
"email",
"password",
"accessToken",
"refreshToken",
"token"
);

private static final Set<String> SENSITIVE_QUERY_PARAMS = Set.of(
"code",
"state"
);

private static final Pattern JWT_PATTERN =
Pattern.compile("eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+");

private static final String[] SKIP_PATH_PREFIXES = {
"/actuator",
"/favicon.ico",
"/swagger",
"/v3/api-docs",
"/health-check"
};
Comment thread
Goder-0 marked this conversation as resolved.

private static final int MAX_BODY_LENGTH = 2000;

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

filterChain.doFilter(wrappingRequest, response);
HttpServletRequest requestToUse = isLoggableBody(request.getContentType())
? new ContentCachingRequestWrapper(request)
: request;

ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);

logRequestDetails(wrappingRequest);
long startNs = System.nanoTime();
try {
filterChain.doFilter(requestToUse, responseWrapper);
} finally {
logRequestDetails(requestToUse, responseWrapper, startNs);
responseWrapper.copyBodyToResponse();
}
}

private void logRequestDetails(ContentCachingRequestWrapper request) {
String method = request.getMethod();
String url = request.getRequestURI();
String queryString = request.getQueryString() != null ? "?" + request.getQueryString() : "";
private void logRequestDetails(HttpServletRequest request, ContentCachingResponseWrapper response, long startNs) {
String uri = request.getRequestURI();
if (shouldSkip(uri)) {
return;
}

StringBuilder logMsg = new StringBuilder();
logMsg.append("\n[API REQUEST] ").append(method).append(" ").append(url).append(queryString).append("\n");
long tookMs = (System.nanoTime() - startNs) / 1_000_000;
String query = maskQueryString(request.getQueryString());

logMsg.append(" > [HEADERS]\n");
Collections.list(request.getHeaderNames()).forEach(headerName ->
logMsg.append(" - ").append(headerName).append(": ").append(request.getHeader(headerName)).append("\n")
);
log.info("[API] {} {}{} -> {} ({}ms) ip={} ua=\"{}\"",
request.getMethod(),
uri,
query,
response.getStatus(),
tookMs,
clientIp(request),
request.getHeader("User-Agent"));

byte[] content = request.getContentAsByteArray();
if (content.length > 0) {
String body = new String(content, StandardCharsets.UTF_8);
logMsg.append(" > [Body Data] : ").append(body.trim()).append("\n");
} else {
logMsg.append(" > [Body Data] : (Empty)\n");
if (log.isDebugEnabled()) {
log.debug("[API REQUEST BODY] {} {} body={}", request.getMethod(), uri, requestBody(request));
log.debug("[API HEADERS] {} {} {}", request.getMethod(), uri, maskedHeaders(request));
log.debug("[API RESPONSE] {} {} -> {} body={}", request.getMethod(), uri, response.getStatus(),
responseBody(response));
}
}

logMsg.append("--------------------------------------------------------------------------------");
private boolean shouldSkip(String uri) {
for (String prefix : SKIP_PATH_PREFIXES) {
if (uri.startsWith(prefix)) {
return true;
}
}
return false;
}

log.info(logMsg.toString());
private String maskQueryString(String queryString) {
if (queryString == null || queryString.isBlank()) {
return "";
}

StringBuilder sb = new StringBuilder("?");
String[] pairs = queryString.split("&");
for (int i = 0; i < pairs.length; i++) {
String pair = pairs[i];
int eq = pair.indexOf('=');
if (eq > 0) {
String key = pair.substring(0, eq);
if (SENSITIVE_QUERY_PARAMS.contains(key.toLowerCase())) {
sb.append(key).append("=***");
} else {
sb.append(pair);
}
} else {
sb.append(pair);
}
if (i < pairs.length - 1) {
sb.append("&");
}
}
return sb.toString();
}

private String clientIp(HttpServletRequest request) {
String forwarded = request.getHeader("X-Forwarded-For");
if (forwarded != null && !forwarded.isBlank()) {
return forwarded.split(",")[0].trim();
}
String realIp = request.getHeader("X-Real-IP");
if (realIp != null && !realIp.isBlank()) {
return realIp;
}
return request.getRemoteAddr();
}
Comment thread
Goder-0 marked this conversation as resolved.

private String maskedHeaders(HttpServletRequest request) {
StringBuilder sb = new StringBuilder();
Enumeration<String> names = request.getHeaderNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
String value = SENSITIVE_HEADERS.contains(name.toLowerCase())
? "***MASKED***"
: request.getHeader(name);
sb.append(name).append("=").append(value).append("; ");
}
return sb.toString();
}

private String requestBody(HttpServletRequest request) {
if (request instanceof ContentCachingRequestWrapper wrapper) {
return formatBody(wrapper.getContentType(), wrapper.getContentAsByteArray());
}
return "(skipped, content-type=" + request.getContentType() + ")";
}

private String responseBody(ContentCachingResponseWrapper response) {
return formatBody(response.getContentType(), response.getContentAsByteArray());
}

private String formatBody(String contentType, byte[] content) {
if (!isLoggableBody(contentType)) {
return "(skipped, content-type=" + contentType + ")";
}
if (content.length == 0) {
return "(empty)";
}
String body = maskBody(new String(content, StandardCharsets.UTF_8));
if (body.length() > MAX_BODY_LENGTH) {
return body.substring(0, MAX_BODY_LENGTH) + "...(truncated, total " + body.length() + " chars)";
}
return body;
}

private boolean isLoggableBody(String contentType) {
return contentType != null && (contentType.contains("json") || contentType.startsWith("text/"));
}

private String maskBody(String body) {
String masked = body;
for (String field : SENSITIVE_BODY_FIELDS) {
masked = masked.replaceAll("(\"" + field + "\"\\s*:\\s*)\"[^\"]*\"", "$1\"***\"");
}
masked = JWT_PATTERN.matcher(masked).replaceAll("***JWT***");
return masked;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ public String resolveToken(HttpServletRequest request) {
return cookie.getValue();
}

log.warn("Token not found (missing in both header and cookie)");
return null;
}

Expand Down
Loading