[EDMT-453] JMX Prometheus 모니터링 구성 및 학생기록 메트릭스 패키지 구조 개선#71
Conversation
WalkthroughJMX Prometheus 에이전트 통합을 docker-compose(개발/프로덕션)에 추가하고 관련 JMX 설정 파일과 .gitignore 규칙을 추가했습니다. 메트릭 관련 클래스들이 패키지로 이동되었고, 불필요한 컨버터 클래스와 컨트롤러의 서비스 필드가 제거되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Client as 클라이언트
participant App as 앱 (JVM)
participant Agent as JMX Prometheus 에이전트
participant Prom as Prometheus
Client->>App: HTTP 요청 (blue/green)
rect rgba(230,240,255,0.5)
note over App,Agent: 메트릭 노출 추가
App-->>Agent: JMX MBean 데이터 제공
Agent-->>Prom: /metrics 응답 (포트 8081/8082)
end
Prom->>Agent: GET /metrics
Agent-->>Prom: jmx-config.yml에 따른 메트릭 텍스트
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches🧪 Generate unit tests
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
edukit-core/src/main/java/com/edukit/core/studentrecord/metric/RecordGenerationTracker.java (1)
16-17: TTL 프로퍼티 기본값과 유효성 보강 필요
@Value주입 실패(프로퍼티 누락) 시 애플리케이션이 부팅 실패합니다. 운영 안전성을 위해 기본값을 두고, 0/음수일 때 경고 로그를 남겨 동작을 중지하거나 합리적 기본값을 사용하세요.- @Value("${record.generation.ttl}") - private long ttlSeconds; + @Value("${record.generation.ttl:3600}") + private long ttlSeconds;docker-compose.prod.yml (1)
46-46:latest태그 고정은 배포 재현성/안정성을 해칩니다프로덕션에서는 구체 버전(가능하면 다이제스트 포함)으로 핀 고정하세요.
- image: nginx:latest + image: nginx:1.27.2 @@ - image: grafana/promtail:latest + image: grafana/promtail:2.9.0버전은 조직 표준/호환성에 맞춰 조정 바랍니다.
Also applies to: 58-58
edukit-core/src/main/java/com/edukit/core/studentrecord/metric/StudentRecordMetricsAspect.java (2)
25-44: 메트릭 수집 실패가 비즈니스 흐름을 깨뜨릴 수 있음 (예외 전파 위험)
studentRecordService.getRecordDetail호출이try-catch밖에 있어 예외가 상위로 전파됩니다. 메트릭은 베스트‑에포트여야 하므로 전체 블록을 예외로부터 격리하세요.@AfterReturning(pointcut = "@annotation(com.edukit.common.annotation.StudentRecordMetrics)") public void collectCompletionMetrics(final JoinPoint joinPoint) { - Object[] args = joinPoint.getArgs(); - - if (args.length == 3) { - long memberId = (Long) args[0]; - long recordId = (Long) args[1]; - String description = (String) args[2]; - - StudentRecord studentRecord = studentRecordService.getRecordDetail(memberId, recordId); - StudentRecordType recordType = studentRecord.getStudentRecordType(); - - try { - metricsService.recordCompletion(recordType, description); - } catch (Exception e) { - // 메트릭 수집 실패는 로그만 남기고 비즈니스 로직은 계속 진행 - log.warn("Error collecting completion metrics for recordType: {}", recordType, e); - } - } + try { + Object[] args = joinPoint.getArgs(); + if (args.length == 3 + && args[0] instanceof Long + && args[1] instanceof Long + && args[2] instanceof String) { + long memberId = (Long) args[0]; + long recordId = (Long) args[1]; + String description = (String) args[2]; + + StudentRecord studentRecord = studentRecordService.getRecordDetail(memberId, recordId); + StudentRecordType recordType = studentRecord.getStudentRecordType(); + metricsService.recordCompletion(recordType, description); + } + } catch (Exception e) { + log.warn("Error collecting completion metrics", e); + } }
46-80: Around 어드바이스도 동일하게 격리 필요DB 조회 및 Redis 접근 전반을
try-catch로 감싸 예외가 비즈니스 로직에 영향 주지 않게 하세요.@Around("@annotation(com.edukit.common.annotation.AIGenerationMetrics)") public Object collectAIGenerationMetrics(final ProceedingJoinPoint joinPoint) throws Throwable { - Object[] args = joinPoint.getArgs(); - - if (args.length == 4) { - long memberId = (Long) args[0]; - long recordId = (Long) args[1]; - - StudentRecord studentRecord = studentRecordService.getRecordDetail(memberId, recordId); - StudentRecordType recordType = studentRecord.getStudentRecordType(); - - try { - boolean isFirstGeneration = recordGenerationTracker.isFirstGeneration(recordId); - // 전체 AI 생성 요청 카운트 - metricsService.recordAIGenerationRequest(recordType); - // 첫 생성 vs 재생성 구분 메트릭 - if (isFirstGeneration) { - metricsService.recordFirstGeneration(recordType); - log.debug("First generation request for recordId: {}", recordId); - } else { - metricsService.recordRegeneration(recordType); - log.debug("Regeneration request for recordId: {}", recordId); - } - } catch (Exception e) { - // 메트릭 수집 실패는 로그만 남기고 비즈니스 로직은 계속 진행 - log.warn("Error collecting AI generation metrics for recordId: {}", recordId, e); - } - } - - // 비즈니스 로직은 반드시 1회만 실행, 예외는 그대로 전파 - return joinPoint.proceed(); + try { + Object[] args = joinPoint.getArgs(); + if (args.length == 4 && args[0] instanceof Long && args[1] instanceof Long) { + long memberId = (Long) args[0]; + long recordId = (Long) args[1]; + + StudentRecord studentRecord = studentRecordService.getRecordDetail(memberId, recordId); + StudentRecordType recordType = studentRecord.getStudentRecordType(); + + boolean isFirstGeneration = recordGenerationTracker.isFirstGeneration(recordId); + metricsService.recordAIGenerationRequest(recordType); + if (isFirstGeneration) { + metricsService.recordFirstGeneration(recordType); + log.debug("First generation request for recordId: {}", recordId); + } else { + metricsService.recordRegeneration(recordType); + log.debug("Regeneration request for recordId: {}", recordId); + } + } + } catch (Exception e) { + log.warn("Error collecting AI generation metrics", e); + } + return joinPoint.proceed(); }
🧹 Nitpick comments (6)
edukit-core/src/main/java/com/edukit/core/studentrecord/metric/StudentRecordMetricsCounter.java (1)
46-53: 완료 기준(minBytes) 하드코딩 제거 및 설정화 제안타입별 최소 바이트 임계치가 하드코딩되어 있어 릴리스 없이 조정이 어렵습니다. 설정값으로 추출하세요.
- private boolean isCompleted(final StudentRecordType type, final String description) { + private boolean isCompleted(final StudentRecordType type, final String description) { if (description == null || description.trim().isEmpty()) { return false; } - - int minBytes = (type == StudentRecordType.SUBJECT) ? 1000 : 750; + int minBytes = (type == StudentRecordType.SUBJECT) ? minSubjectBytes : minDefaultBytes; return description.getBytes(StandardCharsets.UTF_8).length >= minBytes; }추가로 클래스 필드에 다음을 선언해 주세요:
@org.springframework.beans.factory.annotation.Value("${studentrecord.metrics.min_bytes.subject:1000}") private int minSubjectBytes; @org.springframework.beans.factory.annotation.Value("${studentrecord.metrics.min_bytes.default:750}") private int minDefaultBytes;docker-compose.dev.yml (1)
18-19: JAR/설정 파일 마운트는 읽기 전용으로에이전트 JAR와 설정 파일을
:ro로 마운트해 우발적 변경을 방지하세요.- - /opt/jmx_prometheus_javaagent.jar:/app/jmx_prometheus_javaagent.jar - - ./jmx-config.yml:/app/jmx-config.yml + - /opt/jmx_prometheus_javaagent.jar:/app/jmx_prometheus_javaagent.jar:ro + - ./jmx-config.yml:/app/jmx-config.yml:roAlso applies to: 39-40
docker-compose.prod.yml (1)
18-19: 읽기 전용 마운트로 불변성 강화운영 구성/바이너리 파일은
:ro로 마운트하세요.- - /opt/jmx_prometheus_javaagent.jar:/app/jmx_prometheus_javaagent.jar - - ./jmx-config.yml:/app/jmx-config.yml + - /opt/jmx_prometheus_javaagent.jar:/app/jmx_prometheus_javaagent.jar:ro + - ./jmx-config.yml:/app/jmx-config.yml:roAlso applies to: 39-40
edukit-core/src/main/java/com/edukit/core/studentrecord/metric/StudentRecordMetricsAspect.java (1)
29-33: 인자 포지션/타입 가정 완화 제안포인트컷 대상 메서드 시그니처가 바뀌면 조용히 스킵되거나
ClassCastException이 발생할 수 있습니다. 어노테이션에 파라미터 메타데이터를 추가하거나,@AfterReturning(pointcut=..., returning="ret")로 반환 객체에서 타입을 얻는 방식을 검토하세요.해당 어노테이션이 붙은 모든 메서드 시그니처가 (memberId: Long, recordId: Long, ...) 순서를 보장하는지 확인 부탁드립니다.
Also applies to: 50-56
jmx-config.yml (2)
11-17: Tomcat Connector 패턴도 하이픈 미매치Connector 이름에도 하이픈이 포함됩니다. 패턴을 수정하세요.
- - pattern: 'Catalina<type=GlobalRequestProcessor, name="(\w+)"><>(requestCount|bytesReceived|bytesSent): (\d+)' + - pattern: 'Catalina<type=GlobalRequestProcessor, name="([^"]+)"><>(requestCount|bytesReceived|bytesSent): (\d+)'
64-64: 파일 말미 개행 누락YAML 린트 에러를 제거하려면 마지막 줄에 개행을 추가하세요.
- type: GAUGE \ No newline at end of file + type: GAUGE +
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
.gitignore(1 hunks)docker-compose.dev.yml(2 hunks)docker-compose.prod.yml(2 hunks)edukit-api/src/main/java/com/edukit/studentrecord/controller/StudentRecordAIController.java(0 hunks)edukit-core/src/main/java/com/edukit/core/common/converter/StudentRecordTypeConverter.java(0 hunks)edukit-core/src/main/java/com/edukit/core/studentrecord/metric/RecordGenerationTracker.java(1 hunks)edukit-core/src/main/java/com/edukit/core/studentrecord/metric/StudentRecordMetricsAspect.java(1 hunks)edukit-core/src/main/java/com/edukit/core/studentrecord/metric/StudentRecordMetricsCounter.java(1 hunks)jmx-config.yml(1 hunks)
💤 Files with no reviewable changes (2)
- edukit-core/src/main/java/com/edukit/core/common/converter/StudentRecordTypeConverter.java
- edukit-api/src/main/java/com/edukit/studentrecord/controller/StudentRecordAIController.java
🧰 Additional context used
🪛 YAMLlint (1.37.1)
jmx-config.yml
[error] 64-64: no new line character at the end of file
(new-line-at-end-of-file)
🔇 Additional comments (4)
edukit-core/src/main/java/com/edukit/core/studentrecord/metric/RecordGenerationTracker.java (1)
21-27: Lua 스크립트로 INCR+EXPIRE 원자화 잘 처리됨초기 증가에만 TTL을 설정해 윈도우 내 재생성을 구분하는 의도에 맞습니다. 이 부분은 그대로 가도 좋습니다.
운영에 적용 전,
record.generation.ttl을 0으로 설정했을 때 키가 만료되지 않는 것이 의도인지 확인 부탁드립니다.Also applies to: 31-36
.gitignore (1)
167-168: 에이전트 JAR ignore 추가 LGTM에이전트 바이너리 추적 방지에 적절합니다.
호스트 경로가
/opt/jmx_prometheus_javaagent.jar외 다른 위치를 쓰는 서버가 있는지 점검해 주세요(있다면 해당 경로 패턴도 함께 ignore 필요).jmx-config.yml (1)
29-36: GC 메트릭 네이밍과 타입 적절 — LGTM컬렉션 횟수/시간을 COUNTER로 분리하고 GC 이름을 라벨로 둔 점 좋습니다.
Also applies to: 37-43
docker-compose.dev.yml (1)
9-9: JAVA_OPTS 전달 여부 수동 확인 필요파일: docker-compose.dev.yml — 줄 9, 30에 설정한 JAVA_OPTS가 베이스 이미지의 ENTRYPOINT/시작 스크립트에서 실제로 JVM으로 전달되는지 수동으로 확인하세요. 자동 검사에서 관련 파일을 찾지 못했습니다.
- 확인 방법: Dockerfile 및 ENTRYPOINT(또는 시작 스크립트)를 열어 'JAVA_OPTS' 참조 여부 또는 JVM 실행 명령(java ...)에 해당 옵션이 포함되는지 확인. 베이스 이미지가 'JAVA_TOOL_OPTIONS'만 인식하면 JAVA_TOOL_OPTIONS로 옮기거나 entrypoint에서 JAVA_OPTS를 JVM 커맨드에 포함시키도록 수정하세요.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (6)
.claude/CLAUDE.md (6)
13-20: 코드 블록에 언어 지정 누락(MD040) — 간단히text지정 권장마크다운 린트 경고를 해소하려면 언어를 지정하세요.
-``` +```text edukit-api (REST API Layer) ├── edukit-core (Business Logic) │ └── edukit-common (Shared Utilities) └── edukit-external (External Integrations) ├── edukit-core └── edukit-common -``` +```
115-123: 패키지 구조 코드 블록도 언어 지정 필요(MD040)디렉터리 레이아웃 예시는
text가 적절합니다.-``` +```text com.edukit.{domain} ├── controller/ # REST endpoints ├── service/ # Business logic ├── repository/ # Data access ├── entity/ # JPA entities ├── dto/ # Data transfer objects └── exception/ # Domain-specific exceptions -``` +```
60-62: 멀티모듈에서 bootRun 대상 명시루트에서
bootRun은 오작동할 수 있습니다. API 모듈을 명시하세요.-# Run application locally -./gradlew bootRun +# Run application locally (API module) +./gradlew :edukit-api:bootRun
93-103: JMX Java Agent 사용 예시 추가 제안운영 가이드 강화를 위해
-javaagent예시와 로컬 바인딩 권고를 바로 여기에 넣어두면 좋습니다.- Environment variables managed via `.env` files - JMX monitoring configured in `jmx-config.yml` + +#### JMX Prometheus Java Agent 예시 (로컬 바인딩 권장) +```bash +# bind to loopback to avoid public exposure +-javaagent:/opt/jmx_prometheus_javaagent.jar=127.0.0.1:8081:/opt/jmx/jmx-config.yml +``` +외부 노출이 필요한 경우 반드시 방화벽/보안그룹/네트워크 정책으로 접근을 제한하세요.
134-139: 민감 데이터/프롬프트 로그 처리 지침 보강AI 연동 특성상 프롬프트/응답과 학생 식별자가 로그·메트릭으로 유출되지 않도록 명시해두는 것이 안전합니다.
### 🔐 Security Considerations - JWT tokens for authentication - AWS credentials managed via environment variables - Database credentials in profile-specific configs - Never commit `.env` files or sensitive data + - 프롬프트/응답 본문은 로그로 남기지 않기(샘플링·마스킹 적용, PII 제거) + - 메트릭/로그에는 학생 ID·이메일 등 고유 식별자 사용 금지(대신 해시/익명화 키 사용)
153-157: 메트릭 레이블 카디널리티/수집 주기 가이드 추가운영 관점에서 레이블 폭증과 scrape 주기 과도 설정은 비용·성능 이슈로 직결됩니다. 간단한 원칙을 문서에 추가하세요.
### 🔄 Metrics Collection - Custom metrics using `@Aspect` and Micrometer - Student record generation tracking - Performance monitoring for AI operations + - 레이블 카디널리티 관리: 사용자/요청 ID 등 고카디널리티 필드 금지, bounded set만 허용 + - 수집 주기: 기본 15s 권장(고비용 메트릭은 30–60s), 테스트 환경은 더 길게
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
.claude/CLAUDE.md(1 hunks)jmx-config.yml(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- jmx-config.yml
🧰 Additional context used
🪛 markdownlint-cli2 (0.18.1)
.claude/CLAUDE.md
13-13: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
115-115: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
📣 Jira Ticket
EDMT-453
👩💻 작업 내용
🔍 JMX Prometheus 모니터링 구성
📊 메트릭스 수집 범위
🔧 코드 구조 개선
📝 리뷰 요청 & 논의하고 싶은 내용
Test Plan
✅ Manual Testing Checklist
🧪 Automated Tests
./gradlew test./gradlew build🔍 Code Quality
Deployment Notes
🤖 Generated with Claude Code
Summary by CodeRabbit