Conversation
- TYPE을 HTTP, METHOD로 나눠 http request에 대한 로그와 메서드 로그 분리
- title은 errrorCode, subtitle은 message로 표시되도록 설정
|
Caution Review failedThe pull request is closed. WalkthroughAOP 기반 메서드 로깅과 HTTP 요청/응답 로깅이 추가되고 Sentry 통합(빌드·설정·BeforeSend·예외 전송) 및 관련 Logback 설정이 도입되었습니다. 기존 ByteBuddy 테스트 self-attachment 설정은 제거되었습니다. Changes
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
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
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":[]}
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 detailsConfiguration used: .coderabbit.yaml 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (8)
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
Status, Documentation and Community
|
There was a problem hiding this comment.
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.
📒 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.javasrc/main/java/akuma/whiplash/global/log/HttpLoggingFilter.javasrc/main/java/akuma/whiplash/global/log/LogConst.javasrc/main/java/akuma/whiplash/global/log/LogUtils.javasrc/main/java/akuma/whiplash/global/log/MethodLoggingAspect.javasrc/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) 명시도 👍
| sentry { | ||
| includeSourceContext = true | ||
|
|
||
| org = "akuma-ir" | ||
| projectName = "nuntteo_dev" | ||
| authToken = System.getenv("SENTRY_AUTH_TOKEN") | ||
| } |
There was a problem hiding this comment.
🧹 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.
| 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.
| 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); |
There was a problem hiding this comment.
컴파일 오류: 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.
| 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.
| 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; |
There was a problem hiding this comment.
🛠️ 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.
| 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.
| sendErrorToSentry( | ||
| e, | ||
| request.getDescription(false), // ex: "uri=/api/alarms/100/checkin" | ||
| request.getParameterMap().toString(), // 쿼리 스트링 (없으면 빈 Map) | ||
| e.getBody().getTitle() | ||
| ); |
There was a problem hiding this comment.
🛠️ 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.
| 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 |
There was a problem hiding this comment.
🧹 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.
| dsn: ${SENTRY_DSN} | ||
| send-default-pii: true | ||
| logs: | ||
| enabled: true | ||
| traces-sample-rate: 1.0 # 트랜잭션 100% 수집 | ||
| environment: dev |
There was a problem hiding this comment.
💡 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.0→0.1~0.3
- spring.config.activate.on-profile: prod
traces-sample-rate: 1.0→0.1~0.3
- spring.config.activate.on-profile: dev
원한다면 프로파일별 또는 환경 변수 기반 샘플링으로 전환하는 예시 패치도 제안 가능합니다.
🧰 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.
src/main/resources/sentry.yml
Outdated
| send-default-pii: true | ||
| logs: |
There was a problem hiding this comment.
🧹 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.
There was a problem hiding this comment.
@coderabbitai 민감 필드 마스킹/제외 정책을 환경설정과 코드(스코프/BeforeSend)에서 일관되게 적용하는 패치 제안해줘
There was a problem hiding this comment.
🧩 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
- internalToken5. 환경별 설정을 위한 application-prod.yml
# application-prod.yml (새로 생성)
sentry:
masking:
strict-mode: true # 프로덕션에서는 엄격한 마스킹 적용
custom-sensitive-keys:
- customSecretField
- internalToken
- clientId
- deviceId주요 개선 사항
- 환경별 차등 마스킹: 개발/운영 환경에 따라 마스킹 수준 조절
- 일관된 정책 적용: LogUtils와 SentryConfig에서 동일한 마스킹 로직 사용
- 설정 기반 확장성: application.yml을 통한 커스텀 민감 키 추가 가능
- 안전한 실패 처리: 마스킹 실패시에도 이벤트 전송 보장
- 성능 최적화: 개발 환경에서는 최소한의 마스킹으로 디버깅 효율성 확보
이 패치를 적용하면 모든 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: prod2. 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");
}주요 개선점
- 기존 코드 활용: 이미 구현된 LogUtils.maskSensitiveJson 기반으로 확장
- 환경별 차등 적용: 개발 환경은 기본 마스킹, 프로덕션은 강화된 마스킹
- 실용적 접근: 복잡한 설정 없이 코드 레벨에서 환경 감지
- 점진적 적용: 기존 동작은 유지하면서 Sentry만 강화
- 안전한 실패: 마스킹 오류가 발생해도 로깅/모니터링 기능 유지
이 패치는 기존 로깅 동작을 변경하지 않으면서 Sentry로 전송되는 데이터만 선별적으로 마스킹합니다.
- ERROR 레벨 이상의 로그는 Sentry 이벤트(Event) 로 전송 - INFO 레벨 이상의 로그는 Breadcrumb으로 수집, Breadcrumb는 오류가 발생하기 전후에 어떤 로그/동작이 있었는지 “빵부스러기”처럼 추적 정보로 저장하는 것
- 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
📄 PR 요약
✍🏻 PR 상세
👀 참고사항
✅ 체크리스트
🚪 연관된 이슈 번호
Closes #53
Summary by CodeRabbit