Skip to content

Conversation

@sjk4618
Copy link
Member

@sjk4618 sjk4618 commented Oct 6, 2025

🔥Pull requests

⛳️ 작업한 브랜치

👷 작업한 내용

  • feat: dev용 모든 요청 로깅 필터(with Loki+Grafana)
  • loki와 grafana를 이용하여 dev에서 테스트와 qa를 진행할 때, 모든 요청에 대한 정보들을 loki에서 볼 수 있도록 dev 전용 로깅 필터를 추가하였습니다.

🚨 참고 사항

  • 400이나 500에러는 디스코드로 알림을 가도록 웹훅 연동할 예정입니다.

@sjk4618 sjk4618 self-assigned this Oct 6, 2025
@coderabbitai
Copy link

coderabbitai bot commented Oct 6, 2025

📝 Walkthrough

Summary by CodeRabbit

  • 신규 기능
    • 개발 환경에서 HTTP 요청/응답 로깅을 추가했습니다: 메서드, URL, 상태 코드, 처리 시간 및 본문 일부를 기록하며 본문 재읽기를 지원합니다.
    • 응답 본문을 복사하여 정상 전달을 보장하고, 과도하게 긴 본문은 절단합니다.
    • 로깅 오류는 무시되어 서비스에 영향이 없고, 기능은 dev 프로필에서만 활성화됩니다.

Walkthrough

dev 프로필에서만 활성화되는 요청/응답 로깅 필터가 추가됨. ContentCaching 래퍼로 요청/응답 본문을 캐싱하고, 처리 시간과 메서드/URL/상태코드/본문(일부 절단 포함)을 기록. 예외 발생 시 로깅 실패가 본 처리를 방해하지 않도록 보호. 응답 본문은 copyBodyToResponse로 복사하여 클라이언트 전달 보장.

Changes

Cohort / File(s) Change Summary
Dev 로깅 필터 추가
src/main/java/com/permitseoul/permitserver/global/filter/DevRequestResponseLoggingFilter.java
dev 프로필 전용 Filter 추가. 요청/응답을 ContentCachingRequestWrapper/ContentCachingResponseWrapper로 감싸고, 처리 시간 측정 및 메타/본문 로깅. 로깅 실패 시 예외 억제. 응답 본문 copyBodyToResponse 호출로 정상 반환 보장.

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
Loading

Estimated code review effort

🎯 3 (보통) | ⏱️ ~25 minutes

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed 제목은 새로운 dev 환경 요청 로깅 필터의 도입을 명확히 요약하며 변경 사항의 핵심을 잘 반영하고 있어 이 변경 사항을 한눈에 이해하기에 충분합니다.
Linked Issues Check ✅ Passed 새로 추가된 DevRequestResponseLoggingFilter 클래스는 dev 환경에서 HTTP 요청과 응답을 로깅하도록 구현되어 있으며 Loki와 Grafana 연계를 위해 로그를 남기도록 설계되어 이슈 #138의 요구 사항을 충족합니다.
Out of Scope Changes Check ✅ Passed 해당 PR은 DevRequestResponseLoggingFilter 파일 하나의 추가만을 포함하며 다른 영역 또는 불필요한 변경이 없어 이슈 범위 이탈 요소가 없습니다.
Description Check ✅ Passed 설명은 변경된 dev 전용 로깅 필터의 목적과 Loki+Grafana 통합 준비 사항을 언급하며 변경 내용과 일치하여 충분히 관련성을 갖추고 있습니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#138

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between 069ed9b and c60902e.

📒 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을 사용 중이므로 멀티라인 텍스트 블록을 안전하게 사용할 수 있습니다.

Comment on lines +29 to +35
final long start = System.currentTimeMillis();
chain.doFilter(reqWrapper, resWrapper);
final long duration = System.currentTimeMillis() - start;

logRequestResponse(reqWrapper, resWrapper, duration);
resWrapper.copyBodyToResponse();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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().

Comment on lines +41 to +48
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);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

문자셋/콘텐츠타입 고려 없이 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.

@sjk4618 sjk4618 merged commit ca75f2b into dev Oct 6, 2025
2 checks passed
@sjk4618 sjk4618 deleted the feat/#138 branch October 6, 2025 17:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: dev용 모든 요청 로깅 필터(with Loki+Grafana)

2 participants