Skip to content

feat(observability): 리포트 다운로드·SSE 스트림·JWT 인증 구현 #29 - #72

Open
tlgms wants to merge 3 commits into
feature/29-observability-03-logs-resourcesfrom
feature/29-observability-도메인-개발

Hidden character warning

The head ref may contain hidden characters: "feature/29-observability-\ub3c4\uba54\uc778-\uac1c\ubc1c"
Open

feat(observability): 리포트 다운로드·SSE 스트림·JWT 인증 구현 #29#72
tlgms wants to merge 3 commits into
feature/29-observability-03-logs-resourcesfrom
feature/29-observability-도메인-개발

Conversation

@tlgms

@tlgms tlgms commented Aug 11, 2026

Copy link
Copy Markdown

Summary

Related Issue

Implementation

  • 리포트 다운로드: 대시보드 스냅샷을 POI(xlsx)/CSV로 변환, 로컬 디스크 + Redis 만료 토큰(5분)으로 S3 presigned URL 대체. 동기 생성만 지원(202 GENERATING/폴링 큐 없음 — 데이터량이 적어 실익 없음)
  • SSE 스트림: SseEmitter + 스케줄러로 snapshot(연결 시 1회)/traffic·api·business·service(5초)/resource(5분)/ping(15초)/log(발생 즉시) 브로드캐스트. 계정 인증이 아직 없어 IP 기준 동시 커넥션 3개 제한(TOO_MANY_CONNECTIONS)
  • JWT 인증: identity의 정식 Spring Security/JWT가 아직 병합되지 않아, 자체 경량 HandlerInterceptor로 Bearer 토큰 서명·발급자·만료·role=ADMIN 클레임을 검증. 설정 키 이름(auth.jwt.secret/issuer)은 identity와 통일해 정식 인증 병합 시 시크릿만 맞추면 되게 함
  • collect/session, collect/client-log 두 공개 엔드포인트만 인증 제외

알려진 제약 (문서에 근거 없어 의도적으로 비워둔 부분)

Testing

  • bazel build //..., bazel test //systems/... 전체 통과, 다른 시스템 영향 없음 확인
  • JwtAuthInterceptor 서명/만료/role 검증 단위 테스트 포함
  • Manual verification — 로컬 Redis 필요, 미실행

Deployment Notes

  • Rollout 시 spring.data.redis.*, auth.jwt.secret/issuer, monitor.services.*.base-url, monitor.round.*, monitor.report.storage-dir 환경변수 설정 필요 (dev 기본값은 application.yaml에 있음)

Checklist

  • API 단위로 커밋 분리 (전체 13개 커밋, PR 4개로 분할)
  • 코드 리뷰

tlgms and others added 3 commits August 9, 2026 22:23
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>
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
📝 Walkthrough

대시보드 스냅샷을 XLSX·CSV 리포트로 다운로드하고, 실시간 지표와 클라이언트 로그를 SSE로 구독할 수 있습니다. 관제 API에는 ADMIN Bearer JWT 인증이 적용됩니다.

주요 변경

아키텍처

  • GenerateReportUseCase, ReportGeneratorPort, ReportObjectStoragePort를 추가했습니다.
  • 로컬 파일 시스템과 Redis를 사용하는 리포트 저장 어댑터를 추가했습니다.
  • 리포트 토큰은 5분 후 만료됩니다.
  • ReportService가 대시보드 스냅샷을 동기적으로 생성합니다.
  • /api/monitor/v11/reports에서 리포트를 생성합니다.
  • /api/monitor/v11/reports/download에서 토큰 기반 파일을 다운로드합니다.
  • /api/monitor/v11/stream에서 SSE 스트림을 제공합니다.
  • SSE는 스냅샷, 트래픽, API, 비즈니스, 서비스, 리소스, 핑, 로그 이벤트를 전달합니다.
  • 클라이언트 IP별 SSE 연결을 최대 3개로 제한합니다.
  • JwtAuthInterceptor가 서명, issuer, 만료 시간, role=ADMIN을 검증합니다.
  • collect/session, collect/client-log 경로는 인증에서 제외합니다.
  • Spring Scheduling을 활성화했습니다.

위험 영역

  • SSE 팬아웃과 연결 제한 카운터는 단일 인스턴스 인메모리 방식입니다.
  • 서버 API 오류 로그와 API 요청 집계는 외부 보고 연동 없이 조회 API만 제공합니다.
  • X-Forwarded-For 사용 환경에서는 프록시가 전달하는 IP 값의 신뢰 경계를 확인해야 합니다.
  • 리포트 파일은 로컬 디스크에 저장되므로 디스크 용량과 정리 정책을 운영 환경에서 관리해야 합니다.
  • Redis 장애 시 리포트 토큰 조회와 다운로드가 실패할 수 있습니다.

마이그레이션 및 호환성

  • 관제 API 호출자는 ADMIN 권한의 유효한 Bearer JWT를 전송해야 합니다.
  • auth.jwt.secretauth.jwt.issuer 설정이 필요합니다.
  • 리포트 기능은 Redis와 리포트 저장 디렉터리가 필요합니다.
  • 다중 인스턴스 배포 전에는 SSE 브로커와 분산 연결 제한 저장소가 필요합니다.
  • Apache POI 및 POI OOXML 의존성을 추가했습니다.

검증 및 롤아웃

  • 전체 빌드 및 테스트 통과
  • JWT 관리자 토큰 허용 검증
  • 토큰 누락 검증
  • 비관리자 role 거부 검증
  • 만료 토큰 검증
  • 잘못된 서명 검증
  • ReportService 리포트 생성 테스트
  • 로컬 Redis 기반 수동 검증
  • 운영 환경의 저장 디렉터리 권한과 디스크 용량 확인
  • 프록시의 X-Forwarded-For 설정 확인
  • 다중 인스턴스 배포 전 SSE 및 레이트리밋 분산화 검토

Walkthrough

관측성 모듈에 CSV·XLSX 리포트 생성 및 다운로드 API, SSE 기반 실시간 로그 스트림, 관리자 JWT 인증을 추가했습니다. Redis 기반 파일 토큰 저장과 Spring Scheduling 설정도 추가했습니다.

Changes

관측성 기능 확장

Layer / File(s) Summary
리포트 계약과 응답 모델
systems/observability/observability-application/..., systems/observability/observability-adapter-in/.../dto/response/*
리포트 생성·저장 포트와 결과 모델을 추가했습니다. 생성 결과를 HTTP 응답 DTO로 변환합니다.
리포트 생성과 다운로드 흐름
systems/observability/observability-application/.../ReportService.kt, systems/observability/observability-adapter-out/.../report/*, systems/observability/observability-adapter-in/.../controller/*, systems/observability/.../ReportServiceTest.kt
대시보드 스냅샷을 CSV 또는 XLSX로 변환하고, 파일과 Redis 토큰을 저장합니다. 생성 결과와 다운로드 파일을 API로 반환합니다. 관련 테스트를 추가했습니다.
SSE 스트림과 실시간 로그
systems/observability/observability-adapter-in/.../web/sse/*, systems/observability/.../MonitorStreamController.kt, systems/observability/observability-application/.../ClientLogCollectionService.kt, systems/observability/observability-bootstrap/.../ExampleApplication.kt
SSE emitter를 관리하고 스냅샷, 로그 이벤트, ping을 전송합니다. IP별 연결을 최대 3개로 제한합니다. 클라이언트 로그 저장 후 실시간 이벤트를 발행합니다.
관리자 JWT 인증
systems/observability/observability-adapter-in/.../web/security/*, systems/observability/.../JwtAuthInterceptorTest.kt
/api/monitor/v11/** 경로에 관리자 JWT 인증을 적용합니다. 누락, 만료, 서명 오류 및 비관리자 role을 검증하는 테스트를 추가했습니다.

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: 생성 결과 반환
Loading

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 이벤트 전송
Loading

Suggested labels: feature, kotlin, bazel, security, test

🚥 Pre-merge checks | ✅ 6 | ❌ 4

❌ Failed checks (4 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning 이슈 #29는 Entity, Value Object, Domain Service를 포함한 도메인 계층 구현을 요구하지만 PR은 해당 목표를 충족하지 않습니다. 이슈 #29의 Entity, Value Object, Domain Service 구현을 추가하거나, 애플리케이션 및 인프라 구현을 별도 이슈로 분리하십시오.
Out of Scope Changes check ⚠️ Warning 애플리케이션, 어댑터, 보안, 부트스트랩 계층 변경은 도메인 계층만 허용한 이슈 #29의 범위를 벗어납니다. PR을 observability 도메인 계층 변경으로 제한하고, 리포트·SSE·JWT 및 인프라 변경은 별도 이슈와 PR로 분리하십시오.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 85.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Behavior Change Needs Tests ⚠️ Warning PR 범위에서 운영 Kotlin 23개가 변경됐지만 테스트 변경은 5개입니다. SSE·보고서 컨트롤러와 adapter-out의 XLSX/CSV·Redis 저장소는 테스트 참조가 없습니다. 설명에도 테스트 불필요 사유가 없습니다. adapter-in의 SSE·보고서 컨트롤러와 adapter-out의 XLSX/CSV 생성기·Redis 저장소에 정상, 오류, 경계 조건 테스트를 추가하십시오.
✅ Passed checks (6 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 Conventional Commit 형식이며 리포트 다운로드, SSE 스트림, JWT 인증 변경을 명확히 설명하고 이슈 #29를 포함합니다.
Description check ✅ Passed 설명이 리포트 다운로드, SSE 스트림, JWT 인증 구현과 테스트 및 제약 사항을 변경 내용과 일치하게 설명합니다.
Kotlin Layer Boundary ✅ Passed 도메인 모듈에 adapter/bootstrap 또는 Spring import가 없습니다. 애플리케이션의 유일한 프레임워크 직접 사용은 기존 패턴과 같은 Spring @Service이며 DI 목적과 starter 의존성으로 정당화됩니다.
Go Error Context ✅ Passed 변경 파일과 observability 영역에 Go 코드가 없으며, 저장소 전체에도 .go 파일이 확인되지 않아 이 검사는 적용되지 않습니다.
Bazel Formatting ✅ Passed 변경된 deps.bzl은 저장소의 buildifier 형식과 일치하며 공백 오류가 없습니다. 의존성 라벨은 존재하는 :main 타깃을 사용합니다.
Todo Must Reference Issue ✅ Passed PR 변경 범위(HEAD~3..HEAD)의 추가 라인에 TODO 또는 FIXME 주석이 없습니다.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/29-observability-도메인-개발
  • 🛠️ cleanup stale imports
  • 🛠️ harden error messages
  • 🛠️ test clarity pass

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 5ce0b76 and 4ccbb43.

📒 Files selected for processing (30)
  • systems/observability/observability-adapter-in/deps.bzl
  • 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/controller/ReportController.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/ReportDownloadController.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/common/ResponseMapper.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/response/LiveLogEventResponse.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/response/ReportGeneratedResponse.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthInterceptor.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthProperties.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/WebMvcConfig.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseBroadcaster.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseConnectionLimiter.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseLiveLogPublisher.kt
  • systems/observability/observability-adapter-in/src/test/kotlin/hs/kr/entrydsm/TestMain.kt
  • systems/observability/observability-adapter-in/src/test/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthInterceptorTest.kt
  • systems/observability/observability-adapter-out/deps.bzl
  • systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/LocalFileReportObjectStorageAdapter.kt
  • systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/XlsxCsvReportGenerator.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionService.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ReportService.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/GenerateReportUseCase.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/GetDashboardSnapshotUseCase.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/result/ReportResult.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/LiveLogPublisherPort.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/ReportGeneratorPort.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/ReportObjectStoragePort.kt
  • systems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/TestMain.kt
  • systems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionServiceTest.kt
  • systems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/ReportServiceTest.kt
  • systems/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.bzl
  • systems/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 name argument 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.bzl
  • systems/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.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/GenerateReportUseCase.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthProperties.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/ReportGeneratorPort.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/response/ReportGeneratedResponse.kt
  • systems/observability/observability-adapter-in/src/test/kotlin/hs/kr/entrydsm/TestMain.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/response/LiveLogEventResponse.kt
  • systems/observability/observability-adapter-in/src/test/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthInterceptorTest.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/WebMvcConfig.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/result/ReportResult.kt
  • systems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/ReportServiceTest.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/ReportDownloadController.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/ReportObjectStoragePort.kt
  • systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/XlsxCsvReportGenerator.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/MonitorStreamController.kt
  • systems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/TestMain.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseLiveLogPublisher.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseConnectionLimiter.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/GetDashboardSnapshotUseCase.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/LiveLogPublisherPort.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseBroadcaster.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/ReportController.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionService.kt
  • systems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionServiceTest.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ReportService.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthInterceptor.kt
  • systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/LocalFileReportObjectStorageAdapter.kt
  • systems/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 #123 or a full tracker key like PROJ-123

Files:

  • systems/observability/observability-bootstrap/src/main/kotlin/hs/kr/entrydsm/ExampleApplication.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/GenerateReportUseCase.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthProperties.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/ReportGeneratorPort.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/response/ReportGeneratedResponse.kt
  • systems/observability/observability-adapter-in/src/test/kotlin/hs/kr/entrydsm/TestMain.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/response/LiveLogEventResponse.kt
  • systems/observability/observability-adapter-in/src/test/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthInterceptorTest.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/WebMvcConfig.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/result/ReportResult.kt
  • systems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/ReportServiceTest.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/ReportDownloadController.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/ReportObjectStoragePort.kt
  • systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/XlsxCsvReportGenerator.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/MonitorStreamController.kt
  • systems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/TestMain.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseLiveLogPublisher.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseConnectionLimiter.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/GetDashboardSnapshotUseCase.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/LiveLogPublisherPort.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseBroadcaster.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/ReportController.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionService.kt
  • systems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionServiceTest.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ReportService.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthInterceptor.kt
  • systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/LocalFileReportObjectStorageAdapter.kt
  • systems/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.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/GenerateReportUseCase.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthProperties.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/ReportGeneratorPort.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/response/ReportGeneratedResponse.kt
  • systems/observability/observability-adapter-in/src/test/kotlin/hs/kr/entrydsm/TestMain.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/response/LiveLogEventResponse.kt
  • systems/observability/observability-adapter-in/src/test/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthInterceptorTest.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/WebMvcConfig.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/result/ReportResult.kt
  • systems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/ReportServiceTest.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/ReportDownloadController.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/ReportObjectStoragePort.kt
  • systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/XlsxCsvReportGenerator.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/MonitorStreamController.kt
  • systems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/TestMain.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseLiveLogPublisher.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseConnectionLimiter.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/GetDashboardSnapshotUseCase.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/LiveLogPublisherPort.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/sse/SseBroadcaster.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/controller/ReportController.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionService.kt
  • systems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionServiceTest.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ReportService.kt
  • systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/security/JwtAuthInterceptor.kt
  • systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/report/LocalFileReportObjectStorageAdapter.kt
  • systems/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.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/ReportGeneratorPort.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/result/ReportResult.kt
  • systems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/ReportServiceTest.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/ReportObjectStoragePort.kt
  • systems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/TestMain.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/GetDashboardSnapshotUseCase.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/LiveLogPublisherPort.kt
  • systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionService.kt
  • systems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionServiceTest.kt
  • systems/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!

Comment on lines +26 to +28
emitter.onCompletion { connectionLimiter.release(clientKey) }
emitter.onTimeout { connectionLimiter.release(clientKey) }
emitter.onError { connectionLimiter.release(clientKey) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 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' || true

Repository: 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' || true

Repository: 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:


🏁 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.")
PY

Repository: 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.")
PY

Repository: 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 종료 시 카운터가 정확히 한 번 감소하는 테스트를 추가하십시오.

Comment on lines +33 to +38
private fun clientIp(request: HttpServletRequest): String =
request.getHeader("X-Forwarded-For")
?.substringBefore(",")
?.trim()
?.takeIf { it.isNotBlank() }
?: request.remoteAddr

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 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
done

Repository: 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
done

Repository: 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']}")
PY

Repository: 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
PY

Repository: EntryDSM/entrydsm-platform

Length of output: 3748


신뢰된 프록시가 정규화한 IP만 제한 키로 사용하십시오.

현재 구현은 요청 경로와 관계없이 X-Forwarded-For의 첫 값을 사용합니다. 애플리케이션이 직접 노출되거나 프록시가 헤더를 덮어쓰지 않으면 공격자는 값을 변경하여 SseConnectionLimitersession:·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.

Comment on lines +18 to +25
@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())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 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.

Suggested change
@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.

Comment on lines +13 to +14
lateinit var secret: String
lateinit var issuer: String

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 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}")
PY

Repository: 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/observability

Repository: 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:


기본 JWT 시크릿으로 애플리케이션이 시작되지 않게 하세요.

application.yamlJWT_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

Comment on lines +66 to +69
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) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 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*' | sort

Repository: 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$' || true

Repository: 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}")
PY

Repository: 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.forEachSseEmitter.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.

Comment on lines +13 to +64
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()
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 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

Comment on lines +41 to +45
private fun toCsv(snapshot: DashboardSnapshotResult): String =
buildString {
appendLine("metric,value")
rows(snapshot).forEach { (key, value) -> appendLine("$key,$value") }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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.

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

Comment on lines +41 to +42
clientLogStorePort.record(input)
liveLogPublisherPort.publishClientLog(input)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 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-L33
  • systems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/ClientLogCollectionServiceTest.kt#L45-L45
  • systems/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

Comment on lines +14 to +18
import org.springframework.stereotype.Service

/** ponytail: 데이터량이 적어 동기 생성만 지원한다(202 GENERATING/폴링 큐 없음). 느려지면 잡 큐로 교체. */
@Service
class ReportService(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 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/observability

Repository: 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/observability

Repository: 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 || true

Repository: 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/observability

Repository: 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

Comment on lines +30 to +32
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ 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는 전달받은 토큰별 고유 경로에 저장하도록 수정해 기존 토큰이 새 리포트 객체를
참조하지 않게 하세요.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant