-
Notifications
You must be signed in to change notification settings - Fork 2
feat: dev용 모든 요청 로깅 필터(with Loki+Grafana) - #138 #152
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
Conversation
📝 WalkthroughSummary by CodeRabbit
Walkthroughdev 프로필에서만 활성화되는 요청/응답 로깅 필터가 추가됨. ContentCaching 래퍼로 요청/응답 본문을 캐싱하고, 처리 시간과 메서드/URL/상태코드/본문(일부 절단 포함)을 기록. 예외 발생 시 로깅 실패가 본 처리를 방해하지 않도록 보호. 응답 본문은 copyBodyToResponse로 복사하여 클라이언트 전달 보장. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant C as Client
participant F as DevRequestResponseLoggingFilter (dev)
participant D as DispatcherServlet
participant H as Controller/Handler
rect rgba(230,240,255,0.6)
note over F: 요청/응답 래핑 (ContentCaching*)
C->>F: HTTP Request
F->>D: Wrapped Request
D->>H: Dispatch
H-->>D: Handler Response
D-->>F: Response
end
rect rgba(240,255,240,0.6)
note over F: 처리시간 측정 및 본문/메타 로깅<br/>(본문 길이 제한, 비어있음 처리)
F-->>C: copyBodyToResponse()로 Response 반환
end
alt 로깅 중 예외
note over F: 로깅 예외는 전파하지 않음
end
Estimated code review effort🎯 3 (보통) | ⏱️ ~25 minutes Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (5)
src/main/java/com/permitseoul/permitserver/global/filter/DevRequestResponseLoggingFilter.java (5)
49-61: Loki/Grafana엔 단일 라인·구조화 로그가 더 적합합니다멀티라인/이모지 메시지는 파싱이 어려워요. key=value 또는 JSON 단일 라인으로 로깅하세요. 타임스탬프는 로거가 포함하므로 메시지 내 Time 필드는 생략해도 됩니다.
- log.info(""" - 🧩 [HTTP LOG] - ▶️ Time: {} - ▶️ Method: {} - ▶️ URL: {} - ▶️ Status: {} - ▶️ Duration: {} ms - ▶️ Request Body: {} - ▶️ Response Body: {} - """, - LocalDateTime.now(), method, fullUrl, - response.getStatus(), duration, sanitize(reqBody), sanitize(resBody) - ); + log.info("http_log method={} url=\"{}\" status={} duration_ms={} req_body=\"{}\" res_body=\"{}\"", + method, fullUrl, response.getStatus(), duration, sanitize(reqBody), sanitize(resBody));추가 팁:
- MDC(traceId, spanId, userId 등) 연동 시 쿼리 용이합니다.
- req_len/res_len, req_ct/res_ct 필드도 라벨/파싱에 유용합니다.
68-72: 민감정보 마스킹 추가 권장현재 sanitize는 길이 절단만 합니다. password/token/authorization 등은 기본 마스킹이 필요합니다. (dev라도 로그 외부 유출 위험 완화)
- return input.length() > 2000 ? input.substring(0, 2000) + "...(truncated)" : input; //2000자까지만 보임 + String masked = maskSensitive(input); + return masked.length() > MAX_LOG_BODY_CHARS + ? masked.substring(0, MAX_LOG_BODY_CHARS) + "...(truncated)" + : masked;헬퍼 추가:
private static final int MAX_LOG_BODY_CHARS = 2000; private static final Pattern SENSITIVE_KEYS = Pattern.compile( "(\"?(?:password|pwd|token|access_token|refresh_token|secret|authorization)\"?\\s*[:=]\\s*\")([^\"]*)(\")", Pattern.CASE_INSENSITIVE); private static String maskSensitive(String in) { return SENSITIVE_KEYS.matcher(in).replaceAll("$1***$3"); }원하시면 쿼리스트링도 access_token 등 키를 마스킹하도록 보완해 드리겠습니다.
17-19: 필터 순서/실행 보장“가장 처음” 실행은 @component만으로 보장되지 않습니다. 주문도 중요하고, 에러/ASYNC 디스패치에서 중복 실행을 막으려면 OncePerRequestFilter 사용이 좋습니다.
간단 옵션:
+import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; @@ -@Component // 필터 가장 처음 +@Component +@Order(Ordered.HIGHEST_PRECEDENCE)대안(더 권장):
- Filter 구현 대신 OncePerRequestFilter 상속으로 전환하고 doFilterInternal 사용.
- async 디스패치 제외 필요 시 shouldNotFilterAsyncDispatch() 오버라이드.
22-25: 헬스체크/프리플라이트 등 소음 엔드포인트는 스킵/actuator/health, /favicon.ico, OPTIONS/HEAD 등은 로그 소음이 큽니다. 조기 반환으로 제외하세요.
예:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - - // 요청 응답을 여러 번 읽을 수 있도록 래핑 + final HttpServletRequest httpReq = (HttpServletRequest) request; + if (shouldSkip(httpReq)) { + chain.doFilter(request, response); + return; + } + // 요청/응답을 여러 번 읽을 수 있도록 래핑헬퍼:
private boolean shouldSkip(HttpServletRequest r) { String m = r.getMethod(); String u = r.getRequestURI(); if ("OPTIONS".equalsIgnoreCase(m) || "HEAD".equalsIgnoreCase(m)) return true; return u.startsWith("/actuator/health") || "/favicon.ico".equals(u); }
26-27: ContentCachingRequestWrapper/ResponseWrapper에 본문 버퍼링 상한 추가
두-인자 생성자(HttpServletRequest/HttpServletResponse, int)는 Spring 4.3.6+에서 지원되므로, 아래와 같이 MAX_LOG_BODY_BYTES를 지정해 메모리 과다 사용을 방지하세요.- final ContentCachingRequestWrapper reqWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request); - final ContentCachingResponseWrapper resWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response); + final ContentCachingRequestWrapper reqWrapper = + new ContentCachingRequestWrapper((HttpServletRequest) request, MAX_LOG_BODY_BYTES); + final ContentCachingResponseWrapper resWrapper = + new ContentCachingResponseWrapper((HttpServletResponse) response, MAX_LOG_BODY_BYTES);클래스 상단에 상수 추가:
private static final int MAX_LOG_BODY_BYTES = 64 * 1024; // 64KB (환경변수/설정으로 조정 권장)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/main/java/com/permitseoul/permitserver/global/filter/DevRequestResponseLoggingFilter.java(1 hunks)
🔇 Additional comments (1)
src/main/java/com/permitseoul/permitserver/global/filter/DevRequestResponseLoggingFilter.java (1)
49-58: 텍스트 블록(JDK 15+) 사용 가능 확인
프로젝트 Gradle toolchain 및 CI 워크플로우에서 Java 17을 사용 중이므로 멀티라인 텍스트 블록을 안전하게 사용할 수 있습니다.
| final long start = System.currentTimeMillis(); | ||
| chain.doFilter(reqWrapper, resWrapper); | ||
| final long duration = System.currentTimeMillis() - start; | ||
|
|
||
| logRequestResponse(reqWrapper, resWrapper, duration); | ||
| resWrapper.copyBodyToResponse(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
copyBodyToResponse는 finally에서 보장해야 합니다
예외/에러 디스패치 시에도 응답이 복사되지 않아 본문이 유실될 수 있습니다. 처리시간 계산과 로깅, copyBodyToResponse를 finally로 이동하세요.
- final long start = System.currentTimeMillis();
- chain.doFilter(reqWrapper, resWrapper);
- final long duration = System.currentTimeMillis() - start;
-
- logRequestResponse(reqWrapper, resWrapper, duration);
- resWrapper.copyBodyToResponse();
+ final long start = System.currentTimeMillis();
+ try {
+ chain.doFilter(reqWrapper, resWrapper);
+ } finally {
+ final long duration = System.currentTimeMillis() - start;
+ logRequestResponse(reqWrapper, resWrapper, duration);
+ resWrapper.copyBodyToResponse();
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| final long start = System.currentTimeMillis(); | |
| chain.doFilter(reqWrapper, resWrapper); | |
| final long duration = System.currentTimeMillis() - start; | |
| logRequestResponse(reqWrapper, resWrapper, duration); | |
| resWrapper.copyBodyToResponse(); | |
| } | |
| final long start = System.currentTimeMillis(); | |
| try { | |
| chain.doFilter(reqWrapper, resWrapper); | |
| } finally { | |
| final long duration = System.currentTimeMillis() - start; | |
| logRequestResponse(reqWrapper, resWrapper, duration); | |
| resWrapper.copyBodyToResponse(); | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/com/permitseoul/permitserver/global/filter/DevRequestResponseLoggingFilter.java
around lines 29-35, move the timing, logging and response-copying into a finally
block so the response body is always copied even on exceptions; record start =
System.currentTimeMillis() before calling chain.doFilter(reqWrapper,
resWrapper), wrap chain.doFilter in try { ... } (rethrow any exceptions),
declare duration variable outside, and in finally compute duration =
System.currentTimeMillis() - start, call logRequestResponse(reqWrapper,
resWrapper, duration) and resWrapper.copyBodyToResponse().
| final String method = request.getMethod(); | ||
| final String uri = request.getRequestURI(); | ||
| final String query = request.getQueryString(); | ||
| final String fullUrl = uri + (query != null ? "?" + query : ""); | ||
|
|
||
| final String reqBody = new String(request.getContentAsByteArray(), StandardCharsets.UTF_8); | ||
| final String resBody = new String(response.getContentAsByteArray(), StandardCharsets.UTF_8); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
문자셋/콘텐츠타입 고려 없이 UTF-8로 디코딩 — 바이너리/멀티파트는 건너뛰고 선언된 charset을 사용하세요
현재 모든 본문을 UTF-8로 변환합니다. 바이너리(이미지/압축), multipart/form-data, application/octet-stream 등은 로그를 건너뛰고, 텍스트형만 request/response의 characterEncoding을 사용해 디코딩하세요.
- final String reqBody = new String(request.getContentAsByteArray(), StandardCharsets.UTF_8);
- final String resBody = new String(response.getContentAsByteArray(), StandardCharsets.UTF_8);
+ final String reqCt = request.getContentType();
+ final String resCt = response.getContentType();
+ final byte[] reqBytes = request.getContentAsByteArray();
+ final byte[] resBytes = response.getContentAsByteArray();
+ final String reqBody = isTextContentType(reqCt)
+ ? new String(reqBytes, request.getCharacterEncoding() != null
+ ? request.getCharacterEncoding() : StandardCharsets.UTF_8)
+ : "(non-text body)";
+ final String resBody = isTextContentType(resCt)
+ ? new String(resBytes, response.getCharacterEncoding() != null
+ ? response.getCharacterEncoding() : StandardCharsets.UTF_8)
+ : "(non-text body)";추가 메서드:
private static boolean isTextContentType(String ct) {
if (ct == null) return false;
final String s = ct.toLowerCase();
return s.startsWith("text/")
|| s.contains("json")
|| s.contains("xml")
|| s.contains("html")
|| s.contains("x-www-form-urlencoded");
}🤖 Prompt for AI Agents
In
src/main/java/com/permitseoul/permitserver/global/filter/DevRequestResponseLoggingFilter.java
around lines 41-48, the code unconditionally decodes request/response bodies as
UTF-8; change it to first check Content-Type (use
request.getContentType()/response.getContentType()) with a helper
isTextContentType(ct) and only decode when that returns true, otherwise set a
placeholder like "[binary or non-text content omitted]"; when decoding, use
request.getCharacterEncoding() / response.getCharacterEncoding() (falling back
to StandardCharsets.UTF_8 if null) instead of hard-coded UTF-8; add the
suggested isTextContentType method to the class and use it for both request and
response checks.
🔥Pull requests
⛳️ 작업한 브랜치
👷 작업한 내용
🚨 참고 사항