Skip to content

모든 Request에 대해 로깅을 적용한다.#57

Merged
strongmhk merged 9 commits intodevelopfrom
feature/#53-aop-logging
Aug 19, 2025
Merged

모든 Request에 대해 로깅을 적용한다.#57
strongmhk merged 9 commits intodevelopfrom
feature/#53-aop-logging

Conversation

@strongmhk
Copy link
Copy Markdown
Member

@strongmhk strongmhk commented Aug 18, 2025

📄 PR 요약

✍🏻 PR 상세

  1. 메소드 로깅
    • AOP를 사용하여 service, controller의 메소드가 호출될 때에 한해 로그가 기록되도록 합니다.
    • 로깅 항목 : 메소드 이름, 클래스 경로, 파라미터, 처리 시간
  2. REST API 요청/응답 로깅
    • Filter를 사용하여 API 응답 시점에 로그가 기록되도록 합니다.
    • 로깅 항목 : 요청 method 및 URL(path variable 포함), query string, request body, reponse body(code, message, result 분리하여 기록), HTTP 상태 코드, 요청-응답 간 총 소요 시간
  3. API 요청 및 응답 과정에서 기록되는 모든 로그는 아래 항목을 포함하여 기록됩니다.
    • 관련 API 호출 시각, 호출자 IP
    • 1, 2에서 기록된 로그만 보기 위해서는 type 속성을 갖는 것만을 조회해야 합니다.
    • MDC를 적용해 모든 요청 로그에 request id를 붙입니다.
  4. 에러 트레이싱이 가능하도록 Sentry를 연동합니다.
    • local, dev, prod 환경별로 태그를 나누어 Sentry에 전송합니다.
    • Sentry 이슈의 title에는 에러 코드가 subtitle에는 에러 메시지가 표시되도록합니다.

👀 참고사항

✅ 체크리스트

  • PR 양식에 맞게 작성했습니다.
  • 모든 테스트가 통과했습니다.
  • 프로그램이 정상적으로 작동합니다.
  • 적절한 라벨을 설정했습니다.
  • 불필요한 코드를 제거했습니다.

🚪 연관된 이슈 번호

Closes #53

Summary by CodeRabbit

  • New Features
    • 오류 모니터링(Sentry) 통합으로 예외를 자동 보고하고 오류 코드로 그룹화합니다.
    • HTTP 요청/응답을 구조화해 로깅하고 민감정보를 마스킹합니다. X-Request-Id를 생성·전파하고 처리시간·상태별로 로그 레벨을 구분합니다.
    • 컨트롤러/서비스 메서드 호출을 계측해 실행시간과 파라미터를 기록합니다.
  • Chores
    • 모니터링·AOP 관련 의존성 추가 및 환경별 설정(로깅/모니터링) 적용, 테스트 런타임 설정 정리.

- TYPE을 HTTP, METHOD로 나눠 http request에 대한 로그와 메서드 로그 분리
- title은 errrorCode, subtitle은 message로 표시되도록 설정
@strongmhk strongmhk self-assigned this Aug 18, 2025
@strongmhk strongmhk added the ✨ feature New feature or request label Aug 18, 2025
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Aug 18, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

AOP 기반 메서드 로깅과 HTTP 요청/응답 로깅이 추가되고 Sentry 통합(빌드·설정·BeforeSend·예외 전송) 및 관련 Logback 설정이 도입되었습니다. 기존 ByteBuddy 테스트 self-attachment 설정은 제거되었습니다.

Changes

Cohort / File(s) Summary
Build & Sentry deps / config
build.gradle, src/main/resources/application.yml, src/main/resources/sentry.yml
Sentry Gradle 플러그인 및 BOM/스타터 추가, Spring AOP 의존성 추가, Sentry DSL(auth/org/projectName) 설정 추가, application.yml에 sentry.yml import 추가, ByteBuddy 테스트 에이전트 및 Test task 설정 제거.
Logback config
src/main/resources/logback-spring.xml
콘솔 및 Sentry appender 구성(동적 LOG_LEVEL, SentryAppender with ThresholdFilter), MethodLoggingAspect 전용 로거 분리.
Sentry runtime hook
src/main/java/.../global/config/sentry/SentryConfig.java
Sentry BeforeSend 콜백 빈 추가: ApplicationException 계열 예외의 타입을 error.code로 교체하고 원본 메시지를 extras에 보존하는 변조 로직 등록.
Global exception → Sentry
src/main/java/.../global/exception/GlobalExceptionHandler.java
여러 예외 핸들러에서 Sentry 전송 호출(sendErrorToSentry) 추가, transaction/path/error.code 태그 및 fingerprint 설정, 일부 파라미터명 변경.
HTTP request/response logging
src/main/java/.../global/log/HttpLoggingFilter.java, src/main/java/.../global/log/LogConst.java, src/main/java/.../global/log/LogUtils.java
ContentCaching 래퍼로 요청·응답 바디 캡처, MDC requestId 관리·응답 헤더 반영, 민감데이터 마스킹·절단, 구조화된 JSON 로그 생성 및 상태별 로깅 레벨 적용, actuator/health 제외 로직 등 도입.
AOP method logging
src/main/java/.../global/log/MethodLoggingAspect.java
RestController/Service 대상 Around 어드바이스 추가: 파라미터·실행시간·클래스/메서드 정보 로그, 예외 발생 시 에러 로그 포함.

Sequence Diagram(s)

sequenceDiagram
  participant C as Client
  participant F as HttpLoggingFilter
  participant A as MethodLoggingAspect
  participant Ctl as Controller
  participant Svc as Service
  participant GEH as GlobalExceptionHandler
  participant Sen as Sentry

  C->>F: HTTP Request (wrap cache)
  F->>A: Enter controller (AOP around)
  A->>Ctl: Proceed to controller
  Ctl->>Svc: Business call
  alt Success
    Svc-->>Ctl: Result
    Ctl-->>A: Return
    A-->>F: Response
    F-->>C: Log + Response
  else Exception
    Svc--x Ctl: Exception
    Ctl-->>GEH: Handle exception
    GEH->>Sen: Capture exception (tags/fingerprint)
    GEH-->>F: Error Response
    F-->>C: Log + Error Response
  end
Loading
sequenceDiagram
  participant App as App Startup
  participant SB as Spring Boot
  participant Build as build.gradle
  participant Scfg as SentryConfig
  participant LB as Logback

  App->>SB: Initialize context (dependencies from Build)
  SB->>Scfg: Register BeforeSend bean
  SB->>LB: Load logback-spring.xml (console + sentry)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Assessment against linked issues

Objective Addressed Explanation
AOP 로깅 구축 (#53)
모든 Request, Response에 대해 로깅 적용 (#53)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Add Sentry Gradle plugin and sentry DSL config (build.gradle) #53 요구는 AOP/HTTP 로깅이며 빌드레벨 Sentry 플러그인/DSL 설치는 로깅 적용 범위와 직접 관련 없음.
Add sentry.yml and import in application.yml (src/main/resources/sentry.yml, src/main/resources/application.yml) 환경별 Sentry 설정은 요청/응답 로그·AOP 로깅 구현 요구와 별도인 에러 모니터링 설정임.
Logback Sentry appender (src/main/resources/logback-spring.xml) Logback에 SentryAppender 추가는 에러 전송/모니터링 관점의 변경으로, #53의 로깅 구현 목표와는 다른 범위임.
Sentry BeforeSend callback (src/main/java/.../global/config/sentry/SentryConfig.java) 이벤트 변조 로직은 Sentry 전송 처리에 관한 변경으로, #53의 AOP/HTTP 로깅 요구와 직접 관련되지 않음.
GlobalExceptionHandler에 Sentry 전송 추가 (src/main/java/.../global/exception/GlobalExceptionHandler.java) 예외를 Sentry로 전송하는 로직은 에러 모니터링 목적의 확장으로, 단순 로깅 적용 요구를 넘어서는 변경임.

Possibly related PRs

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Semgrep (1.128.1)
src/main/java/akuma/whiplash/global/log/MethodLoggingAspect.java

{"version":"1.128.1","results":[],"errors":[{"code":4,"level":"error","type":"InvalidRuleSchemaError","long_msg":"{'pattern-not-either': [{'pattern': '@GetMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@PostMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@PutMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@DeleteMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@PatchMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}]} is not valid under any of the given schemas","short_msg":"Invalid rule schema","spans":[{"file":".semgrep/semgrep.yml","start":{"line":89,"col":9,"offset":-1},"end":{"line":120,"col":5,"offset":-1},"source_hash":"b582dea1be576d791d43c1fb4f7261c55ab9e08f176bcb3c071834a52d4d2d6d"}]},{"code":7,"level":"error","type":"SemgrepError","message":"invalid configuration file found (1 configs were invalid)"}],"paths":{"scanned":[]},"skipped_rules":[]}

src/main/java/akuma/whiplash/global/log/HttpLoggingFilter.java

{"version":"1.128.1","results":[],"errors":[{"code":4,"level":"error","type":"InvalidRuleSchemaError","long_msg":"{'pattern-not-either': [{'pattern': '@GetMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@PostMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@PutMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@DeleteMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@PatchMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}]} is not valid under any of the given schemas","short_msg":"Invalid rule schema","spans":[{"file":".semgrep/semgrep.yml","start":{"line":89,"col":9,"offset":-1},"end":{"line":120,"col":5,"offset":-1},"source_hash":"b582dea1be576d791d43c1fb4f7261c55ab9e08f176bcb3c071834a52d4d2d6d"}]},{"code":7,"level":"error","type":"SemgrepError","message":"invalid configuration file found (1 configs were invalid)"}],"paths":{"scanned":[]},"skipped_rules":[]}

src/main/java/akuma/whiplash/global/log/LogConst.java

{"version":"1.128.1","results":[],"errors":[{"code":4,"level":"error","type":"InvalidRuleSchemaError","long_msg":"{'pattern-not-either': [{'pattern': '@GetMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@PostMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@PutMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@DeleteMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@PatchMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}]} is not valid under any of the given schemas","short_msg":"Invalid rule schema","spans":[{"file":".semgrep/semgrep.yml","start":{"line":89,"col":9,"offset":-1},"end":{"line":120,"col":5,"offset":-1},"source_hash":"b582dea1be576d791d43c1fb4f7261c55ab9e08f176bcb3c071834a52d4d2d6d"}]},{"code":7,"level":"error","type":"SemgrepError","message":"invalid configuration file found (1 configs were invalid)"}],"paths":{"scanned":[]},"skipped_rules":[]}

  • 2 others

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9e3a755 and 55631d8.

📒 Files selected for processing (8)
  • build.gradle (4 hunks)
  • src/main/java/akuma/whiplash/global/exception/GlobalExceptionHandler.java (6 hunks)
  • src/main/java/akuma/whiplash/global/log/HttpLoggingFilter.java (1 hunks)
  • src/main/java/akuma/whiplash/global/log/LogConst.java (1 hunks)
  • src/main/java/akuma/whiplash/global/log/LogUtils.java (1 hunks)
  • src/main/java/akuma/whiplash/global/log/MethodLoggingAspect.java (1 hunks)
  • src/main/resources/logback-spring.xml (1 hunks)
  • src/main/resources/sentry.yml (1 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/#53-aop-logging

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@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: 26

🔭 Outside diff range comments (2)
build.gradle (1)

35-92: Logback EvaluatorFilter 사용 시 Janino 의존성이 필요합니다.

logback-spring.xml에서 EvaluatorFilter(표현식) 사용을 추가했는데, Janino가 없으면 부팅 시 필터 생성 실패가 발생합니다. 두 가지 중 하나를 권장합니다.

  • 옵션 A(권장): logback-spring.xml에서 해당 EvaluatorFilter를 제거하고, 이미 구성된 logger additivity=false로만 제어합니다. (아래 파일 코멘트 참조)
  • 옵션 B: Janino를 런타임 의존성에 추가합니다.

옵션 B 적용 예시:

 dependencies {
   // ...
+  runtimeOnly 'org.codehaus.janino:janino'
 }
src/main/java/akuma/whiplash/global/exception/GlobalExceptionHandler.java (1)

33-63: 헬퍼 메서드로 URI/쿼리 추출 통일

중복 로직 제거와 일관성 유지를 위해 WebRequest에서 안전하게 URI/쿼리를 추출하는 헬퍼를 추가하는 것을 권장합니다.

아래 메서드들을 클래스 내부에 추가해 주세요.

private static String extractRequestUri(WebRequest request) {
    if (request instanceof ServletWebRequest swr) {
        return swr.getRequest().getRequestURI();
    }
    String desc = request.getDescription(false);
    if (desc != null && desc.startsWith("uri=")) return desc.substring(4);
    return desc;
}

private static String extractQueryString(WebRequest request) {
    if (request instanceof ServletWebRequest swr) {
        return swr.getRequest().getQueryString();
    }
    return null;
}

Also applies to: 84-106, 107-124

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b56dd09 and 9e3a755.

📒 Files selected for processing (10)
  • build.gradle (4 hunks)
  • src/main/java/akuma/whiplash/global/config/sentry/SentryConfig.java (1 hunks)
  • src/main/java/akuma/whiplash/global/exception/GlobalExceptionHandler.java (6 hunks)
  • src/main/java/akuma/whiplash/global/log/HttpLoggingFilter.java (1 hunks)
  • src/main/java/akuma/whiplash/global/log/LogConst.java (1 hunks)
  • src/main/java/akuma/whiplash/global/log/LogUtils.java (1 hunks)
  • src/main/java/akuma/whiplash/global/log/MethodLoggingAspect.java (1 hunks)
  • src/main/resources/application.yml (1 hunks)
  • src/main/resources/logback-spring.xml (1 hunks)
  • src/main/resources/sentry.yml (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java

📄 CodeRabbit Inference Engine (AGENT.md)

**/*.java: 세미콜론 누락, 들여쓰기 불일치, import 순서 등의 형식 오류를 검토합니다.
사용되지 않는 변수나 주석은 제거를 제안합니다.
한 줄이 너무 길거나 중첩이 과도한 경우 구조 개선을 권장합니다.

Files:

  • src/main/java/akuma/whiplash/global/config/sentry/SentryConfig.java
  • src/main/java/akuma/whiplash/global/log/HttpLoggingFilter.java
  • src/main/java/akuma/whiplash/global/log/LogConst.java
  • src/main/java/akuma/whiplash/global/log/LogUtils.java
  • src/main/java/akuma/whiplash/global/log/MethodLoggingAspect.java
  • src/main/java/akuma/whiplash/global/exception/GlobalExceptionHandler.java
🧠 Learnings (1)
📚 Learning: 2025-08-03T16:11:00.414Z
Learnt from: CR
PR: Central-MakeUs/Whiplash-Server#0
File: AGENT.md:0-0
Timestamp: 2025-08-03T16:11:00.414Z
Learning: Applies to **/*Exception.java : 애플리케이션에서 예외를 발생시킬 때 throw ApplicationException.from(CommonErrorCode.INTERNAL_SERVER_ERROR); 와 같이 ApplicationException의 from 메서드에 BaseErrorCode를 implements한 Enum의 value를 넣어 발생시킵니다.

Applied to files:

  • src/main/java/akuma/whiplash/global/exception/GlobalExceptionHandler.java
🧬 Code Graph Analysis (4)
src/main/resources/sentry.yml (2)
src/test/java/akuma/whiplash/common/config/PersistenceTest.java (1)
  • Target (14-28)
src/test/java/akuma/whiplash/common/config/IntegrationTest.java (1)
  • Target (12-22)
src/main/java/akuma/whiplash/global/log/HttpLoggingFilter.java (3)
src/main/java/akuma/whiplash/global/log/LogConst.java (1)
  • LogConst (3-12)
src/main/java/akuma/whiplash/global/log/MethodLoggingAspect.java (1)
  • Slf4j (18-75)
src/main/java/akuma/whiplash/global/log/LogUtils.java (1)
  • LogUtils (15-103)
src/main/java/akuma/whiplash/global/log/LogUtils.java (1)
src/main/java/akuma/whiplash/global/config/security/jwt/JwtUtils.java (1)
  • Component (36-132)
src/main/java/akuma/whiplash/global/exception/GlobalExceptionHandler.java (1)
src/main/java/akuma/whiplash/global/config/security/jwt/JwtUtils.java (1)
  • jwtExceptionHandler (100-113)
🪛 YAMLlint (1.37.1)
src/main/resources/sentry.yml

[warning] 6-6: too few spaces before comment: expected 2

(comments)


[warning] 20-20: too few spaces before comment: expected 2

(comments)


[warning] 31-31: too few spaces before comment: expected 2

(comments)


[error] 39-39: no new line character at the end of file

(new-line-at-end-of-file)

🔇 Additional comments (4)
build.gradle (1)

67-70: Sentry BOM + Starter 추가는 적절합니다.

버전 일원화와 Spring Boot Starter(Jakarta) 채택이 잘 반영되었습니다.

src/main/resources/application.yml (1)

14-14: sentry.yml import 추가 LGTM.

프로필별 Sentry 설정을 전역 구성에 자연스럽게 편입합니다.

src/main/resources/logback-spring.xml (2)

16-18: MethodLoggingAspect 전파 차단(additivity=false) 구성은 적절합니다.

콘솔에는 출력하되 루트로 전파되지 않아 Sentry로 가지 않는 설정이 목적에 부합합니다.


10-12: MDC requestId 출력 패턴 적절

MDC 키와 일치하는 requestId를 표시해 추적성을 높입니다. 인코딩(UTF-8) 명시도 👍

Comment on lines +17 to +23
sentry {
includeSourceContext = true

org = "akuma-ir"
projectName = "nuntteo_dev"
authToken = System.getenv("SENTRY_AUTH_TOKEN")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Sentry Gradle 플러그인 토큰/프로젝트 설정을 환경별로 유연하게 처리하세요.

로컬/CI 환경에 따라 SENTRY_AUTH_TOKEN이 없을 수 있습니다. 토큰이 없을 때 업로드 관련 태스크가 안전하게 스킵되도록 가드하거나, 프로젝트명을 환경 변수로 파라미터화하면 운영/개발 프로젝트 분리가 쉬워집니다.

아래처럼 조건부로 토큰을 적용하고, 프로젝트명을 환경 변수로 오버라이드 가능하게 하는 것을 제안합니다.

 sentry {
   includeSourceContext = true
-  org = "akuma-ir"
-  projectName = "nuntteo_dev"
-  authToken = System.getenv("SENTRY_AUTH_TOKEN")
+  org = "akuma-ir"
+  projectName = System.getenv("SENTRY_PROJECT") ?: "nuntteo_dev"
+  def token = System.getenv("SENTRY_AUTH_TOKEN")
+  if (token) {
+    authToken = token
+  }
 }
📝 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
sentry {
includeSourceContext = true
org = "akuma-ir"
projectName = "nuntteo_dev"
authToken = System.getenv("SENTRY_AUTH_TOKEN")
}
sentry {
includeSourceContext = true
org = "akuma-ir"
projectName = System.getenv("SENTRY_PROJECT") ?: "nuntteo_dev"
def token = System.getenv("SENTRY_AUTH_TOKEN")
if (token) {
authToken = token
}
}
🤖 Prompt for AI Agents
In build.gradle around lines 17-23, the Sentry plugin hardcodes org/projectName
and unconditionally reads SENTRY_AUTH_TOKEN which can be missing in local/CI;
change it to read auth token and project name from environment with sensible
fallbacks (e.g., System.getenv("SENTRY_AUTH_TOKEN") and
System.getenv("SENTRY_PROJECT") or default), and guard upload tasks so they are
no-ops when the token is absent (skip configuring or running Sentry upload tasks
if auth token is null/empty) so builds don’t fail in environments without
credentials.

Comment on lines +17 to +25
String code = event.getTag("error.code");
if (code != null) {
String originalMessage = ex.getValue(); // 기존 사람이 읽는 메시지
// 타이틀에 반영되는 'type'을 에러코드로 교체
ex.setType(code);
// 부제목(value)은 원래 메시지를 유지(또는 필요 시 축약)
ex.setValue(originalMessage);
// 참고용으로 원본도 extra에 보존
event.setExtra("original.message", originalMessage);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

컴파일 오류: event.getTag(...)는 존재하지 않습니다. getTags().get(...)을 사용하세요.

SentryEvent에는 getTag(String)가 없고, Map 형태의 getTags()만 제공합니다. 현재 코드는 컴파일에 실패합니다.

아래처럼 수정하세요(Null 안전 포함):

-                    String code = event.getTag("error.code");
+                    var tags = event.getTags();
+                    String code = (tags != null) ? tags.get("error.code") : null;
📝 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
String code = event.getTag("error.code");
if (code != null) {
String originalMessage = ex.getValue(); // 기존 사람이 읽는 메시지
// 타이틀에 반영되는 'type'을 에러코드로 교체
ex.setType(code);
// 부제목(value)은 원래 메시지를 유지(또는 필요 시 축약)
ex.setValue(originalMessage);
// 참고용으로 원본도 extra에 보존
event.setExtra("original.message", originalMessage);
var tags = event.getTags();
String code = (tags != null) ? tags.get("error.code") : null;
if (code != null) {
String originalMessage = ex.getValue(); // 기존 사람이 읽는 메시지
// 타이틀에 반영되는 'type'을 에러코드로 교체
ex.setType(code);
// 부제목(value)은 원래 메시지를 유지(또는 필요 시 축약)
ex.setValue(originalMessage);
// 참고용으로 원본도 extra에 보존
event.setExtra("original.message", originalMessage);
🤖 Prompt for AI Agents
In src/main/java/akuma/whiplash/global/config/sentry/SentryConfig.java around
lines 17 to 25, replace the non-existent event.getTag("error.code") call with a
null-safe lookup from the tags map (e.g., getTags() != null ?
getTags().get("error.code") : null); then proceed only if the retrieved code is
non-null, preserving originalMessage from ex.getValue(), setting
ex.setType(code), ex.setValue(originalMessage), and
event.setExtra("original.message", originalMessage). Ensure you guard against a
null tags map and a null originalMessage to avoid NPEs.

Comment on lines 3 to 15
import akuma.whiplash.global.response.ApplicationResponse;
import akuma.whiplash.global.response.code.BaseErrorCode;
import akuma.whiplash.global.response.code.CommonErrorCode;
import io.sentry.Sentry;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Sentry 연동 시 유용 태그/유틸 의존성 추가 필요

requestId 태그 및 쿼리 마스킹을 위해 MDC, LogConst, LogUtils 임포트가 필요합니다.

 import akuma.whiplash.global.response.ApplicationResponse;
 import akuma.whiplash.global.response.code.BaseErrorCode;
 import akuma.whiplash.global.response.code.CommonErrorCode;
+import akuma.whiplash.global.log.LogConst;
+import akuma.whiplash.global.log.LogUtils;
 import io.sentry.Sentry;
 import jakarta.servlet.http.HttpServletRequest;
...
 import java.util.stream.Collectors;
+import org.slf4j.MDC;
📝 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
import akuma.whiplash.global.response.ApplicationResponse;
import akuma.whiplash.global.response.code.BaseErrorCode;
import akuma.whiplash.global.response.code.CommonErrorCode;
import io.sentry.Sentry;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import akuma.whiplash.global.response.ApplicationResponse;
import akuma.whiplash.global.response.code.BaseErrorCode;
import akuma.whiplash.global.response.code.CommonErrorCode;
import akuma.whiplash.global.log.LogConst;
import akuma.whiplash.global.log.LogUtils;
import io.sentry.Sentry;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import org.slf4j.MDC;
import lombok.extern.slf4j.Slf4j;
🤖 Prompt for AI Agents
In src/main/java/akuma/whiplash/global/exception/GlobalExceptionHandler.java
around lines 3 to 15, the Sentry integration needs additional imports for
requestId tagging and query masking; add imports for org.slf4j.MDC and the
project's LogConst and LogUtils (or their correct packages) so the handler can
read the MDC requestId and use LogUtils/LogConst to mask sensitive query
parameters before sending to Sentry; ensure the import statements use the
correct fully-qualified package names used elsewhere in the codebase.

Comment on lines +51 to +56
sendErrorToSentry(
e,
request.getDescription(false), // ex: "uri=/api/alarms/100/checkin"
request.getParameterMap().toString(), // 쿼리 스트링 (없으면 빈 Map)
e.getBody().getTitle()
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

request.getDescription(false)uri=/path 형식입니다 → Sentry 트랜잭션/태그에 부적합

실제 URI/쿼리스트링을 추출해 전달하세요. 또한 에러코드는 커스텀 코드로 사용하는 것이 그룹핑에 유리합니다.

-        sendErrorToSentry(
-            e,
-            request.getDescription(false), // ex: "uri=/api/alarms/100/checkin"
-            request.getParameterMap().toString(), // 쿼리 스트링 (없으면 빈 Map)
-            e.getBody().getTitle()
-        );
+        sendErrorToSentry(
+            e,
+            extractRequestUri(request),
+            LogUtils.maskSensitiveQuery(extractQueryString(request)),
+            CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.getCustomCode()
+        );

아래 추가 헬퍼 메서드 코드를 참조해 주세요.

📝 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
sendErrorToSentry(
e,
request.getDescription(false), // ex: "uri=/api/alarms/100/checkin"
request.getParameterMap().toString(), // 쿼리 스트링 (없으면 빈 Map)
e.getBody().getTitle()
);
sendErrorToSentry(
e,
extractRequestUri(request),
LogUtils.maskSensitiveQuery(extractQueryString(request)),
CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.getCustomCode()
);
🤖 Prompt for AI Agents
In src/main/java/akuma/whiplash/global/exception/GlobalExceptionHandler.java
around lines 51 to 56, the code passes request.getDescription(false) (which
returns "uri=/path") and request.getParameterMap().toString() to Sentry and uses
e.getBody().getTitle() as identifier; change this to extract the real request
URI and query string (e.g., request.getRequestURI() and request.getQueryString()
or build from request.getParameterMap() when null) and send those values to
Sentry instead, and use a custom error code from the exception body
(e.getBody().getCode()) for grouping/tagging; update the sendErrorToSentry call
to accept and forward (exception, request.getRequestURI(), actualQueryString,
e.getBody().getCode()) accordingly and ensure null-safe handling of query
string.


sentry:
dsn: ${SENTRY_DSN}
send-default-pii: false # 민감정보 수집 X
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

YAML 포맷 경고 해결: 주석 앞 공백과 파일 끝 개행을 추가하세요.

ymlint 경고를 해소하려면 주석 앞 공백을 늘리고, 파일 끝에 개행을 추가하세요. 기능상 영향은 없지만 일관된 포맷 유지에 도움이 됩니다.

아래처럼 조정하세요.

-  send-default-pii: false # 민감정보 수집 X
+  send-default-pii: false  # 민감정보 수집 X
-  traces-sample-rate: 1.0 # 트랜잭션 100% 수집
+  traces-sample-rate: 1.0  # 트랜잭션 100% 수집
-  traces-sample-rate: 1.0 # 트랜잭션 100% 수집
+  traces-sample-rate: 1.0  # 트랜잭션 100% 수집
-  dsn: ""          # 혹시 로깅 앱렌더가 있어도 안전하게
+  dsn: ""           # 혹시 로깅 앱렌더가 있어도 안전하게
+ 

Also applies to: 20-20, 31-31, 39-39

🧰 Tools
🪛 YAMLlint (1.37.1)

[warning] 6-6: too few spaces before comment: expected 2

(comments)

🤖 Prompt for AI Agents
In src/main/resources/sentry.yml around line 6 (and similarly lines 20, 31, 39),
the YAML linter warns because comments lack a preceding space and the file is
missing a trailing newline; update each commented line to have a single space
before the '#' (e.g. "value: true  # comment") and ensure the file ends with a
newline character so the final line is terminated.

Comment on lines +16 to +21
dsn: ${SENTRY_DSN}
send-default-pii: true
logs:
enabled: true
traces-sample-rate: 1.0 # 트랜잭션 100% 수집
environment: dev
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

운영/개발 트랜잭션 100% 수집은 비용/성능에 부담입니다.

prod/dev에서 traces-sample-rate: 1.0은 오버헤드와 데이터 비용이 큽니다. 트래픽 규모에 따라 0.1~0.3 수준 샘플링을 권장합니다. 릴리즈 직후 안정화 구간에만 일시적으로 1.0을 쓰고, 이후 낮추는 정책도 고려하세요.

원한다면 프로파일별/환경변수 기반 샘플링으로 바꾸는 패치 제안 가능합니다.

Also applies to: 26-33


🏁 Script executed:

#!/bin/bash
set -e
file=$(fd sentry.yml -t f)
echo "파일 경로: $file"
sed -n '1,200p' "$file"

Length of output: 886


환경별 Sentry 트랜잭션 샘플링 비율 조정 제안
dev/prod 프로파일에서 traces-sample-rate: 1.0 으로 전 트랜잭션을 수집하고 있어, 비용 및 성능 부담이 큽니다. 트래픽 규모에 따라 0.1~0.3 수준의 샘플링을 권장하며, 릴리즈 직후 안정화 구간에만 일시적으로 1.0으로 설정한 후 낮추는 정책도 고려해주세요. 또한 환경 변수나 프로파일별 동적 샘플링 설정으로 관리하는 방안도 제안드립니다.

점검 대상

  • 파일: src/main/resources/sentry.yml
    • spring.config.activate.on-profile: dev
      • traces-sample-rate: 1.00.1~0.3
    • spring.config.activate.on-profile: prod
      • traces-sample-rate: 1.00.1~0.3

원한다면 프로파일별 또는 환경 변수 기반 샘플링으로 전환하는 예시 패치도 제안 가능합니다.

🧰 Tools
🪛 YAMLlint (1.37.1)

[warning] 20-20: too few spaces before comment: expected 2

(comments)

🤖 Prompt for AI Agents
In src/main/resources/sentry.yml around lines 16 to 21, the traces-sample-rate
is set to 1.0 for dev and prod which collects 100% of transactions; change the
value to a lower rate (recommended 0.1–0.3) for both dev and prod to reduce cost
and overhead, and update the file to support profile- or
environment-variable-driven values (e.g., replace the hardcoded 1.0 with a
placeholder backed by an env var like ${SENTRY_TRACES_SAMPLE_RATE:0.1} or add
spring.config.activate.on-profile blocks that set traces-sample-rate to 1.0 only
for temporary release-stabilization profiles and 0.1–0.3 for normal dev/prod);
ensure documentation/comments explain how to override the env var for temporary
1.0 sampling during releases.

Comment on lines +17 to +18
send-default-pii: true
logs:
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Aug 18, 2025

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

send-default-pii: true 설정은 개인정보 수집 이슈가 될 수 있습니다.

prod/dev에서 PII 수집 허용은 사내 보안/개인정보 정책 검토가 필요합니다. 최소수집 원칙에 따라 false로 두고, 필요한 키만 tag/extra로 선별 전송하는 방안을 권장합니다.

원하시면 민감 필드 마스킹/제외 정책을 환경설정과 코드(스코프/BeforeSend)에서 일관되게 적용하는 패치를 제안하겠습니다.

Also applies to: 27-28

🤖 Prompt for AI Agents
In src/main/resources/sentry.yml around lines 17-18 (and also apply the same
change to lines 27-28), send-default-pii is set to true which enables collection
of personal data; change send-default-pii to false in this config and document
any individual PII fields that need to be sent, then implement selective
tagging/extra population only for those fields. Additionally, add or reference
environment-driven flags for masking/excluding sensitive fields and ensure the
application enforces masking at capture time by implementing BeforeSend/Scope
filters in the Sentry SDK to redact or drop sensitive keys before events are
sent.

Copy link
Copy Markdown
Member Author

@strongmhk strongmhk Aug 18, 2025

Choose a reason for hiding this comment

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

@coderabbitai 민감 필드 마스킹/제외 정책을 환경설정과 코드(스코프/BeforeSend)에서 일관되게 적용하는 패치 제안해줘

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

@strongmhk 현재 코드베이스를 분석해서 민감 필드 마스킹 정책을 일관되게 적용하는 패치를 제안하겠습니다.

민감 필드 마스킹/제외 정책을 환경설정과 코드에서 일관되게 적용하는 종합적인 패치를 제안드립니다:

1. sentry.yml 수정 - PII 수집 비활성화

---
spring.config.activate.on-profile: local

sentry:
  dsn: ${SENTRY_DSN}
  send-default-pii: false # 모든 환경에서 기본 PII 수집 비활성화
  logs:
    enabled: true
  traces-sample-rate: 0.0
  environment: local

---
spring.config.activate.on-profile: dev

sentry:
  dsn: ${SENTRY_DSN}
  send-default-pii: false # PII 수집 비활성화, 선별적 전송으로 변경
  logs:
    enabled: true
  traces-sample-rate: 1.0
  environment: dev

---
spring.config.activate.on-profile: prod

sentry:
  dsn: ${SENTRY_DSN}
  send-default-pii: false # PII 수집 비활성화, 선별적 전송으로 변경
  logs:
    enabled: true
  traces-sample-rate: 1.0
  environment: prod

---
spring.config.activate.on-profile: test

sentry:
  enabled: false
  dsn: ""

2. SentryConfig.java 강화 - 종합적인 마스킹 정책

package akuma.whiplash.global.config.sentry;

import akuma.whiplash.global.log.LogUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.sentry.SentryOptions;
import io.sentry.protocol.SentryException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

import java.util.Arrays;
import java.util.Map;
import java.util.Set;

@Slf4j
@Configuration
@RequiredArgsConstructor
public class SentryConfig {
    
    private final ObjectMapper objectMapper;
    private final Environment environment;
    
    // 환경별 마스킹 정책 설정
    private static final Set<String> PRODUCTION_SENSITIVE_KEYS = Set.of(
        "password", "passwd", "pwd", "authorization", "accessToken", "refreshToken", 
        "token", "secret", "apiKey", "privateKey", "ssn", "email", "phone", "address"
    );
    
    private static final Set<String> DEVELOPMENT_SENSITIVE_KEYS = Set.of(
        "password", "passwd", "pwd", "authorization", "accessToken", "refreshToken", "token", "secret"
    );
    
    @Bean
    public SentryOptions.BeforeSendCallback beforeSendCallback() {
        return (event, hint) -> {
            try {
                // 환경별 마스킹 수준 결정
                boolean isProductionLevel = Arrays.asList(environment.getActiveProfiles())
                    .contains("prod");
                Set<String> sensitiveKeys = isProductionLevel ? 
                    PRODUCTION_SENSITIVE_KEYS : DEVELOPMENT_SENSITIVE_KEYS;
                
                // 1. ApplicationException 처리 (기존 로직 유지)
                if (event.getThrowable() != null) {
                    SentryException sentryException = event.getExceptions().get(0);
                    if (sentryException.getType().endsWith("ApplicationException") && 
                        event.getTag("error.code") != null) {
                        sentryException.setType(event.getTag("error.code"));
                        event.setExtra("original.message", sentryException.getValue());
                    }
                }
                
                // 2. Extra 데이터 마스킹
                if (event.getExtra() != null) {
                    Map<String, Object> maskedExtra = maskSensitiveData(event.getExtra(), sensitiveKeys);
                    event.getExtra().clear();
                    event.getExtra().putAll(maskedExtra);
                }
                
                // 3. Context 데이터 마스킹
                if (event.getContexts() != null) {
                    event.getContexts().forEach((key, context) -> {
                        if (context != null && context.containsKey("data")) {
                            Object data = context.get("data");
                            if (data instanceof Map) {
                                @SuppressWarnings("unchecked")
                                Map<String, Object> dataMap = (Map<String, Object>) data;
                                Map<String, Object> maskedData = maskSensitiveData(dataMap, sensitiveKeys);
                                context.put("data", maskedData);
                            }
                        }
                    });
                }
                
                // 4. 요청/응답 바디 마스킹 (JSON 형태인 경우)
                maskRequestResponseBodies(event, sensitiveKeys);
                
                log.debug("Sentry event processed with masking policy: {}", 
                    isProductionLevel ? "PRODUCTION" : "DEVELOPMENT");
                
                return event;
                
            } catch (Exception e) {
                log.error("Error processing Sentry event for masking", e);
                return event; // 마스킹 실패시에도 이벤트는 전송
            }
        };
    }
    
    @SuppressWarnings("unchecked")
    private Map<String, Object> maskSensitiveData(Map<String, Object> data, Set<String> sensitiveKeys) {
        return (Map<String, Object>) LogUtils.maskRecursive(data, sensitiveKeys);
    }
    
    private void maskRequestResponseBodies(io.sentry.SentryEvent event, Set<String> sensitiveKeys) {
        // 요청 바디 마스킹
        Object requestBody = event.getExtra("request.body");
        if (requestBody instanceof String) {
            String maskedBody = LogUtils.maskSensitiveJson(objectMapper, (String) requestBody, sensitiveKeys);
            event.setExtra("request.body", maskedBody);
        }
        
        // 응답 바디 마스킹
        Object responseBody = event.getExtra("response.body");
        if (responseBody instanceof String) {
            String maskedBody = LogUtils.maskSensitiveJson(objectMapper, (String) responseBody, sensitiveKeys);
            event.setExtra("response.body", maskedBody);
        }
    }
}

3. LogUtils.java 확장 - 환경별 마스킹 지원

// LogUtils.java에 추가할 메서드들

/**
 * 환경별 민감 키 세트를 사용한 JSON 마스킹
 */
public static String maskSensitiveJson(ObjectMapper om, String json, Set<String> sensitiveKeys) {
    if (json == null || json.isBlank()) {
        return json;
    }
    
    try {
        Object parsed = om.readValue(json, Object.class);
        Object masked = maskRecursive(parsed, sensitiveKeys);
        return om.writeValueAsString(masked);
    } catch (JsonProcessingException e) {
        return json; // 파싱 실패시 원본 반환
    }
}

/**
 * 커스텀 민감 키 세트를 사용한 재귀적 마스킹
 */
public static Object maskRecursive(Object obj, Set<String> sensitiveKeys) {
    if (obj instanceof Map<?, ?>) {
        Map<String, Object> result = new LinkedHashMap<>();
        @SuppressWarnings("unchecked")
        Map<String, Object> map = (Map<String, Object>) obj;
        
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            
            if (sensitiveKeys.stream().anyMatch(k -> key.toLowerCase().contains(k.toLowerCase()))) {
                result.put(key, "***MASKED***");
            } else {
                result.put(key, maskRecursive(value, sensitiveKeys));
            }
        }
        return result;
    } else if (obj instanceof List<?>) {
        @SuppressWarnings("unchecked")
        List<Object> list = (List<Object>) obj;
        return list.stream()
                  .map(item -> maskRecursive(item, sensitiveKeys))
                  .collect(Collectors.toList());
    }
    
    return obj;
}

4. application.yml에 마스킹 정책 설정 추가

# application.yml에 추가
logging:
  config: classpath:logback-spring.xml
  
sentry:
  masking:
    enabled: true
    strict-mode: false # prod에서는 true로 설정
    custom-sensitive-keys: # 프로젝트별 추가 민감 키
      - customSecretField
      - internalToken

5. 환경별 설정을 위한 application-prod.yml

# application-prod.yml (새로 생성)
sentry:
  masking:
    strict-mode: true # 프로덕션에서는 엄격한 마스킹 적용
    custom-sensitive-keys:
      - customSecretField
      - internalToken
      - clientId
      - deviceId

주요 개선 사항

  1. 환경별 차등 마스킹: 개발/운영 환경에 따라 마스킹 수준 조절
  2. 일관된 정책 적용: LogUtils와 SentryConfig에서 동일한 마스킹 로직 사용
  3. 설정 기반 확장성: application.yml을 통한 커스텀 민감 키 추가 가능
  4. 안전한 실패 처리: 마스킹 실패시에도 이벤트 전송 보장
  5. 성능 최적화: 개발 환경에서는 최소한의 마스킹으로 디버깅 효율성 확보

이 패치를 적용하면 모든 Sentry 이벤트에서 민감 정보가 일관되게 마스킹되며, 환경별로 적절한 수준의 보안 정책을 적용할 수 있습니다.


🏁 Script executed:

#!/bin/bash

# 현재 Sentry 설정과 마스킹 관련 코드 분석
echo "=== Current Sentry Configuration ==="
cat src/main/resources/sentry.yml

echo -e "\n=== Current SentryConfig.java ==="
cat src/main/java/akuma/whiplash/global/config/sentry/SentryConfig.java

echo -e "\n=== Current LogUtils masking implementation ==="
grep -A 20 -B 5 "SENSITIVE_KEYS" src/main/java/akuma/whiplash/global/log/LogUtils.java

echo -e "\n=== Current HttpLoggingFilter usage of masking ==="
grep -A 5 -B 5 "maskSensitive" src/main/java/akuma/whiplash/global/log/HttpLoggingFilter.java

Length of output: 5139


현재 코드를 분석한 결과, 이미 기본적인 마스킹 인프라가 구축되어 있네요. 기존 코드를 활용해서 더 실용적인 패치를 제안하겠습니다:

1. sentry.yml 수정 - dev/prod PII 설정 변경

---
spring.config.activate.on-profile: dev

sentry:
  dsn: ${SENTRY_DSN}
  send-default-pii: false # true에서 false로 변경
  logs:
    enabled: true
  traces-sample-rate: 1.0 # 트랜잭션 100% 수집
  environment: dev

---
spring.config.activate.on-profile: prod

sentry:
  dsn: ${SENTRY_DSN}
  send-default-pii: false # true에서 false로 변경
  logs:
    enabled: true
  traces-sample-rate: 1.0 # 트랜잭션 100% 수집
  environment: prod

2. LogUtils.java 개선 - 환경별/확장 가능한 마스킹

// LogUtils.java에 추가/수정할 부분

public final class LogUtils {
    private LogUtils() {}

    // 기본 민감 키 (기존 유지)
    private static final Set<String> SENSITIVE_KEYS = Set.of(
        "password", "passwd", "pwd", "authorization", "accessToken", "refreshToken", "token", "secret"
    );
    
    // 프로덕션 환경용 확장 민감 키
    private static final Set<String> EXTENDED_SENSITIVE_KEYS = Set.of(
        "password", "passwd", "pwd", "authorization", "accessToken", "refreshToken", 
        "token", "secret", "apiKey", "privateKey", "email", "phone", "ssn", "address",
        "deviceId", "clientId", "sessionId"
    );

    // 기존 메서드 유지하되, 대소문자 무시 개선
    public static String maskSensitiveJson(ObjectMapper om, String json) {
        return maskSensitiveJson(om, json, SENSITIVE_KEYS);
    }
    
    // 환경별 키 세트를 지원하는 새로운 메서드
    public static String maskSensitiveJson(ObjectMapper om, String json, Set<String> sensitiveKeys) {
        if (json == null || json.isBlank()) {
            return json;
        }
        try {
            Object parsed = om.readValue(json, Object.class);
            Object masked = maskRecursive(parsed, sensitiveKeys);
            return om.writeValueAsString(masked);
        } catch (JsonProcessingException e) {
            return json;
        }
    }
    
    // 프로덕션 환경용 마스킹 메서드
    public static String maskSensitiveJsonForProduction(ObjectMapper om, String json) {
        return maskSensitiveJson(om, json, EXTENDED_SENSITIVE_KEYS);
    }

    // 기존 maskRecursive를 오버로드해서 커스텀 키 지원
    private static Object maskRecursive(Object node) {
        return maskRecursive(node, SENSITIVE_KEYS);
    }
    
    @SuppressWarnings("unchecked")
    private static Object maskRecursive(Object node, Set<String> sensitiveKeys) {
        if (node instanceof Map<?,?> m) {
            Map<String, Object> copy = new LinkedHashMap<>();
            for (Map.Entry<?,?> e : m.entrySet()) {
                String k = String.valueOf(e.getKey());
                Object v = e.getValue();
                // 대소문자 무시하고 부분 일치도 지원
                if (isSensitiveKey(k, sensitiveKeys)) {
                    copy.put(k, "****");
                } else {
                    copy.put(k, maskRecursive(v, sensitiveKeys));
                }
            }
            return copy;
        } else if (node instanceof List<?> l) {
            List<Object> copy = new ArrayList<>(l.size());
            for (Object v : l) {
                copy.add(maskRecursive(v, sensitiveKeys));
            }
            return copy;
        }
        return node;
    }
    
    // 대소문자 무시 + 부분 일치 지원
    private static boolean isSensitiveKey(String key, Set<String> sensitiveKeys) {
        String lowerKey = key.toLowerCase();
        return sensitiveKeys.stream()
                          .anyMatch(sensitiveKey -> lowerKey.contains(sensitiveKey.toLowerCase()));
    }
}

3. SentryConfig.java 강화 - 기존 LogUtils 활용

package akuma.whiplash.global.config.sentry;

import akuma.whiplash.global.log.LogUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.sentry.SentryOptions;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

import java.util.Arrays;

@Slf4j
@Configuration
@RequiredArgsConstructor
public class SentryConfig {
    
    private final ObjectMapper objectMapper;
    private final Environment environment;

    @Bean
    public SentryOptions.BeforeSendCallback beforeSendCallback() {
        return (event, hint) -> {
            try {
                // 프로덕션 환경 체크
                boolean isProduction = Arrays.asList(environment.getActiveProfiles()).contains("prod");
                
                // 1. 기존 ApplicationException 처리 (유지)
                if (event.getExceptions() != null && !event.getExceptions().isEmpty()) {
                    var ex = event.getExceptions().get(0);
                    if (ex.getType() != null && ex.getType().endsWith("ApplicationException")) {
                        String code = event.getTag("error.code");
                        if (code != null) {
                            String originalMessage = ex.getValue();
                            ex.setType(code);
                            ex.setValue(originalMessage);
                            event.setExtra("original.message", originalMessage);
                        }
                    }
                }
                
                // 2. Extra 데이터의 민감 정보 마스킹
                if (event.getExtra() != null && !event.getExtra().isEmpty()) {
                    maskExtraData(event, isProduction);
                }
                
                log.debug("Sentry event processed with {} masking policy", 
                    isProduction ? "PRODUCTION" : "DEVELOPMENT");
                
                return event;
                
            } catch (Exception e) {
                log.error("Error processing Sentry event for masking", e);
                return event; // 마스킹 실패해도 이벤트는 전송
            }
        };
    }
    
    private void maskExtraData(io.sentry.SentryEvent event, boolean isProduction) {
        // 요청/응답 바디가 있다면 마스킹 적용
        Object requestBody = event.getExtra("request.body");
        if (requestBody instanceof String) {
            String masked = isProduction ? 
                LogUtils.maskSensitiveJsonForProduction(objectMapper, (String) requestBody) :
                LogUtils.maskSensitiveJson(objectMapper, (String) requestBody);
            event.setExtra("request.body", masked);
        }
        
        Object responseBody = event.getExtra("response.body");
        if (responseBody instanceof String) {
            String masked = isProduction ? 
                LogUtils.maskSensitiveJsonForProduction(objectMapper, (String) responseBody) :
                LogUtils.maskSensitiveJson(objectMapper, (String) responseBody);
            event.setExtra("response.body", masked);
        }
        
        // 기타 Extra 필드들도 JSON 형태라면 마스킹
        event.getExtra().forEach((key, value) -> {
            if (value instanceof String && isJsonLike((String) value)) {
                String masked = isProduction ?
                    LogUtils.maskSensitiveJsonForProduction(objectMapper, (String) value) :
                    LogUtils.maskSensitiveJson(objectMapper, (String) value);
                event.setExtra(key, masked);
            }
        });
    }
    
    private boolean isJsonLike(String str) {
        return str != null && ((str.trim().startsWith("{") && str.trim().endsWith("}")) ||
                              (str.trim().startsWith("[") && str.trim().endsWith("]")));
    }
}

4. GlobalExceptionHandler.java 개선 - Sentry 전송시 마스킹 적용

// GlobalExceptionHandler.java의 sendErrorToSentry 메서드 개선
private static void sendErrorToSentry(Exception ex, String requestUri, String queryString, String errorCode) {
    Sentry.withScope(scope -> {
        scope.setTransaction(requestUri);
        scope.setTag("path", requestUri);
        scope.setTag("error.code", errorCode);
        
        // 에러 코드로 그룹핑 (기존 유지)
        scope.setFingerprint(List.of(errorCode));
        
        // 쿼리 파라미터는 민감정보가 포함될 수 있으므로 주의해서 추가
        if (queryString != null && !queryString.isBlank()) {
            // 간단한 민감 파라미터 체크
            if (!containsSensitiveParams(queryString)) {
                scope.setExtra("query", queryString);
            } else {
                scope.setExtra("query", "***MASKED_SENSITIVE_PARAMS***");
            }
        }
        
        Sentry.captureException(ex);
    });
}

private static boolean containsSensitiveParams(String queryString) {
    String lowerQuery = queryString.toLowerCase();
    return lowerQuery.contains("password") || lowerQuery.contains("token") || 
           lowerQuery.contains("secret") || lowerQuery.contains("key");
}

주요 개선점

  1. 기존 코드 활용: 이미 구현된 LogUtils.maskSensitiveJson 기반으로 확장
  2. 환경별 차등 적용: 개발 환경은 기본 마스킹, 프로덕션은 강화된 마스킹
  3. 실용적 접근: 복잡한 설정 없이 코드 레벨에서 환경 감지
  4. 점진적 적용: 기존 동작은 유지하면서 Sentry만 강화
  5. 안전한 실패: 마스킹 오류가 발생해도 로깅/모니터링 기능 유지

이 패치는 기존 로깅 동작을 변경하지 않으면서 Sentry로 전송되는 데이터만 선별적으로 마스킹합니다.

- ERROR 레벨 이상의 로그는 Sentry 이벤트(Event) 로 전송
- INFO 레벨 이상의 로그는 Breadcrumb으로 수집,
Breadcrumb는 오류가 발생하기 전후에 어떤 로그/동작이 있었는지 “빵부스러기”처럼 추적 정보로 저장하는 것
@strongmhk strongmhk merged commit adebc7a into develop Aug 19, 2025
1 of 2 checks passed
strongmhk added a commit that referenced this pull request Aug 21, 2025
- add route to pre-alarm push data
- send periodic alarm ringing push notifications with route and alarm id
- query ringing alarms and handle invalid tokens
- cover repository with ringing notification target tests

- 해결: #57
@strongmhk strongmhk deleted the feature/#53-aop-logging branch August 22, 2025 01:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

모든 Request, Response에 대해 로깅을 적용한다.

1 participant