feat(observability): 리포트 다운로드·SSE 스트림·JWT 인증 구현 #29 - #72
Hidden character warning
Conversation
GET /api/monitor/v11/reports 구현. 리포트 내용은 대시보드 스냅샷을 재사용해 POI(xlsx)/CSV로 생성, 로컬 디스크에 저장하고 Redis 만료 토큰(5분)으로 S3 presigned URL을 대체한다. 실제 다운로드는 /api/monitor/v11/reports/download?token=으로 서빙. 동기 생성만 지원(202 GENERATING/폴링 큐 없음) — 데이터량이 적어 실익이 없다고 판단, 느려지면 잡 큐로 교체. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GET /api/monitor/v11/stream 구현. SseEmitter 기반 브로드캐스터로 snapshot(연결 시 1회) + traffic/api/business/service(5초) + resource(5분) + ping(15초)을 내보내고, 클라이언트 로그 수집 시 log 이벤트를 즉시 push한다. 계정 인증이 아직 없어 IP 기준으로 동시 커넥션 3개를 넘으면 TOO_MANY_CONNECTIONS. ponytail: 커넥션 목록이 인스턴스 인메모리라 다중 인스턴스에서는 브로드캐스트가 인스턴스별로 갈린다 — 수평 확장 시 Redis Pub/Sub로 교체. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
전체 관제 API에 Bearer JWT 검증(HandlerInterceptor)을 적용한다. 토큰 없음/서명 불일치/만료는 UNAUTHORIZED(401), role 클레임이 ADMIN이 아니면 FORBIDDEN(403). collect/session, collect/client-log 두 공개 엔드포인트만 제외. 이 저장소엔 아직 identity의 정식 Spring Security/JWT 인증이 병합되어 있지 않아(발급된 토큰에 역할 부여조차 없는 상태) 전체 Security 스택 대신 자체 경량 인터셉터로 구현. auth.jwt.secret/issuer 설정 키 이름은 identity와 통일해 나중에 시크릿만 맞추면 되게 했다. 이것으로 monitoring-observability.md의 10개 API 구현이 모두 끝났다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
📝 Walkthrough대시보드 스냅샷을 XLSX·CSV 리포트로 다운로드하고, 실시간 지표와 클라이언트 로그를 SSE로 구독할 수 있습니다. 관제 API에는 주요 변경아키텍처
위험 영역
마이그레이션 및 호환성
검증 및 롤아웃
Walkthrough관측성 모듈에 CSV·XLSX 리포트 생성 및 다운로드 API, SSE 기반 실시간 로그 스트림, 관리자 JWT 인증을 추가했습니다. Redis 기반 파일 토큰 저장과 Spring Scheduling 설정도 추가했습니다. Changes관측성 기능 확장
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)리포트 생성 및 다운로드sequenceDiagram
participant Client
participant ReportController
participant ReportService
participant XlsxCsvReportGenerator
participant LocalFileReportObjectStorageAdapter
Client->>ReportController: 리포트 생성 요청
ReportController->>ReportService: 형식 전달
ReportService->>XlsxCsvReportGenerator: 스냅샷 변환
ReportService->>LocalFileReportObjectStorageAdapter: 파일 저장
LocalFileReportObjectStorageAdapter-->>ReportService: URL와 만료 시각 반환
ReportService-->>Client: 생성 결과 반환
SSE 실시간 로그sequenceDiagram
participant Client
participant MonitorStreamController
participant SseBroadcaster
participant ClientLogCollectionService
participant SseLiveLogPublisher
Client->>MonitorStreamController: SSE 연결 요청
MonitorStreamController->>SseBroadcaster: emitter 등록
ClientLogCollectionService->>SseLiveLogPublisher: 클라이언트 로그 전달
SseLiveLogPublisher->>SseBroadcaster: 로그 이벤트 발행
SseBroadcaster-->>Client: SSE 이벤트 전송
Suggested labels: 🚥 Pre-merge checks | ✅ 6 | ❌ 4❌ Failed checks (4 warnings)
✅ Passed checks (6 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 12
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/MonitorStreamController.kt`:
- Around line 26-28: MonitorStreamController의 ResponseBodyEmitter 종료 처리에서
connectionLimiter.release(clientKey)가 한 번만 실행되도록 수정하십시오. onCompletion만 해제 경로로
사용하거나 AtomicBoolean으로 onCompletion, onTimeout, onError의 중복 호출을 방지하고, timeout 및
error 종료 시 카운터가 정확히 한 번 감소하는 테스트를 추가하십시오.
- Around line 33-38: Update MonitorStreamController.clientIp to use
X-Forwarded-For only for requests from a trusted proxy; otherwise use
request.remoteAddr, ensuring all limiter keys use the normalized trusted
address. In SseConnectionLimiter.kt lines 13-24, make acquire and release use
atomic ConcurrentHashMap key operations, remove entries when the final
connection is released, and preserve the excess-connection rejection behavior.
Add tests covering connection-limit rejection, removal after release, and
concurrent acquire/release.
In
`@systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/ReportController.kt`:
- Around line 18-25: The MVC test suite requires coverage for both changed web
flows. In ReportController.kt lines 18-25, add tests for the default XLSX
request, CSV format, and invalid format error response; in
ReportDownloadController.kt lines 16-22, add tests for a valid token returning
200 with attachment headers and for expired or missing tokens returning 404.
Verify request parsing, HTTP status, headers, and response bodies using the
subsystem’s existing MVC test conventions.
- Around line 18-25: Update the generate method mapping in ReportController from
GET to POST while preserving its format parsing and report-generation logic.
In
`@systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthProperties.kt`:
- Around line 13-14: Remove the development fallback for JWT_SECRET in the
application configuration and make auth.jwt.secret mandatory during startup.
Update JwtAuthProperties so missing or blank secret values fail validation
during configuration binding, and add binding tests covering both absent and
whitespace-only values; keep issuer handling unchanged.
In
`@systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseBroadcaster.kt`:
- Around line 66-69: Update SseBroadcaster.broadcast and the emitter lifecycle
so scheduled or API threads only enqueue events, while each emitter drains
through a dedicated bounded executor and per-connection bounded outbound queue.
Preserve event order per connection, remove and complete emitters whose queue is
full, and ensure send failures also clean up the connection; add tests covering
ordered delivery and queue-saturation termination.
In
`@systems/observability/observability-adapter-in/src/test/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthInterceptorTest.kt`:
- Around line 24-68: Extend JwtAuthInterceptorTest around token and the
rejection tests to create a correctly signed token with an incorrect issuer, and
verify that preHandle rejects it. Strengthen each rejection assertion by
checking the resulting MonitorDomainException ErrorCode or HTTP translation,
explicitly distinguishing UNAUTHORIZED from FORBIDDEN for missing, invalid,
expired, wrong-signature, and non-admin tokens.
In
`@systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/XlsxCsvReportGenerator.kt`:
- Around line 13-64:
systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/XlsxCsvReportGenerator.kt:13-64
— Add direct tests for XlsxCsvReportGenerator.generate, covering CSV escaping of
commas, quotes, and newlines and verifying generated XLSX row and cell values.
systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/LocalFileReportObjectStorageAdapter.kt:26-43
— Add direct tests for LocalFileReportObjectStorageAdapter covering save,
resolve, expiration handling, and unique object paths.
- Around line 41-45: Update toCsv and its row serialization so every CSV field,
including the header values and each key/value from rows(snapshot), is enclosed
in double quotes and any embedded double quotes are doubled, preserving commas
and newlines according to RFC 4180.
In
`@systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionService.kt`:
- Around line 41-42: Update ClientLogCollectionService.kt:41-42 to preserve
saving first, then publish exactly one normalized ClientLogInput. In
ClientLogCollectionServiceTest.kt:33-33, replace the no-op LiveLogPublisherPort
with a recording fake and assert the published value equals the stored value; at
lines 45-45 and 55-55, assert publisher calls remain zero for empty/oversized
batches and rate-limit rejection.
In
`@systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ReportService.kt`:
- Around line 30-32: ReportService.kt:30-32의 ReportService에서 표시용 다운로드 파일명과 저장 객체
식별자를 분리하고, 토큰마다 고유한 저장 경로를 생성해 Redis에 저장 경로와 원래 파일명을 함께 기록하세요.
LocalFileReportObjectStorageAdapter.kt:26-31의
LocalFileReportObjectStorageAdapter는 전달받은 토큰별 고유 경로에 저장하도록 수정해 기존 토큰이 새 리포트 객체를
참조하지 않게 하세요.
- Around line 14-18: Remove the Spring `@Service` dependency from ReportService
and register it through a `@Bean` configuration in observability-bootstrap
instead. Remove the direct Spring Boot starter dependency from
observability-application, and apply the same migration to any other
application-layer services using Spring annotations.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 88a152a5-dbea-4399-a461-87012a200f93
📒 Files selected for processing (30)
systems/observability/observability-adapter-in/deps.bzlsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/MonitorStreamController.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/ReportController.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/ReportDownloadController.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/common/ResponseMapper.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/response/LiveLogEventResponse.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/response/ReportGeneratedResponse.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthInterceptor.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthProperties.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/WebMvcConfig.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseBroadcaster.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseConnectionLimiter.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseLiveLogPublisher.ktsystems/observability/observability-adapter-in/src/test/kotlin/hs/kr/entrydsm/TestMain.ktsystems/observability/observability-adapter-in/src/test/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthInterceptorTest.ktsystems/observability/observability-adapter-out/deps.bzlsystems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/LocalFileReportObjectStorageAdapter.ktsystems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/XlsxCsvReportGenerator.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionService.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ReportService.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/GenerateReportUseCase.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/GetDashboardSnapshotUseCase.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/result/ReportResult.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/LiveLogPublisherPort.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/ReportGeneratorPort.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/ReportObjectStoragePort.ktsystems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/TestMain.ktsystems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionServiceTest.ktsystems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/ReportServiceTest.ktsystems/observability/observability-bootstrap/src/main/kotlin/hs/kr/entrydsm/ExampleApplication.kt
📜 Review details
🧰 Additional context used
📓 Path-based instructions (6)
**/{BUILD.bazel,*.bzl}
📄 CodeRabbit inference engine (Custom checks)
In BUILD.bazel and .bzl files, require buildifier-compatible formatting and stable target naming
Files:
systems/observability/observability-adapter-out/deps.bzlsystems/observability/observability-adapter-in/deps.bzl
**/*.bzl
⚙️ CodeRabbit configuration file
**/*.bzl: Apply Bazel Starlark (.bzl) style guidance.Readability and docs:
- Keep file/module docstrings and docstrings for public functions/macros.
- Use descriptive parameter names and document attribute intent.
API design:
- Macros should take a
nameargument and derive generated target names from it.- Prefer keyword arguments when calling macros for clarity and stability.
- Keep macro side effects predictable and visible.
Encapsulation:
- Use private visibility for helper targets created by macros unless explicitly public.
- Avoid exposing internal implementation targets unintentionally.
Tooling:
- Enforce buildifier formatting and lint compliance.
Files:
systems/observability/observability-adapter-out/deps.bzlsystems/observability/observability-adapter-in/deps.bzl
**/*.{kt,go}
📄 CodeRabbit inference engine (Custom checks)
If production logic is changed in Kotlin or Go files, require corresponding test updates in the same subsystem unless the PR description explicitly justifies why tests are unnecessary
Files:
systems/observability/observability-bootstrap/src/main/kotlin/hs/kr/entrydsm/ExampleApplication.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/GenerateReportUseCase.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthProperties.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/ReportGeneratorPort.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/response/ReportGeneratedResponse.ktsystems/observability/observability-adapter-in/src/test/kotlin/hs/kr/entrydsm/TestMain.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/response/LiveLogEventResponse.ktsystems/observability/observability-adapter-in/src/test/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthInterceptorTest.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/WebMvcConfig.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/result/ReportResult.ktsystems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/ReportServiceTest.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/ReportDownloadController.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/ReportObjectStoragePort.ktsystems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/XlsxCsvReportGenerator.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/MonitorStreamController.ktsystems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/TestMain.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseLiveLogPublisher.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseConnectionLimiter.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/GetDashboardSnapshotUseCase.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/LiveLogPublisherPort.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseBroadcaster.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/ReportController.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionService.ktsystems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionServiceTest.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ReportService.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthInterceptor.ktsystems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/LocalFileReportObjectStorageAdapter.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/common/ResponseMapper.kt
**/*.{java,kt,scala,groovy,go,js,ts,tsx,jsx,py,rb,rs,cpp,c,h,hpp,cs}
📄 CodeRabbit inference engine (Custom checks)
Flag TODO/FIXME comments introduced by this PR that do not include an issue reference in the form
#123or a full tracker key like PROJ-123
Files:
systems/observability/observability-bootstrap/src/main/kotlin/hs/kr/entrydsm/ExampleApplication.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/GenerateReportUseCase.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthProperties.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/ReportGeneratorPort.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/response/ReportGeneratedResponse.ktsystems/observability/observability-adapter-in/src/test/kotlin/hs/kr/entrydsm/TestMain.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/response/LiveLogEventResponse.ktsystems/observability/observability-adapter-in/src/test/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthInterceptorTest.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/WebMvcConfig.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/result/ReportResult.ktsystems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/ReportServiceTest.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/ReportDownloadController.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/ReportObjectStoragePort.ktsystems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/XlsxCsvReportGenerator.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/MonitorStreamController.ktsystems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/TestMain.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseLiveLogPublisher.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseConnectionLimiter.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/GetDashboardSnapshotUseCase.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/LiveLogPublisherPort.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseBroadcaster.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/ReportController.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionService.ktsystems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionServiceTest.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ReportService.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthInterceptor.ktsystems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/LocalFileReportObjectStorageAdapter.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/common/ResponseMapper.kt
**/*.kt
⚙️ CodeRabbit configuration file
**/*.kt: Apply Kotlin Official Coding Conventions.Formatting and structure:
- Use 4 spaces for indentation; no tabs.
- Keep files focused and readable; avoid horizontal alignment for spacing.
- Place related declarations together and keep overloads adjacent.
- Keep implementation member order stable and logical for readability.
Naming:
- Package names are lowercase and do not use underscores.
- Class/object names use UpperCamelCase.
- Functions/properties/local variables use lowerCamelCase.
- Constants use UPPER_SNAKE_CASE only for true constants.
API and null-safety:
- Avoid platform type leakage in public APIs.
- Use explicit types in public APIs when inference obscures meaning.
- Prefer immutable values (
val) over mutable values (var) unless mutation is required.- Flag nullable flows that can be replaced with safer modeling.
Imports and idioms:
- Avoid wildcard imports unless justified by language/tooling conventions.
- Prefer expression bodies for short, clear functions.
- Prefer standard library idioms over custom utility wrappers when equivalent.
Architecture and tests:
- Respect module boundaries (domain/application/adapter/bootstrap layering).
- Highlight behavior-changing code that lacks corresponding unit/integration tests.
- Ask for deterministic tests and meaningful assertions, not only happy-path checks.
Files:
systems/observability/observability-bootstrap/src/main/kotlin/hs/kr/entrydsm/ExampleApplication.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/GenerateReportUseCase.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthProperties.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/ReportGeneratorPort.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/response/ReportGeneratedResponse.ktsystems/observability/observability-adapter-in/src/test/kotlin/hs/kr/entrydsm/TestMain.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/response/LiveLogEventResponse.ktsystems/observability/observability-adapter-in/src/test/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthInterceptorTest.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/WebMvcConfig.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/result/ReportResult.ktsystems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/ReportServiceTest.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/ReportDownloadController.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/ReportObjectStoragePort.ktsystems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/XlsxCsvReportGenerator.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/MonitorStreamController.ktsystems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/TestMain.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseLiveLogPublisher.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseConnectionLimiter.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/GetDashboardSnapshotUseCase.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/LiveLogPublisherPort.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseBroadcaster.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/ReportController.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionService.ktsystems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionServiceTest.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ReportService.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthInterceptor.ktsystems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/LocalFileReportObjectStorageAdapter.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/common/ResponseMapper.kt
**/*-application/**/*.{java,kt,scala,groovy}
📄 CodeRabbit inference engine (Custom checks)
For files under *-application modules, flag direct dependency on infrastructure-specific framework classes unless justified
Files:
systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/GenerateReportUseCase.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/ReportGeneratorPort.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/result/ReportResult.ktsystems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/ReportServiceTest.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/ReportObjectStoragePort.ktsystems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/TestMain.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/GetDashboardSnapshotUseCase.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/LiveLogPublisherPort.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionService.ktsystems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionServiceTest.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ReportService.kt
🪛 ast-grep (0.45.1)
systems/observability/observability-adapter-in/src/test/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthInterceptorTest.kt
[warning] 14-14: A credential is hard-coded by assigning a string literal to a password/secret/API-key variable. Secrets stored in source code can be leaked and abused by internal or external malicious actors. Remove the literal, rotate the exposed secret, and load it at runtime from an environment variable, a secure secret vault, or a Hardware Security Module (HSM).
Context: private val secret = "test-secret-key-at-least-32-bytes-long!!"
Note: [CWE-798]: Use of Hard-coded Credentials [OWASP A07:2021]: Identification and Authentication Failures
(hardcoded-password-string-literal-kotlin)
🪛 detekt (1.23.8)
systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthProperties.kt
[warning] 13-13: Usages of lateinit should be avoided.
(detekt.potential-bugs.LateinitUsage)
[warning] 14-14: Usages of lateinit should be avoided.
(detekt.potential-bugs.LateinitUsage)
systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthInterceptor.kt
[warning] 34-34: The caught exception is swallowed. The original exception could be lost.
(detekt.exceptions.SwallowedException)
[warning] 36-36: The caught exception is swallowed. The original exception could be lost.
(detekt.exceptions.SwallowedException)
🔇 Additional comments (12)
systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthInterceptor.kt (1)
19-55: LGTM!systems/observability/observability-adapter-in/src/test/kotlin/hs/kr/entrydsm/TestMain.kt (1)
4-11: LGTM!systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/WebMvcConfig.kt (1)
12-17: 🔒 Security & Privacy인터셉터 경로 범위는 컨트롤러 매핑과 일치합니다.
모든 관제 API가
"/api/monitor/v11/**"에 포함되며, 두 수집 엔드포인트도 정확히 제외됩니다. 경로 우회 문제는 확인되지 않았습니다.> Likely an incorrect or invalid review comment.systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/GenerateReportUseCase.kt (1)
1-8: LGTM!systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/result/ReportResult.kt (1)
1-10: LGTM!systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/common/ResponseMapper.kt (1)
23-23: LGTM!Also applies to: 52-52, 199-207
systems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/ReportServiceTest.kt (1)
24-56: LGTM!systems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/TestMain.kt (1)
16-16: LGTM!systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/ReportGeneratorPort.kt (1)
1-8: LGTM!systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/ReportObjectStoragePort.kt (1)
1-13: LGTM!systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/response/ReportGeneratedResponse.kt (1)
1-11: LGTM!systems/observability/observability-adapter-out/deps.bzl (1)
4-5: LGTM!
| emitter.onCompletion { connectionLimiter.release(clientKey) } | ||
| emitter.onTimeout { connectionLimiter.release(clientKey) } | ||
| emitter.onError { connectionLimiter.release(clientKey) } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
file='systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/MonitorStreamController.kt'
printf '%s\n' '--- controller outline ---'
ast-grep outline "$file" || true
printf '%s\n' '--- controller ---'
cat -n "$file"
printf '%s\n' '--- limiter definitions and usages ---'
rg -n -C 4 'connectionLimiter|class .*Limiter|interface .*Limiter|fun release|fun acquire' systems/observability
printf '%s\n' '--- related tests ---'
rg -n -C 3 'MonitorStreamController|connectionLimiter|SseEmitter|onCompletion|onTimeout|onError' systems/observability --glob '*Test*' --glob '*.kt' || trueRepository: EntryDSM/entrydsm-platform
Length of output: 21189
🏁 Script executed:
#!/bin/bash
set -eu
limiter='systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseConnectionLimiter.kt'
printf '%s\n' '--- limiter ---'
cat -n "$limiter"
printf '%s\n' '--- module files and test layout ---'
git ls-files 'systems/observability/observability-adapter-in' | sed -n '1,240p'
printf '%s\n' '--- all limiter references ---'
rg -n -C 5 'SseConnectionLimiter|tryAcquire\(|release\(' systems/observability --glob '*.kt'
printf '%s\n' '--- Spring callback documentation/source references in repository ---'
rg -n -i -C 3 'onCompletion|onTimeout|onError|ResponseBodyEmitter' . --glob '*.md' --glob '*.kt' --glob '*.java' || trueRepository: EntryDSM/entrydsm-platform
Length of output: 30115
🌐 Web query:
Spring Framework ResponseBodyEmitter SseEmitter onCompletion onTimeout onError callback lifecycle timeout error completion
💡 Result:
In the Spring Framework, ResponseBodyEmitter and its subclass SseEmitter provide mechanisms for asynchronous request processing and streaming, where lifecycle events are managed via specific callback methods [1][2][3]. Lifecycle Callbacks The following methods allow applications to register hooks for specific lifecycle events: - onCompletion(Runnable callback): Registers a callback invoked when the async request completes for any reason, including successful completion, timeout, or network error [1][3]. This is primarily used to detect that the emitter instance is no longer usable [1]. - onTimeout(Runnable callback): Registers a callback invoked when the async request times out [1][3]. - onError(Consumer callback): Registers a callback invoked when an error occurs during async request processing [1][3]. These callbacks are executed from a container thread [1][3]. Timeout and Completion Behavior - Request Timeout: You can specify a custom timeout value (in milliseconds) via the constructor when creating a ResponseBodyEmitter or SseEmitter [1][2]. If not set, the framework uses the default configured in the MVC Java Config or the underlying servlet container [2][4]. - Completion: Applications can call complete or completeWithError(Throwable) to signal the end of request processing [1][5]. - Important Implementation Notes: - When an IOException occurs (e.g., the client disconnects), the application should not manually call complete or completeWithError. Instead, the servlet container initiates an AsyncListener notification, and Spring MVC automatically handles the cleanup [6]. - Recent Spring Framework versions (e.g., 6.2.x) have seen internal adjustments regarding when these flags are set and when callbacks are triggered to address issues with state management and callback invocation consistency [7][8][9]. Users should be aware that attempting to trigger completion within a callback (like onTimeout) may behave differently depending on the specific patch version of the framework [9]. For robust streaming, it is recommended to send periodic heartbeats (e.g., empty comments) because the Servlet API does not provide immediate notification when a remote client disappears, and writes will fail if the client has disconnected [6].
Citations:
- 1: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitter.html
- 2: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/mvc/method/annotation/SseEmitter.html
- 3: https://docs.spring.io/spring-framework/docs/6.2.11-SNAPSHOT/javadoc-api/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitter.html
- 4: https://docs.spring.io/spring-framework/docs/6.1.14/javadoc-api/org/springframework/web/servlet/mvc/method/annotation/SseEmitter.html
- 5: https://www.baeldung.com/spring-mvc-sse-streams
- 6: https://rwinch.github.io/spring-framework/web/webmvc/mvc-ann-async.html
- 7: Inconsistency in
SseEmitter.onCompletion()behavior between Spring 6.2.3 and 6.2.5 spring-projects/spring-framework#34762 - 8: spring-projects/spring-framework@b49924b
- 9: 503 status code after completing SseEmitter in onTimeout spring-projects/spring-framework#34426
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
MAX_CONNECTIONS_PER_KEY = 3
def try_acquire(count):
count += 1
if count <= MAX_CONNECTIONS_PER_KEY:
return count, True
return count - 1, False
def release(count):
return count - 1
# Model one successful stream whose timeout/error callback is followed by
# ResponseBodyEmitter's completion callback.
count = 0
count, acquired = try_acquire(count)
assert acquired and count == 1
count = release(count) # onTimeout or onError
count = release(count) # onCompletion
assert count == -1
accepted = 0
for _ in range(4):
count, acquired = try_acquire(count)
accepted += acquired
print(f"count_after_double_release={-1}")
print(f"additional_acquisitions_accepted={accepted}")
assert accepted == 4
print("PASS: a non-idempotent release permits four additional acquisitions after one double callback.")
PYRepository: EntryDSM/entrydsm-platform
Length of output: 324
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
MAX_CONNECTIONS_PER_KEY = 3
def try_acquire(count):
count += 1
if count <= MAX_CONNECTIONS_PER_KEY:
return count, True
return count - 1, False
def release(count):
return count - 1
count = 0
count, acquired = try_acquire(count)
assert acquired and count == 1
count = release(count)
count = release(count)
assert count == -1
accepted = 0
for _ in range(4):
count, acquired = try_acquire(count)
accepted += acquired
print(f"count_after_double_release={-1}")
print(f"additional_acquisitions_accepted={accepted}")
assert accepted == 4
print("PASS: a non-idempotent release permits four additional acquisitions after one double callback.")
PYRepository: EntryDSM/entrydsm-platform
Length of output: 324
연결 슬롯을 한 번만 해제하십시오.
ResponseBodyEmitter는 timeout 또는 error 뒤에도 onCompletion을 호출할 수 있습니다. 현재 release가 중복 실행되어 카운터가 음수가 되고, IP별 연결 제한 3개를 초과할 수 있습니다. onCompletion에서만 해제하거나 AtomicBoolean으로 중복 실행을 방지하십시오. timeout과 error 종료 시 카운터가 정확히 한 번 감소하는 테스트도 추가하십시오.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/MonitorStreamController.kt`
around lines 26 - 28, MonitorStreamController의 ResponseBodyEmitter 종료 처리에서
connectionLimiter.release(clientKey)가 한 번만 실행되도록 수정하십시오. onCompletion만 해제 경로로
사용하거나 AtomicBoolean으로 onCompletion, onTimeout, onError의 중복 호출을 방지하고, timeout 및
error 종료 시 카운터가 정확히 한 번 감소하는 테스트를 추가하십시오.
| private fun clientIp(request: HttpServletRequest): String = | ||
| request.getHeader("X-Forwarded-For") | ||
| ?.substringBefore(",") | ||
| ?.trim() | ||
| ?.takeIf { it.isNotBlank() } | ||
| ?: request.remoteAddr |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# X-Forwarded-For를 제거·재설정하는 신뢰된 프록시 설정과 Spring forwarded-header 설정을 확인한다.
fd -HI -t f . | rg -i '(ingress|gateway|nginx|traefik|apache|caddy|values.*ya?ml|.*\.conf$)' |
while IFS= read -r file; do
rg -n -C 3 'X-Forwarded-For|ForwardedHeaderFilter|forward-headers|remoteIpHeader|proxy_set_header' "$file" || true
doneRepository: EntryDSM/entrydsm-platform
Length of output: 164
🏁 Script executed:
#!/bin/bash
set -euo pipefail
files=(
systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/MonitorStreamController.kt
systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseConnectionLimiter.kt
)
for file in "${files[@]}"; do
echo "===== $file ====="
wc -l "$file"
cat -n "$file"
done
echo "===== limiter usages ====="
rg -n -C 4 'SseConnectionLimiter|tryAcquire|release|clientIp|X-Forwarded-For|ForwardedHeaderFilter|server\.forward-headers|forward-headers-strategy|remoteAddr' . \
-g '!build' -g '!dist' -g '!node_modules' || true
echo "===== proxy and application configuration files ====="
git ls-files | rg -i '(^|/)(application[^/]*\.(yml|yaml|properties|conf)|.*(ingress|gateway|nginx|traefik|apache|caddy|values).*|.*\.(conf|yaml|yml))$' |
while IFS= read -r file; do
matches=$(rg -n -C 3 'X-Forwarded-For|ForwardedHeaderFilter|forward-headers|remoteIpHeader|proxy_set_header|forward-headers-strategy|server\.forward-headers' "$file" || true)
if [ -n "$matches" ]; then
echo "===== $file ====="
printf '%s\n' "$matches"
fi
doneRepository: EntryDSM/entrydsm-platform
Length of output: 33135
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "===== observability adapter tests ====="
git ls-files 'systems/observability/observability-adapter-in/src/test/**' || true
rg -n -C 3 'MonitorStreamController|SseConnectionLimiter|X-Forwarded-For|clientIp' \
systems/observability/observability-adapter-in/src/test \
systems/observability/observability-adapter-in/src/main \
2>/dev/null || true
echo "===== all forwarded-header references ====="
rg -n -C 2 'X-Forwarded-For|ForwardedHeaderFilter|forward-headers-strategy|server\.forward-headers|proxy_set_header' . \
-g '!build' -g '!dist' -g '!node_modules' || true
echo "===== deterministic limiter behavior model ====="
python3 - <<'PY'
MAX_CONNECTIONS_PER_KEY = 3
counts = {}
def try_acquire(key):
count = counts.setdefault(key, 0)
count += 1
counts[key] = count
if count <= MAX_CONNECTIONS_PER_KEY:
return True
counts[key] -= 1
return False
def release(key):
if key in counts:
counts[key] -= 1
for i in range(10_000):
key = f"attacker-{i}"
assert try_acquire(key)
release(key)
print(f"distinct_keys_after_release={len(counts)}")
print(f"nonzero_keys_after_release={sum(v != 0 for v in counts.values())}")
print(f"sample_count={counts['attacker-0']}")
PYRepository: EntryDSM/entrydsm-platform
Length of output: 14774
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "===== SseBroadcaster lifecycle ====="
fd -HI -t f 'SseBroadcaster.kt' systems/observability/observability-adapter-in |
while IFS= read -r file; do
cat -n "$file"
done
echo "===== atomic-removal interleaving model ====="
python3 - <<'PY'
# Model a common proposed fix:
# release decrements, then removes the map entry when the value reaches zero.
# tryAcquire obtains the AtomicInteger reference first, then increments it.
map_entry = {"k": 1}
entry_reference = map_entry
# Existing connection releases after the new request obtained the reference.
map_entry.pop("k")
entry_reference["value"] = 1 # conceptual AtomicInteger held by the new request
print("map_contains_new_acquisition=", "k" in map_entry)
print("new_acquisition_count_is_tracked=", "k" in map_entry and map_entry["k"] is entry_reference)
echo = None
PYRepository: EntryDSM/entrydsm-platform
Length of output: 3748
신뢰된 프록시가 정규화한 IP만 제한 키로 사용하십시오.
현재 구현은 요청 경로와 관계없이 X-Forwarded-For의 첫 값을 사용합니다. 애플리케이션이 직접 노출되거나 프록시가 헤더를 덮어쓰지 않으면 공격자는 값을 변경하여 SseConnectionLimiter와 session:·clientlog: rate limit을 우회할 수 있습니다. 신뢰된 프록시 요청에서만 전달 헤더를 사용하고, 그 외에는 request.remoteAddr를 사용하십시오.
SseConnectionLimiter.release는 카운트가 0이 된 키를 counts에서 제거하지 않습니다. ConcurrentHashMap의 키별 원자 연산으로 acquire와 release를 갱신하고, 마지막 연결 해제 시 키를 제거하십시오. SseConnectionLimiter에 초과 연결, 해제 후 제거, 동시 acquire/release 테스트도 추가하십시오.
📍 Affects 2 files
systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/MonitorStreamController.kt#L33-L38(this comment)systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseConnectionLimiter.kt#L13-L24
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/MonitorStreamController.kt`
around lines 33 - 38, Update MonitorStreamController.clientIp to use
X-Forwarded-For only for requests from a trusted proxy; otherwise use
request.remoteAddr, ensuring all limiter keys use the normalized trusted
address. In SseConnectionLimiter.kt lines 13-24, make acquire and release use
atomic ConcurrentHashMap key operations, remove entries when the final
connection is released, and preserve the excess-connection rejection behavior.
Add tests covering connection-limit rejection, removal after release, and
concurrent acquire/release.
| @GetMapping("/api/monitor/v11/reports") | ||
| fun generate( | ||
| @RequestParam(defaultValue = "xlsx") format: String, | ||
| ): ApiResponse<ReportGeneratedResponse> { | ||
| val parsedFormat = runCatching { ReportFormat.valueOf(format.trim().uppercase()) } | ||
| .getOrElse { throw MonitorDomainException(ErrorCode.INVALID_FORMAT) } | ||
| val result = generateReportUseCase.generate(parsedFormat) | ||
| return ApiResponse(data = result.toResponse()) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
변경된 두 웹 흐름에 MVC 테스트를 추가하십시오.
현재 추가된 ReportServiceTest는 애플리케이션 서비스만 검증합니다. 웹 어댑터의 요청 파싱, HTTP 상태, 헤더, 본문 계약은 검증하지 않습니다.
systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/ReportController.kt#L18-L25: 기본 XLSX 형식, CSV 형식, 잘못된format의 오류 응답을 검증하는 MVC 테스트를 추가하십시오.systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/ReportDownloadController.kt#L16-L22: 유효 토큰의200응답 및 첨부 헤더와 만료 또는 없는 토큰의404응답을 검증하는 MVC 테스트를 추가하십시오.
As per coding guidelines, "**/*.{kt,go}: If production logic is changed in Kotlin or Go files, require corresponding test updates in the same subsystem unless the PR description explicitly justifies why tests are unnecessary." As per path instructions, "**/*.kt: Highlight behavior-changing code that lacks corresponding unit/integration tests."
📍 Affects 2 files
systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/ReportController.kt#L18-L25(this comment)systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/ReportDownloadController.kt#L16-L22
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/ReportController.kt`
around lines 18 - 25, The MVC test suite requires coverage for both changed web
flows. In ReportController.kt lines 18-25, add tests for the default XLSX
request, CSV format, and invalid format error response; in
ReportDownloadController.kt lines 16-22, add tests for a valid token returning
200 with attachment headers and for expired or missing tokens returning 404.
Verify request parsing, HTTP status, headers, and response bodies using the
subsystem’s existing MVC test conventions.
Sources: Coding guidelines, Path instructions
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
보고서 생성을 GET에서 분리하십시오.
Line 18의 요청은 파일을 저장하고 Redis 다운로드 토큰을 생성합니다. GET 요청은 안전한 조회여야 합니다. 프리페치 또는 재시도가 이 엔드포인트를 호출하면 불필요한 파일과 토큰을 생성할 수 있습니다. 생성 API를 POST로 변경하십시오.
수정 예시
-import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.PostMapping
...
- `@GetMapping`("/api/monitor/v11/reports")
+ `@PostMapping`("/api/monitor/v11/reports")📝 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.
| @GetMapping("/api/monitor/v11/reports") | |
| fun generate( | |
| @RequestParam(defaultValue = "xlsx") format: String, | |
| ): ApiResponse<ReportGeneratedResponse> { | |
| val parsedFormat = runCatching { ReportFormat.valueOf(format.trim().uppercase()) } | |
| .getOrElse { throw MonitorDomainException(ErrorCode.INVALID_FORMAT) } | |
| val result = generateReportUseCase.generate(parsedFormat) | |
| return ApiResponse(data = result.toResponse()) | |
| `@PostMapping`("/api/monitor/v11/reports") | |
| fun generate( | |
| `@RequestParam`(defaultValue = "xlsx") format: String, | |
| ): ApiResponse<ReportGeneratedResponse> { | |
| val parsedFormat = runCatching { ReportFormat.valueOf(format.trim().uppercase()) } | |
| .getOrElse { throw MonitorDomainException(ErrorCode.INVALID_FORMAT) } | |
| val result = generateReportUseCase.generate(parsedFormat) | |
| return ApiResponse(data = result.toResponse()) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/ReportController.kt`
around lines 18 - 25, Update the generate method mapping in ReportController
from GET to POST while preserving its format parsing and report-generation
logic.
| lateinit var secret: String | ||
| lateinit var issuer: String |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ast-grep outline systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthProperties.kt --items all
rg -n -C 3 'ConfigurationPropertiesScan|EnableConfigurationProperties|auth\.jwt|auth:|jwt:|starter-validation|jakarta\.validation' .Repository: EntryDSM/entrydsm-platform
Length of output: 17569
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- JwtAuthProperties.kt ---'
cat -n systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthProperties.kt
echo '--- JwtAuthInterceptor usages and implementation ---'
rg -n -C 5 'class JwtAuthInterceptor|JwtAuthProperties|\.secret|\.issuer' systems/observability
echo '--- configuration registration ---'
rg -n -C 4 'ComponentScan|ConfigurationProperties|JwtAuthInterceptor|WebMvcConfigurer|addInterceptors' systems/observability
echo '--- related tests ---'
rg -n -C 5 'JwtAuthProperties|JwtAuthInterceptor|auth\.jwt|JWT_SECRET|JWT_ISSUER' systems/observability --glob '*Test.kt' --glob '*test.yaml' --glob '*test.yml' --glob 'application*.yaml' --glob 'application*.yml'Repository: EntryDSM/entrydsm-platform
Length of output: 34168
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p = Path("systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthProperties.kt")
text = p.read_text()
required = ["lateinit var secret: String", "lateinit var issuer: String"]
print("properties_file_exists:", p.exists())
for item in required:
print(f"{item!r}:", item in text)
for path in Path("systems/observability").rglob("*.kt"):
t = path.read_text()
if "JwtAuthProperties" in t or "JwtAuthInterceptor" in t:
print(f"reference: {path}")
PYRepository: EntryDSM/entrydsm-platform
Length of output: 955
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- build and test configuration for observability modules ---'
rg -n -C 5 'spring-boot-starter-validation|spring-boot-starter-test|testImplementation|application-test|`@SpringBootTest`|`@WebMvcTest`' systems/observability kotlin.MODULE.bazel WORKSPACE* MODULE.bazel 2>/dev/null || true
echo '--- all JWT property declarations and configuration files ---'
rg -n -C 3 'auth:[[:space:]]*$|jwt:[[:space:]]*$|secret:|issuer:' systems/observability --glob '*.yaml' --glob '*.yml' --glob '*.kt'
echo '--- interceptor source candidates ---'
fd -i 'JwtAuthInterceptor|JwtAuthProperties' systems/observabilityRepository: EntryDSM/entrydsm-platform
Length of output: 4652
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- observability bootstrap entry point and dependencies ---'
fd -i 'application|main|BUILD|MODULE' systems/observability
rg -n -C 5 'SpringBootApplication|observability-adapter-in|resources|application.yaml' systems/observability --glob '*.kt' --glob 'BUILD*' --glob '*.bzl' --glob '*.yaml' --glob '*.yml'
echo '--- interceptor complete relevant flow ---'
cat -n systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthInterceptor.kt | sed -n '19,45p'
echo '--- bootstrap application configuration ---'
cat -n systems/observability/observability-bootstrap/src/main/resources/application.yaml | sed -n '20,32p'Repository: EntryDSM/entrydsm-platform
Length of output: 8693
🌐 Web query:
Spring Boot 4 ConfigurationProperties JavaBean lateinit property missing binding startup validation @validated documentation
💡 Result:
In Spring Boot, using Kotlin's lateinit properties with @ConfigurationProperties can lead to runtime issues, such as UninitializedPropertyAccessException [1][2]. This occurs because the Spring Boot binder may attempt to access these properties to determine if they have already been initialized before applying values, which is incompatible with the semantics of lateinit [2]. For reliable configuration binding and validation: 1. Constructor Binding: Use constructor-based property binding instead of lateinit properties [3][4]. This is the recommended, idiomatic way to handle immutable configuration in Kotlin [3]. 2. Validation: To use @Validated effectively with constructor parameters, ensure you use the appropriate annotation use-site targets (e.g., @field:NotBlank) [5][6]. Simply annotating the constructor parameters directly is often insufficient because Hibernate Validator looks for constraints on fields or getters, not constructor arguments [6]. By using use-site targets, you instruct the Kotlin compiler to place the validation annotations where the validator can discover them [5][6]. Example of recommended usage: @Validated @ConfigurationProperties("my.app") data class MyProperties( @field:NotBlank val username: String, @field:NotBlank val password: String) If you must use setter-based injection, standard mutable properties (or nullable types) are generally preferred over lateinit properties to avoid the uninitialized access exceptions during the binding process [5]. Spring Boot validation at startup requires the presence of a JSR-303 implementation, such as the spring-boot-starter-validation dependency [5][7].
Citations:
- 1: Constructor binding of @ConfigurationProperties to a lateinit property fails with kotlin.UninitializedPropertyAccessException spring-projects/spring-boot#35654
- 2: Constructor binding of @ConfigurationProperties to a lateinit property fails with kotlin.UninitializedPropertyAccessException spring-projects/spring-boot#35603
- 3: https://www.baeldung.com/kotlin/spring-boot-configurationproperties
- 4: https://docs.spring.io/spring-boot/docs/3.2.2/reference/html/features.html
- 5: Unable to use @Validated on constructor arguments spring-projects/spring-boot#31991
- 6: @Validated does not work with @ConstructorBinding on a Kotlin data class spring-projects/spring-boot#22666
- 7: https://docs.spring.io/spring-boot/4.1-SNAPSHOT/reference/io/validation.html
기본 JWT 시크릿으로 애플리케이션이 시작되지 않게 하세요.
application.yaml은 JWT_SECRET이 없을 때 알려진 개발용 문자열을 사용합니다. 운영 환경에서 환경 변수가 누락되면 애플리케이션이 정상 시작하고, 공격자가 해당 시크릿으로 JWT를 위조할 수 있습니다.
JWT_SECRET의 기본값을 제거하고 auth.jwt.secret을 시작 시 필수 값으로 검증하세요. 누락 또는 공백 값에 대한 설정 바인딩 테스트도 추가하세요.
🧰 Tools
🪛 detekt (1.23.8)
[warning] 13-13: Usages of lateinit should be avoided.
(detekt.potential-bugs.LateinitUsage)
[warning] 14-14: Usages of lateinit should be avoided.
(detekt.potential-bugs.LateinitUsage)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthProperties.kt`
around lines 13 - 14, Remove the development fallback for JWT_SECRET in the
application configuration and make auth.jwt.secret mandatory during startup.
Update JwtAuthProperties so missing or blank secret values fail validation
during configuration binding, and add binding tests covering both absent and
whitespace-only values; keep issuer handling unchanged.
Source: Linters/SAST tools
| private fun broadcast(eventName: String, data: Any) { | ||
| emitters.forEach { emitter -> | ||
| runCatching { emitter.send(SseEmitter.event().id(nextId()).name(eventName).data(data)) } | ||
| .onFailure { emitters.remove(emitter) } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- target outline ---'
ast-grep outline systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseBroadcaster.kt || true
printf '%s\n' '--- target source ---'
cat -n systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseBroadcaster.kt
printf '%s\n' '--- relevant symbols and scheduler configuration ---'
rg -n -S \
'SseBroadcaster|broadcastFrequent|broadcastResource|ping|Scheduled|TaskScheduler|ThreadPoolTaskScheduler|SseEmitter|\.send\(' \
systems gradle* build.gradle* settings.gradle* 2>/dev/null | head -n 300
printf '%s\n' '--- related tests ---'
rg -l -S 'SseBroadcaster|SseEmitter|broadcastFrequent|broadcastResource' . --glob '*Test.kt' --glob '*Tests.kt' --glob '*test*' | sortRepository: EntryDSM/entrydsm-platform
Length of output: 12866
🌐 Web query:
site:docs.spring.io/spring-framework/reference/web/webmvc/mvc-ann-async.html SseEmitter send blocking TaskScheduler default scheduler @Scheduled``
💡 Result:
SseEmitter writes to the HTTP response are blocking operations [1]. When using SseEmitter, these blocking writes are executed on a separate thread, which is managed by a configured AsyncTaskExecutor [1]. It is important to distinguish between the AsyncTaskExecutor used for SseEmitter and the TaskScheduler typically used for @Scheduled tasks. While the AsyncTaskExecutor handles the blocking I/O required for streaming responses [1], using a default, unconfigured executor in production is discouraged as it is not suitable for handling load [1]. Because SseEmitter sends are blocking, they should not be executed directly on the default TaskScheduler thread pool if that pool is also responsible for executing @Scheduled tasks. Doing so could lead to thread starvation, where blocking I/O operations from SseEmitter prevent scheduled tasks from executing on time. To avoid this, you should ensure that your application uses distinct, appropriately sized thread pools for background streaming tasks versus scheduled periodic tasks. Additionally, since the Servlet API does not notify the server when a client disconnects, you should implement periodic heartbeats to detect failed writes [1].
Citations:
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- scheduling and executor configuration ---'
rg -n -S \
'`@EnableScheduling`|`@EnableAsync`|SchedulingConfigurer|configureTasks|TaskScheduler|TaskExecutor|AsyncTaskExecutor|ThreadPoolTaskScheduler|ThreadPoolTaskExecutor|spring\.task\.scheduling|scheduling' \
. --glob '!**/build/**' --glob '!**/node_modules/**' | head -n 400
printf '%s\n' '--- broadcaster call sites ---'
rg -n -S 'sseBroadcaster\.(register|publishLog)|SseLiveLogPublisher|MonitorStreamController' \
systems --glob '*.kt' | head -n 300
printf '%s\n' '--- stream and publisher implementations ---'
for f in $(fd -t f -e kt . systems/observability/observability-adapter-in/src/main/kotlin | \
xargs rg -l -S 'class MonitorStreamController|class SseLiveLogPublisher'); do
echo "--- $f"
cat -n "$f"
done
printf '%s\n' '--- test files in the observability adapter-in subsystem ---'
git ls-files 'systems/observability/observability-adapter-in' | \
rg '(^|/)(test|tests)(/|$)|Test\.kt$|Tests\.kt$' || trueRepository: EntryDSM/entrydsm-platform
Length of output: 5359
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re
target = Path("systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseBroadcaster.kt")
text = target.read_text()
checks = {
"scheduled methods call broadcast": bool(re.search(r'`@Scheduled`[\s\S]*?fun broadcastFrequent[\s\S]*?broadcast\(', text)),
"scheduled methods call emitter.send": bool(re.search(r'`@Scheduled`[\s\S]*?fun ping[\s\S]*?emitter\.send\(', text)),
"broadcast iterates emitters": bool(re.search(r'emitters\.forEach\s*\{\s*emitter\s*->[\s\S]*?emitter\.send\(', text)),
"send failures remove emitter": bool(re.search(r'\.onFailure\s*\{\s*emitters\.remove\(emitter\)', text)),
}
for name, result in checks.items():
print(f"{name}: {result}")
for m in re.finditer(r'(`@Scheduled`[^\n]*\n\s*fun\s+\w+\(\)[\s\S]*?)(?=\n\s*`@Scheduled`|\n\s*private fun|\n\s*companion object)', text):
block = m.group(1)
method = re.search(r'fun\s+(\w+)', block).group(1)
sends = len(re.findall(r'\.send\(', block))
broadcasts = len(re.findall(r'\bbroadcast\(', block))
print(f"{method}: send_calls={sends}, broadcast_calls={broadcasts}")
PYRepository: EntryDSM/entrydsm-platform
Length of output: 454
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- application bootstrap ---'
cat -n systems/observability/observability-bootstrap/src/main/kotlin/hs/kr/entrydsm/ExampleApplication.kt
printf '%s\n' '--- Spring dependency versions ---'
rg -n -S \
'spring-boot|spring-framework|springBoot|org\.springframework' \
gradle.properties settings.gradle* build.gradle* systems --glob '*.gradle' --glob '*.gradle.kts' --glob '*.toml' --glob '*.properties' | head -n 250
printf '%s\n' '--- all scheduled methods ---'
rg -n -B 3 -A 12 '`@Scheduled`' . --glob '*.kt' --glob '*.java' --glob '!**/build/**'Repository: EntryDSM/entrydsm-platform
Length of output: 6422
🌐 Web query:
site:docs.spring.io/spring-framework/reference/integration/scheduling.html @scheduled default TaskScheduler single-threaded
💡 Result:
By default, Spring's @Scheduled annotation uses a single-threaded TaskScheduler [1]. When scheduling tasks without an explicitly configured scheduler bean, Spring defaults to a TaskScheduler with a pool size of one [1]. If you are using XML configuration, the scheduler element defaults to a single-threaded pool if the pool-size attribute is omitted [1]. Similarly, when configuring programmatic scheduling, the default implementation often relies on a scheduler configured with a single thread unless otherwise specified. Consequently, if multiple @Scheduled tasks are configured, they will be executed sequentially by this single thread [1]. To execute tasks in parallel, you must explicitly configure a TaskScheduler bean with a larger pool size [1].
Citations:
🌐 Web query:
site:docs.spring.io/spring-framework/reference/web/webmvc/mvc-ann-async.html ResponseBodyEmitter SseEmitter blocking writes AsyncTaskExecutor send
💡 Result:
In Spring MVC, while ResponseBodyEmitter and its subclass SseEmitter support asynchronous streaming, individual writes to the response remain inherently blocking [1]. When you return an SseEmitter from a controller method, Spring MVC executes the writing process on a separate thread provided by the configured AsyncTaskExecutor [1]. This architecture ensures that the main servlet container thread is not blocked, allowing it to return to the thread pool while the SSE stream continues to process data [1]. Key points regarding the interaction between these components include: 1. Blocking Nature of Writes: Although the overall programming model is asynchronous, the actual I/O operations for each write to the response are blocking [1]. This is a fundamental difference between Spring MVC and Spring WebFlux; WebFlux utilizes non-blocking I/O and does not require a dedicated thread per write [1]. 2. Role of AsyncTaskExecutor: The AsyncTaskExecutor is responsible for handling the blocking writes when streaming, as well as executing tasks associated with asynchronous request processing (such as Callable or DeferredResult) [1]. 3. Performance Considerations: Spring explicitly notes that the default AsyncTaskExecutor is often not suitable for production environments under heavy load [1]. It is recommended to configure a custom TaskExecutor (e.g., using a ThreadPoolTaskExecutor) with appropriately tuned parameters to prevent thread exhaustion or performance degradation during streaming operations [1]. By offloading these blocking writes to a separate thread pool managed by the AsyncTaskExecutor, Spring MVC maintains scalability even while using blocking I/O for streaming responses [1].
Citations:
느린 SSE 구독자의 전송을 분리하십시오.
emitters.forEach는 SseEmitter.send를 순차 실행합니다. SseEmitter.send의 블로킹 쓰기 때문에 느린 연결 하나가 다른 연결과 broadcastFrequent, broadcastResource, ping을 지연시킬 수 있습니다. 별도 TaskScheduler 설정이 없으므로 @Scheduled 작업은 기본 단일 스레드에서 실행됩니다.
전용 bounded executor와 연결별 bounded outbound queue를 사용하십시오. 큐가 가득 차면 해당 emitter를 종료하십시오. 스케줄러 및 API 요청 스레드에서 직접 send를 호출하지 마십시오. 연결별 전송 순서와 큐 포화 동작을 검증하는 테스트도 추가하십시오.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseBroadcaster.kt`
around lines 66 - 69, Update SseBroadcaster.broadcast and the emitter lifecycle
so scheduled or API threads only enqueue events, while each emitter drains
through a dedicated bounded executor and per-connection bounded outbound queue.
Preserve event order per connection, remove and complete emitters whose queue is
full, and ensure send failures also clean up the connection; add tests covering
ordered delivery and queue-saturation termination.
| override fun generate(format: ReportFormat, snapshot: DashboardSnapshotResult): ByteArray = | ||
| when (format) { | ||
| ReportFormat.XLSX -> toXlsx(snapshot) | ||
| ReportFormat.CSV -> toCsv(snapshot).toByteArray() | ||
| } | ||
|
|
||
| private fun rows(snapshot: DashboardSnapshotResult): List<Pair<String, String>> = | ||
| listOf( | ||
| "generatedAt" to snapshot.generatedAt.toString(), | ||
| "round" to snapshot.period.round, | ||
| "totalVisitors" to snapshot.traffic.totalVisitors.toString(), | ||
| "concurrentCurrent" to snapshot.traffic.concurrent.current.toString(), | ||
| "concurrentMax" to snapshot.traffic.concurrent.max.toString(), | ||
| "concurrentAvg" to snapshot.traffic.concurrent.avg.toString(), | ||
| "avgSessionDurationSeconds" to snapshot.traffic.avgSessionDurationSeconds.toString(), | ||
| "apiTotalRequests" to snapshot.api.totalRequests.toString(), | ||
| "apiSuccessCount" to snapshot.api.successCount.toString(), | ||
| "apiFailureCount" to snapshot.api.failureCount.toString(), | ||
| "applicationSubmitSuccess" to snapshot.business.applicationSubmit.success.toString(), | ||
| "applicationSubmitFailure" to snapshot.business.applicationSubmit.failure.toString(), | ||
| "pdfDownloadSuccess" to snapshot.business.pdfDownload.success.toString(), | ||
| "pdfDownloadFailure" to snapshot.business.pdfDownload.failure.toString(), | ||
| "clientLogErrorCount" to snapshot.clientLog.errorCount.toString(), | ||
| "clientLogWarnCount" to snapshot.clientLog.warnCount.toString(), | ||
| "dbUsedBytes" to snapshot.resource.dbUsedBytes.toString(), | ||
| "bucketUsedBytes" to snapshot.resource.bucketUsedBytes.toString(), | ||
| ) + snapshot.services.items.map { "activeUsers_${it.service}" to it.activeUsers.toString() } | ||
|
|
||
| private fun toCsv(snapshot: DashboardSnapshotResult): String = | ||
| buildString { | ||
| appendLine("metric,value") | ||
| rows(snapshot).forEach { (key, value) -> appendLine("$key,$value") } | ||
| } | ||
|
|
||
| private fun toXlsx(snapshot: DashboardSnapshotResult): ByteArray { | ||
| XSSFWorkbook().use { workbook -> | ||
| val sheet = workbook.createSheet("monitor") | ||
| sheet.createRow(0).apply { | ||
| createCell(0).setCellValue("metric") | ||
| createCell(1).setCellValue("value") | ||
| } | ||
| rows(snapshot).forEachIndexed { index, (key, value) -> | ||
| sheet.createRow(index + 1).apply { | ||
| createCell(0).setCellValue(key) | ||
| createCell(1).setCellValue(value) | ||
| } | ||
| } | ||
| val out = ByteArrayOutputStream() | ||
| workbook.write(out) | ||
| return out.toByteArray() | ||
| } | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
새 어댑터 동작을 직접 검증하는 테스트를 추가하세요.
제공된 ReportServiceTest는 두 포트를 가짜 구현으로 대체합니다. 따라서 CSV/XLSX 직렬화와 Redis·파일 저장 동작을 검증하지 못합니다.
systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/XlsxCsvReportGenerator.kt#L13-L64: CSV 특수문자 이스케이프와 생성된 XLSX의 행·셀 값을 검증하는 테스트를 추가하세요.systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/LocalFileReportObjectStorageAdapter.kt#L26-L43: 저장·해결·만료·고유 객체 경로를 검증하는 테스트를 추가하세요.
As per coding guidelines, "**/*.{kt,go}: If production logic is changed in Kotlin or Go files, require corresponding test updates in the same subsystem unless the PR description explicitly justifies why tests are unnecessary".
📍 Affects 2 files
systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/XlsxCsvReportGenerator.kt#L13-L64(this comment)systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/LocalFileReportObjectStorageAdapter.kt#L26-L43
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/XlsxCsvReportGenerator.kt`
around lines 13 - 64,
systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/XlsxCsvReportGenerator.kt:13-64
— Add direct tests for XlsxCsvReportGenerator.generate, covering CSV escaping of
commas, quotes, and newlines and verifying generated XLSX row and cell values.
systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/LocalFileReportObjectStorageAdapter.kt:26-43
— Add direct tests for LocalFileReportObjectStorageAdapter covering save,
resolve, expiration handling, and unique object paths.
Source: Coding guidelines
| private fun toCsv(snapshot: DashboardSnapshotResult): String = | ||
| buildString { | ||
| appendLine("metric,value") | ||
| rows(snapshot).forEach { (key, value) -> appendLine("$key,$value") } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
CSV 필드를 RFC 4180 형식으로 이스케이프하세요.
round 또는 서비스 이름에 쉼표, 큰따옴표, 줄바꿈이 있으면 현재 출력은 열 구조가 깨집니다. 각 필드를 큰따옴표로 감싸고 내부 큰따옴표를 이중화하세요.
수정 예시
+ private fun csvField(value: String): String =
+ "\"${value.replace("\"", "\"\"")}\""
+
private fun toCsv(snapshot: DashboardSnapshotResult): String =
buildString {
appendLine("metric,value")
- rows(snapshot).forEach { (key, value) -> appendLine("$key,$value") }
+ rows(snapshot).forEach { (key, value) ->
+ appendLine("${csvField(key)},${csvField(value)}")
+ }
}📝 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.
| private fun toCsv(snapshot: DashboardSnapshotResult): String = | |
| buildString { | |
| appendLine("metric,value") | |
| rows(snapshot).forEach { (key, value) -> appendLine("$key,$value") } | |
| } | |
| private fun csvField(value: String): String = | |
| "\"${value.replace("\"", "\"\"")}\"" | |
| private fun toCsv(snapshot: DashboardSnapshotResult): String = | |
| buildString { | |
| appendLine("metric,value") | |
| rows(snapshot).forEach { (key, value) -> | |
| appendLine("${csvField(key)},${csvField(value)}") | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/XlsxCsvReportGenerator.kt`
around lines 41 - 45, Update toCsv and its row serialization so every CSV field,
including the header values and each key/value from rows(snapshot), is enclosed
in double quotes and any embedded double quotes are doubled, preserving commas
and newlines according to RFC 4180.
| clientLogStorePort.record(input) | ||
| liveLogPublisherPort.publishClientLog(input) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
새 LiveLogPublisherPort 계약을 테스트로 검증하십시오.
현재 테스트는 LiveLogPublisherPort {}를 사용합니다. 따라서 발행 누락, 정규화 전 데이터 발행, 거부된 요청에서의 발행을 감지하지 못합니다.
systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionService.kt#L41-L42: 저장 후 정규화된ClientLogInput을 한 번만 발행하는 순서를 유지하십시오.systems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionServiceTest.kt#L33-L33: 기록형 fake publisher를 사용하고 발행된 값이 저장된 값과 동일한지 검증하십시오.systems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionServiceTest.kt#L45-L45: 빈 배치와 초과 배치에서 publisher 호출 수가 0인지 검증하십시오.systems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionServiceTest.kt#L55-L55: rate limit 거부에서 publisher 호출 수가 0인지 검증하십시오.
As per coding guidelines, "If production logic is changed in Kotlin or Go files, require corresponding test updates in the same subsystem unless the PR description explicitly justifies why tests are unnecessary."
📍 Affects 2 files
systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionService.kt#L41-L42(this comment)systems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionServiceTest.kt#L33-L33systems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionServiceTest.kt#L45-L45systems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionServiceTest.kt#L55-L55
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionService.kt`
around lines 41 - 42, Update ClientLogCollectionService.kt:41-42 to preserve
saving first, then publish exactly one normalized ClientLogInput. In
ClientLogCollectionServiceTest.kt:33-33, replace the no-op LiveLogPublisherPort
with a recording fake and assert the published value equals the stored value; at
lines 45-45 and 55-55, assert publisher calls remain zero for empty/oversized
batches and rate-limit rejection.
Source: Coding guidelines
| import org.springframework.stereotype.Service | ||
|
|
||
| /** ponytail: 데이터량이 적어 동기 생성만 지원한다(202 GENERATING/폴링 큐 없음). 느려지면 잡 큐로 교체. */ | ||
| @Service | ||
| class ReportService( |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- candidate file ---'
cat -n systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ReportService.kt | sed -n '1,120p'
printf '%s\n' '--- application module build files ---'
find systems/observability -maxdepth 3 -type f \( -name 'build.gradle' -o -name 'build.gradle.kts' -o -name 'pom.xml' \) -print \
-exec sh -c 'echo "--- $1"; sed -n "1,220p" "$1"' _ {} \;
printf '%s\n' '--- ReportService references and Spring wiring ---'
rg -n --glob '!build/**' 'ReportService|`@Service`|ReportService\(' systems/observabilityRepository: EntryDSM/entrydsm-platform
Length of output: 4482
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- relevant build configuration ---'
find . -type f \( -name 'build.gradle' -o -name 'build.gradle.kts' -o -name 'pom.xml' \) -print |
while IFS= read -r f; do
if grep -q 'observability-application\|spring-context\|spring-boot' "$f"; then
echo "--- $f"
grep -n -C 4 'observability-application\|spring-context\|spring-boot' "$f"
fi
done
printf '%s\n' '--- module dependency declarations ---'
rg -n --glob 'build.gradle*' --glob 'pom.xml' 'observability-application|spring' .
printf '%s\n' '--- all application service annotations and configuration classes ---'
rg -n --glob '*.kt' '`@Service`|`@Configuration`|`@Bean`|`@ComponentScan`|`@Import`' systems/observabilityRepository: EntryDSM/entrydsm-platform
Length of output: 240
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- repository build and module files ---'
git ls-files | grep -E '(^|/)(settings|build)\.gradle(\.kts)?$|(^|/)pom\.xml$|(^|/)gradle\.properties$|(^|/)libs\.versions\.toml$' | sed -n '1,160p'
printf '%s\n' '--- observability module files ---'
git ls-files systems/observability | sed -n '1,160p'
printf '%s\n' '--- application-module Spring imports ---'
rg -n --glob '*.kt' '^import org\.springframework|@(Service|Component|Configuration|Bean)' systems/observability/observability-application || trueRepository: EntryDSM/entrydsm-platform
Length of output: 21013
🏁 Script executed:
#!/bin/bash
set -eu
for f in \
systems/observability/observability-application/BUILD.bazel \
systems/observability/observability-application/deps.bzl \
systems/observability/observability-bootstrap/BUILD.bazel \
systems/observability/observability-bootstrap/src/main/kotlin/hs/kr/entrydsm/ExampleApplication.kt \
systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/ExampleApplication.kt \
systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/ExampleApplication.kt
do
if [ -f "$f" ]; then
echo "--- $f"
cat -n "$f" | sed -n '1,220p'
fi
done
printf '%s\n' '--- Spring configuration and explicit service bean registration ---'
rg -n --glob '*.kt' '`@Bean`|`@ComponentScan`|`@SpringBootApplication`|`@Import`|ReportService|ClientLogQueryService|MonitorDashboardService' systems/observabilityRepository: EntryDSM/entrydsm-platform
Length of output: 6054
애플리케이션 계층에서 Spring 의존성을 제거하세요.
observability-application 모듈이 spring-boot-starter에 직접 의존하고 ReportService가 @Service를 사용합니다. 빈 등록을 observability-bootstrap의 @Bean 설정으로 이동하고, 애플리케이션 모듈의 Spring 의존성을 제거하세요. 다른 애플리케이션 서비스에도 같은 기준을 적용하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ReportService.kt`
around lines 14 - 18, Remove the Spring `@Service` dependency from ReportService
and register it through a `@Bean` configuration in observability-bootstrap
instead. Remove the direct Spring Boot starter dependency from
observability-application, and apply the same migration to any other
application-layer services using Spring annotations.
Source: Coding guidelines
| val dateStamp = DATE_FORMATTER.format(Instant.now(clock).atZone(ZONE)) | ||
| val fileName = "entrymonitor_${round.name}_$dateStamp.${format.name.lowercase()}" | ||
| val stored = reportObjectStoragePort.store(fileName, bytes) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
리포트마다 변경 불가능한 저장 객체를 사용하세요.
같은 날짜, 같은 round.name, 같은 ReportFormat으로 생성한 리포트는 같은 fileName을 사용합니다. 이후 생성 요청이 기존 파일을 덮어쓰므로, 이전 토큰도 새 리포트 바이트를 반환합니다. 토큰별 고유 저장 객체를 만들고 Redis에는 저장 경로와 원래 다운로드 파일명을 함께 저장하세요.
systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ReportService.kt#L30-L32: 표시용 파일명과 저장 객체 식별자를 분리하세요.systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/LocalFileReportObjectStorageAdapter.kt#L26-L31: 토큰별 고유 경로에 저장하고, 기존 토큰이 같은 파일을 참조하지 않게 하세요.
📍 Affects 2 files
systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ReportService.kt#L30-L32(this comment)systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/LocalFileReportObjectStorageAdapter.kt#L26-L31
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ReportService.kt`
around lines 30 - 32, ReportService.kt:30-32의 ReportService에서 표시용 다운로드 파일명과 저장
객체 식별자를 분리하고, 토큰마다 고유한 저장 경로를 생성해 Redis에 저장 경로와 원래 파일명을 함께 기록하세요.
LocalFileReportObjectStorageAdapter.kt:26-31의
LocalFileReportObjectStorageAdapter는 전달받은 토큰별 고유 경로에 저장하도록 수정해 기존 토큰이 새 리포트 객체를
참조하지 않게 하세요.
Summary
Related Issue
Implementation
알려진 제약 (문서에 근거 없어 의도적으로 비워둔 부분)
Testing
bazel build //...,bazel test //systems/...전체 통과, 다른 시스템 영향 없음 확인Deployment Notes
spring.data.redis.*,auth.jwt.secret/issuer,monitor.services.*.base-url,monitor.round.*,monitor.report.storage-dir환경변수 설정 필요 (dev 기본값은 application.yaml에 있음)Checklist