feat(observability): 모니터링 API 공용 기반 + Redis 인프라 #29 - #69
Conversation
monitoring-observability.md 명세에 맞춰 observability 시스템의 domain enum/exception/model과 adapter-in 공용 응답/에러 처리(ApiResponse, ErrorDetail, ErrorResponse, GlobalExceptionHandler)를 identity 시스템 컨벤션에 맞춰 구성. Redis/JWT/xlsx(POI) 의존성을 kotlin.MODULE.bazel에 추가. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
세션·트래픽 집계(동시접속자, 총방문자, 체류시간, 기기분포)를 위한 SessionStorePort/RedisSessionStoreAdapter와 IP 기준 rate limit을 위한 RateLimitPort/RedisRateLimitAdapter를 추가. collect/session API의 기반. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
📝 Walkthrough사용자 영향: 모니터링 API의 도메인·세션 집계·공통 오류 처리 기반을 추가했습니다. 실제 API 엔드포인트는 아직 제공하지 않습니다. 주요 변경 사항
위험 영역
마이그레이션 및 호환성
검증 및 롤아웃
WalkthroughChangesObservability 핵심 기능
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant SessionCollectionService
participant RedisRateLimitAdapter
participant RedisSessionStoreAdapter
Client->>SessionCollectionService: 세션 이벤트 전송
SessionCollectionService->>RedisRateLimitAdapter: IP rate limit 확인
RedisRateLimitAdapter-->>SessionCollectionService: 허용 여부 반환
SessionCollectionService->>RedisSessionStoreAdapter: 세션 enter, heartbeat, leave 처리
RedisSessionStoreAdapter-->>SessionCollectionService: 저장 결과 반환
SessionCollectionService-->>Client: 세션 ID와 heartbeat 간격 반환
Suggested labels: 🚥 Pre-merge checks | ✅ 8 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (8 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: 11
🤖 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/test/kotlin/hs/kr/entrydsm/observability/adapterin/web/exception/GlobalExceptionHandlerTest.kt`:
- Around line 45-55: Update
keepsGenericResponseAndLogsCorrelationContextForUnhandledException to capture
logger.error output while invoking handleUnhandledException, then assert an
error event was emitted containing the correlation context
X-trace-Id=test-trace-id. Preserve the existing generic response assertions and
MDC cleanup.
In
`@systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/redis/RedisRateLimitAdapter.kt`:
- Around line 16-19: RedisRateLimitAdapter의 increment 및 expire 호출을 Lua 스크립트로 통합해
INCR 결과가 1일 때 EXPIRE가 동일한 원자적 작업으로 실행되도록 수정하세요. 기존 windowSeconds와 redisKey 값을
스크립트 인자로 전달하고, 실패 시 TTL 없는 키가 남지 않는 경로를 검증하는 테스트를 추가하세요.
In
`@systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/redis/RedisSessionStoreAdapter.kt`:
- Around line 41-44: RedisSessionStoreAdapter의 heartbeat()에서 저장된 FIELD_SERVICE를
먼저 조회해 요청 service와 다르면 실패시키고 기존 서비스 값이나 ZSET을 변경하지 않도록 하세요. 서비스가 일치할 때만
FIELD_LAST_HEARTBEAT_AT, TTL, touchWindow()를 갱신하고 저장된 서비스 기준으로 window를 처리하세요. 이
서비스 소속 변경 차단 동작을 검증하는 Kotlin 테스트를 추가하세요.
- Around line 83-86: Update the CONCURRENT_MAX_KEY handling in sampleConcurrency
to perform the read, comparison, and conditional write as one atomic Redis
operation, such as a Lua script, so concurrent calls cannot overwrite a larger
maximum with a smaller value. Preserve the existing behavior of storing only
when current exceeds the recorded maximum.
- Around line 37-45: Make the session lifecycle transition atomic across
RedisSessionStoreAdapter. In heartbeat(), conditionally update metadata, TTL,
and the activity window only if the session still exists; in leave(), atomically
verify and remove the session while preventing concurrent heartbeats or
duplicate leaves from recreating or double-counting it, using Lua or
WATCH/MULTI/EXEC. Add deterministic concurrency tests in the same subsystem
covering heartbeat-after-leave and two simultaneous leave requests; update the
anchor file at lines 37-45 and sibling location at lines 48-59 as required.
In `@systems/observability/observability-application/deps.bzl`:
- Around line 1-4: Remove the Spring `@Service` annotation from
SessionCollectionService and move its bean registration into the
observability-bootstrap configuration. Then remove
`@maven//`:org_springframework_boot_spring_boot_starter from the application
KOTLIN_DEPS, leaving the domain dependency intact.
In
`@systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/SessionCollectionService.kt`:
- Around line 15-22: SessionCollectionService의 애플리케이션 계층 Spring 의존성을 제거하세요.
클래스에서 `@Service` 어노테이션과 org.springframework.stereotype.Service import를 삭제하고, 해당 빈
등록은 observability-bootstrap 설정에서 유지하세요.
In
`@systems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/SessionCollectionServiceTest.kt`:
- Around line 30-46: Add deterministic tests for the normal HEARTBEAT and LEAVE
flows in SessionCollectionServiceTest. Capture the session ID returned by ENTER,
assert HEARTBEAT succeeds for that ID, and add a state-transition test that
performs ENTER then LEAVE and verifies a subsequent HEARTBEAT fails with
SESSION_NOT_FOUND using a meaningful exception assertion.
In
`@systems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/TestMain.kt`:
- Around line 3-8: TestMain.kt의 suite 선언에서 사용하는 SessionCollectionServiceTest를
hs.kr.entrydsm.observability.application 패키지에서 명시적으로 import하도록 추가하세요.
In
`@systems/observability/observability-bootstrap/src/main/kotlin/hs/kr/entrydsm/observability/config/ClockConfig.kt`:
- Around line 7-10: ClockConfig의 clock()이 UTC를 반환하는지 검증하는
observability-bootstrap 단위 테스트를 추가하세요. ClockConfig().clock().zone을 확인해 UTC와
일치하는지 단언하고, 해당 테스트가 같은 subsystem의 기존 테스트 관례를 따르도록 구성하세요.
In
`@systems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/model/Session.kt`:
- Around line 12-16: Session의 새 동작을 검증하는 SessionTest를 추가하십시오. heartbeat 테스트에서는
service와 lastHeartbeatAt이 갱신되고 나머지 필드는 보존되는지 확인하십시오. isExpired 테스트에서는 만료 직전에는
false, 정확한 만료 시각에는 false, 만료 직후에는 true가 되는 경계 조건을 검증하십시오.
🪄 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: 5653b288-5d7f-47dd-972d-3b8d479af503
⛔ Files ignored due to path filters (1)
kotlin.MODULE.bazelis excluded by none and included by none
📒 Files selected for processing (43)
systems/observability/observability-adapter-in/BUILD.bazelsystems/observability/observability-adapter-in/deps.bzlsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/common/ApiResponse.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/common/ErrorDetail.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/common/ErrorResponse.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/exception/GlobalExceptionHandler.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/exception/GlobalExceptionHandlerTest.ktsystems/observability/observability-adapter-out/BUILD.bazelsystems/observability/observability-adapter-out/deps.bzlsystems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/redis/RedisRateLimitAdapter.ktsystems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/redis/RedisSessionStoreAdapter.ktsystems/observability/observability-application/BUILD.bazelsystems/observability/observability-application/deps.bzlsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/SessionCollectionService.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/RecordSessionEventUseCase.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/result/SessionEventResult.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/RateLimitPort.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/SessionStorePort.ktsystems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/TestMain.ktsystems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/SessionCollectionServiceTest.ktsystems/observability/observability-bootstrap/src/main/kotlin/hs/kr/entrydsm/observability/config/ClockConfig.ktsystems/observability/observability-domain/BUILD.bazelsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/DeviceType.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ErrorCode.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/LogLevel.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/LogSource.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/MetricType.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ReportFormat.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ReportStatus.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ServiceName.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ServiceStatus.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/SessionEventType.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/exception/MonitorDomainException.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/exception/MonitorException.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/model/Cursor.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/model/Session.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/service/DeviceTypeParser.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/service/HealthStatusClassifier.ktsystems/observability/observability-domain/src/test/kotlin/hs/kr/entrydsm/TestMain.ktsystems/observability/observability-domain/src/test/kotlin/hs/kr/entrydsm/observability/domain/CursorTest.ktsystems/observability/observability-domain/src/test/kotlin/hs/kr/entrydsm/observability/domain/DeviceTypeParserTest.ktsystems/observability/observability-domain/src/test/kotlin/hs/kr/entrydsm/observability/domain/HealthStatusClassifierTest.kt
📜 Review details
🧰 Additional context used
📓 Path-based instructions (8)
**/{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-application/deps.bzlsystems/observability/observability-adapter-in/deps.bzlsystems/observability/observability-adapter-out/deps.bzlsystems/observability/observability-adapter-in/BUILD.bazelsystems/observability/observability-adapter-out/BUILD.bazelsystems/observability/observability-domain/BUILD.bazelsystems/observability/observability-application/BUILD.bazel
**/*.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-application/deps.bzlsystems/observability/observability-adapter-in/deps.bzlsystems/observability/observability-adapter-out/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-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/MetricType.ktsystems/observability/observability-bootstrap/src/main/kotlin/hs/kr/entrydsm/observability/config/ClockConfig.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/RateLimitPort.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/RecordSessionEventUseCase.ktsystems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/TestMain.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ServiceStatus.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/common/ApiResponse.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ReportStatus.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/LogLevel.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/result/SessionEventResult.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ServiceName.ktsystems/observability/observability-domain/src/test/kotlin/hs/kr/entrydsm/observability/domain/CursorTest.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/model/Cursor.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/SessionEventType.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ReportFormat.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/LogSource.ktsystems/observability/observability-domain/src/test/kotlin/hs/kr/entrydsm/observability/domain/DeviceTypeParserTest.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/service/HealthStatusClassifier.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/exception/MonitorException.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/common/ErrorDetail.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/DeviceType.ktsystems/observability/observability-domain/src/test/kotlin/hs/kr/entrydsm/TestMain.ktsystems/observability/observability-adapter-in/src/test/kotlin/hs/kr/entrydsm/observability/adapterin/web/exception/GlobalExceptionHandlerTest.ktsystems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/SessionCollectionServiceTest.ktsystems/observability/observability-adapter-in/src/test/kotlin/hs/kr/entrydsm/TestMain.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ErrorCode.ktsystems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/redis/RedisRateLimitAdapter.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/exception/MonitorDomainException.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/SessionStorePort.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/common/ErrorResponse.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/service/DeviceTypeParser.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/SessionCollectionService.ktsystems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/redis/RedisSessionStoreAdapter.ktsystems/observability/observability-domain/src/test/kotlin/hs/kr/entrydsm/observability/domain/HealthStatusClassifierTest.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/model/Session.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/exception/GlobalExceptionHandler.kt
**/*-domain/**/*.{java,kt,scala,groovy}
📄 CodeRabbit inference engine (Custom checks)
For files under *-domain modules, fail if imports reference adapter or bootstrap packages
Files:
systems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/MetricType.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ServiceStatus.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ReportStatus.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/LogLevel.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ServiceName.ktsystems/observability/observability-domain/src/test/kotlin/hs/kr/entrydsm/observability/domain/CursorTest.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/model/Cursor.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/SessionEventType.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ReportFormat.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/LogSource.ktsystems/observability/observability-domain/src/test/kotlin/hs/kr/entrydsm/observability/domain/DeviceTypeParserTest.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/service/HealthStatusClassifier.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/exception/MonitorException.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/DeviceType.ktsystems/observability/observability-domain/src/test/kotlin/hs/kr/entrydsm/TestMain.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ErrorCode.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/exception/MonitorDomainException.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/service/DeviceTypeParser.ktsystems/observability/observability-domain/src/test/kotlin/hs/kr/entrydsm/observability/domain/HealthStatusClassifierTest.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/model/Session.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-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/MetricType.ktsystems/observability/observability-bootstrap/src/main/kotlin/hs/kr/entrydsm/observability/config/ClockConfig.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/RateLimitPort.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/RecordSessionEventUseCase.ktsystems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/TestMain.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ServiceStatus.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/common/ApiResponse.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ReportStatus.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/LogLevel.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/result/SessionEventResult.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ServiceName.ktsystems/observability/observability-domain/src/test/kotlin/hs/kr/entrydsm/observability/domain/CursorTest.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/model/Cursor.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/SessionEventType.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ReportFormat.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/LogSource.ktsystems/observability/observability-domain/src/test/kotlin/hs/kr/entrydsm/observability/domain/DeviceTypeParserTest.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/service/HealthStatusClassifier.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/exception/MonitorException.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/common/ErrorDetail.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/DeviceType.ktsystems/observability/observability-domain/src/test/kotlin/hs/kr/entrydsm/TestMain.ktsystems/observability/observability-adapter-in/src/test/kotlin/hs/kr/entrydsm/observability/adapterin/web/exception/GlobalExceptionHandlerTest.ktsystems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/SessionCollectionServiceTest.ktsystems/observability/observability-adapter-in/src/test/kotlin/hs/kr/entrydsm/TestMain.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ErrorCode.ktsystems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/redis/RedisRateLimitAdapter.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/exception/MonitorDomainException.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/SessionStorePort.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/common/ErrorResponse.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/service/DeviceTypeParser.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/SessionCollectionService.ktsystems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/redis/RedisSessionStoreAdapter.ktsystems/observability/observability-domain/src/test/kotlin/hs/kr/entrydsm/observability/domain/HealthStatusClassifierTest.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/model/Session.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/exception/GlobalExceptionHandler.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-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/MetricType.ktsystems/observability/observability-bootstrap/src/main/kotlin/hs/kr/entrydsm/observability/config/ClockConfig.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/RateLimitPort.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/RecordSessionEventUseCase.ktsystems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/TestMain.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ServiceStatus.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/common/ApiResponse.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ReportStatus.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/LogLevel.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/result/SessionEventResult.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ServiceName.ktsystems/observability/observability-domain/src/test/kotlin/hs/kr/entrydsm/observability/domain/CursorTest.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/model/Cursor.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/SessionEventType.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ReportFormat.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/LogSource.ktsystems/observability/observability-domain/src/test/kotlin/hs/kr/entrydsm/observability/domain/DeviceTypeParserTest.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/service/HealthStatusClassifier.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/exception/MonitorException.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/common/ErrorDetail.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/DeviceType.ktsystems/observability/observability-domain/src/test/kotlin/hs/kr/entrydsm/TestMain.ktsystems/observability/observability-adapter-in/src/test/kotlin/hs/kr/entrydsm/observability/adapterin/web/exception/GlobalExceptionHandlerTest.ktsystems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/SessionCollectionServiceTest.ktsystems/observability/observability-adapter-in/src/test/kotlin/hs/kr/entrydsm/TestMain.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ErrorCode.ktsystems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/redis/RedisRateLimitAdapter.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/exception/MonitorDomainException.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/SessionStorePort.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/common/ErrorResponse.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/service/DeviceTypeParser.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/SessionCollectionService.ktsystems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/redis/RedisSessionStoreAdapter.ktsystems/observability/observability-domain/src/test/kotlin/hs/kr/entrydsm/observability/domain/HealthStatusClassifierTest.ktsystems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/model/Session.ktsystems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/exception/GlobalExceptionHandler.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/out/RateLimitPort.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/RecordSessionEventUseCase.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/result/SessionEventResult.ktsystems/observability/observability-application/src/test/kotlin/hs/kr/entrydsm/observability/application/SessionCollectionServiceTest.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/SessionStorePort.ktsystems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/SessionCollectionService.kt
**/BUILD.bazel
⚙️ CodeRabbit configuration file
**/BUILD.bazel: Apply Bazel BUILD style guidance.Core rules:
- BUILD formatting must match buildifier output.
- Prefer DAMP BUILD files over over-abstracted DRY patterns.
- Keep top-level layout clear: load() first, then package/default visibility, then targets.
Target definitions:
- Keep deps explicit and close to each target's real direct dependencies.
- Avoid recursive globs unless there is a clear, documented reason.
- Avoid top-level list comprehensions for generating many targets.
- Prefer literal labels and stable naming for readability and tooling compatibility.
- Use boolean values (True/False), not numeric stand-ins.
Maintenance:
- Flag duplicated target logic that should be moved into a macro.
- Flag macro usage that hides important dependency or visibility decisions.
Files:
systems/observability/observability-adapter-in/BUILD.bazelsystems/observability/observability-adapter-out/BUILD.bazelsystems/observability/observability-domain/BUILD.bazelsystems/observability/observability-application/BUILD.bazel
🔇 Additional comments (35)
systems/observability/observability-adapter-in/deps.bzl (1)
1-11: LGTM!systems/observability/observability-adapter-in/BUILD.bazel (1)
20-20: LGTM!systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/common/ApiResponse.kt (1)
1-7: LGTM!systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/common/ErrorDetail.kt (1)
1-17: LGTM!systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/dto/common/ErrorResponse.kt (1)
1-9: LGTM!systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/exception/GlobalExceptionHandler.kt (1)
22-62: LGTM!systems/observability/observability-adapter-in/src/test/kotlin/hs/kr/entrydsm/TestMain.kt (1)
3-11: LGTM!systems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/DeviceType.kt (1)
1-8: LGTM!systems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ErrorCode.kt (1)
3-30: LGTM!systems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/LogLevel.kt (1)
1-6: LGTM!systems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/LogSource.kt (1)
1-8: LGTM!systems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/MetricType.kt (1)
1-6: LGTM!systems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/model/Cursor.kt (1)
7-31: LGTM!systems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/service/HealthStatusClassifier.kt (1)
5-28: LGTM!systems/observability/observability-domain/BUILD.bazel (1)
20-20: LGTM!systems/observability/observability-domain/src/test/kotlin/hs/kr/entrydsm/TestMain.kt (1)
3-12: LGTM!systems/observability/observability-domain/src/test/kotlin/hs/kr/entrydsm/observability/domain/CursorTest.kt (1)
9-25: LGTM!systems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ReportFormat.kt (1)
1-6: LGTM!systems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ReportStatus.kt (1)
1-6: LGTM!systems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ServiceName.kt (1)
1-11: LGTM!systems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/ServiceStatus.kt (1)
1-7: LGTM!systems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/enum/SessionEventType.kt (1)
1-7: LGTM!systems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/exception/MonitorDomainException.kt (1)
1-7: LGTM!systems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/exception/MonitorException.kt (1)
1-10: LGTM!systems/observability/observability-domain/src/test/kotlin/hs/kr/entrydsm/observability/domain/HealthStatusClassifierTest.kt (1)
8-56: LGTM!systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/result/SessionEventResult.kt (1)
1-6: LGTM!systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/SessionCollectionService.kt (1)
24-65: LGTM!systems/observability/observability-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/service/DeviceTypeParser.kt (1)
1-16: LGTM!systems/observability/observability-domain/src/test/kotlin/hs/kr/entrydsm/observability/domain/DeviceTypeParserTest.kt (1)
8-29: LGTM!systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/in/RecordSessionEventUseCase.kt (1)
1-15: LGTM!systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/RateLimitPort.kt (1)
1-6: LGTM!systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/SessionStorePort.kt (1)
1-30: LGTM!systems/observability/observability-application/BUILD.bazel (1)
14-21: LGTM!systems/observability/observability-adapter-out/deps.bzl (1)
1-11: LGTM!systems/observability/observability-adapter-out/BUILD.bazel (1)
14-21: LGTM!
| fun keepsGenericResponseAndLogsCorrelationContextForUnhandledException() { | ||
| MDC.put("X-trace-Id", "test-trace-id") | ||
| try { | ||
| val response = handler.handleUnhandledException(IllegalStateException("internal detail")) | ||
|
|
||
| assertEquals(500, response.statusCode.value()) | ||
| assertEquals("INTERNAL_SERVER_ERROR", response.body?.error?.code) | ||
| assertEquals(ErrorCode.INTERNAL_SERVER_ERROR.message, response.body?.error?.message) | ||
| } finally { | ||
| MDC.remove("X-trace-Id") | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 3 --glob '*.kt' 'ListAppender|OutputCapture|MDC|LoggerFactory' systems/observability
rg -n -C 3 --glob 'BUILD.bazel' --glob '*.bzl' 'logback|slf4j|spring_boot_starter_test' systems/observabilityRepository: EntryDSM/entrydsm-platform
Length of output: 8638
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- test outline ---'
ast-grep outline systems/observability/observability-adapter-in/src/test/kotlin/hs/kr/entrydsm/observability/adapterin/web/exception/GlobalExceptionHandlerTest.kt
printf '%s\n' '--- implementation ---'
cat -n systems/observability/observability-adapter-in/src/main/kotlin/hs/kr/entrydsm/observability/adapterin/web/exception/GlobalExceptionHandler.kt
printf '%s\n' '--- test ---'
cat -n systems/observability/observability-adapter-in/src/test/kotlin/hs/kr/entrydsm/observability/adapterin/web/exception/GlobalExceptionHandlerTest.kt
printf '%s\n' '--- module/build files ---'
fd -i 'BUILD|BUILD.bazel|deps.bzl|.*logback.*|.*logging.*' systems/observability
printf '%s\n' '--- relevant test patterns ---'
rg -n -C 4 --glob '*.kt' --glob '*.java' \
'ListAppender|OutputCapture|Appender|ILoggingEvent|assert.*log|LoggingEvent|TestAppender|logCaptor|LogCaptor' .Repository: EntryDSM/entrydsm-platform
Length of output: 6967
로그 상관관계를 단언하세요.
keepsGenericResponseAndLogsCorrelationContextForUnhandledException는 응답만 검증합니다. 로그 캡처 도구를 사용해 logger.error 이벤트와 X-trace-Id=test-trace-id를 단언하세요.
🤖 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/test/kotlin/hs/kr/entrydsm/observability/adapterin/web/exception/GlobalExceptionHandlerTest.kt`
around lines 45 - 55, Update
keepsGenericResponseAndLogsCorrelationContextForUnhandledException to capture
logger.error output while invoking handleUnhandledException, then assert an
error event was emitted containing the correlation context
X-trace-Id=test-trace-id. Preserve the existing generic response assertions and
MDC cleanup.
Source: Path instructions
| val count = redis.opsForValue().increment(redisKey) ?: 1L | ||
| if (count == 1L) { | ||
| redis.expire(redisKey, Duration.ofSeconds(windowSeconds)) | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- candidate file ---'
ast-grep outline systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/redis/RedisRateLimitAdapter.kt
cat -n systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/redis/RedisRateLimitAdapter.kt
printf '%s\n' '--- related symbols and tests ---'
rg -n --glob '*.kt' 'RedisRateLimitAdapter|tryAcquire|RateLimit|expire\\(|opsForValue\\(\\)\\.increment' systemsRepository: EntryDSM/entrydsm-platform
Length of output: 1567
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- related symbols ---'
rg -n --glob '*.kt' 'RedisRateLimitAdapter|tryAcquire|RateLimit|expire\(|opsForValue\(\)\.increment' systems || true
printf '%s\n' '--- candidate-module files ---'
git ls-files 'systems/observability/observability-adapter-out' | sed -n '1,160p'
printf '%s\n' '--- build declarations mentioning Redis ---'
rg -n --glob 'build.gradle*' --glob '*.gradle.kts' --glob '*.gradle' 'redis|spring-data' . || true
printf '%s\n' '--- Redis adapter tests ---'
rg -n --glob '*Test.kt' --glob '*Tests.kt' 'RateLimit|RedisRateLimit|tryAcquire|ratelimit' . || trueRepository: EntryDSM/entrydsm-platform
Length of output: 7383
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- rate-limit callers and constants ---'
cat -n systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/SessionCollectionService.kt
cat -n systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/RateLimitPort.kt
cat -n systems/observability/observability-adapter-out/BUILD.bazel
cat -n systems/observability/observability-adapter-out/src/test/kotlin/hs/kr/entrydsm/TestMain.kt
printf '%s\n' '--- read-only behavioral model of the two-command sequence ---'
python3 - <<'PY'
class RedisModel:
def __init__(self):
self.values = {}
self.expiry = {}
def incr(self, key):
self.values[key] = self.values.get(key, 0) + 1
return self.values[key]
def expire(self, key, seconds):
self.expiry[key] = seconds
def try_acquire_with_failure(redis, key, window_seconds, fail_before_expire=False):
count = redis.incr(key)
if count == 1:
if fail_before_expire:
raise ConnectionError("failure between INCR and EXPIRE")
redis.expire(key, window_seconds)
return count
redis = RedisModel()
try:
try_acquire_with_failure(redis, "monitor:ratelimit:client:1", 60, True)
except ConnectionError:
pass
key = "monitor:ratelimit:client:1"
assert redis.values[key] == 1
assert key not in redis.expiry
print("INCR committed while EXPIRE was skipped: key exists=%s, ttl=%s" %
(key in redis.values, redis.expiry.get(key)))
PYRepository: EntryDSM/entrydsm-platform
Length of output: 4943
🌐 Web query:
Redis INCR EXPIRE separate commands key without expiration Lua script atomic official documentation
💡 Result:
To atomically increment a key and set an expiration in Redis, you have two primary options: using the native INCREX command or a Lua script [1][2][3]. 1. Using INCREX (Recommended) The INCREX command is the preferred, native way to perform an atomic increment with an optional expiration [2][3]. It collapses the two-step pattern into a single O(1) command, ensuring atomicity and reducing round-trips [3]. Example usage: INCREX mykey BY 1 EX 60 2. Using a Lua Script If you are using an older version of Redis or have custom requirements, you can use a Lua script executed via the EVAL command [1][4]. Redis guarantees that scripts execute atomically, blocking all other server activities until the script completes [5][6]. The standard pattern to ensure the key is only expired on its first creation (or specific condition) is: local current = redis.call("incr", KEYS[1]) if current == 1 then redis.call("expire", KEYS[1], ARGV[1]) end return current You would call this via: EVAL "local current = redis.call('incr', KEYS[1]); if current == 1 then redis.call('expire', KEYS[1], ARGV[1]) end; return current" 1 mykey 60 Why not just use separate commands? Sending INCR and EXPIRE as separate commands is not atomic [1][4]. If a client fails between the two commands, the key will exist without an expiration, leading to potential "key leakage" where the key persists indefinitely [1][4]. While Redis Transactions (MULTI/EXEC) can group commands, they do not provide the ability to use the result of one command (e.g., the new counter value) to conditionally decide whether to execute the next (e.g., setting the expiration) within the same transaction [7][8]. Lua scripts or native commands like INCREX are necessary for such conditional logic [2][3][8].
Citations:
- 1: https://redis.io/docs/latest/commands/incr/
- 2: https://redis.io/docs/latest/commands/increx/
- 3: redis/redis@9c1ecd0
- 4: https://redis.antirez.com/commands/content/commands/incr.md
- 5: https://redis.io/docs/latest/develop/programmability/eval-intro/
- 6: https://redis.antirez.com/fundamental/atomic-updates.md
- 7: https://redis.io/docs/latest/develop/using-commands/transactions/
- 8: https://oneuptime.com/blog/post/2026-01-27-atomic-operations-redis-lua-scripts/view
INCR과 TTL 설정을 원자적으로 실행하세요.
increment()와 expire()가 별도 명령이므로, 첫 INCR 후 실패하면 키가 TTL 없이 남습니다. Lua 스크립트에서 INCR 결과가 1일 때 EXPIRE를 함께 실행하세요. 실패 경로를 검증하는 테스트도 추가하세요.
🤖 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/redis/RedisRateLimitAdapter.kt`
around lines 16 - 19, RedisRateLimitAdapter의 increment 및 expire 호출을 Lua 스크립트로
통합해 INCR 결과가 1일 때 EXPIRE가 동일한 원자적 작업으로 실행되도록 수정하세요. 기존 windowSeconds와 redisKey
값을 스크립트 인자로 전달하고, 실패 시 TTL 없는 키가 남지 않는 경로를 검증하는 테스트를 추가하세요.
| override fun heartbeat(sessionId: String, service: ServiceName, now: Instant): Boolean { | ||
| if (!redis.hasKey(metaKey(sessionId))) return false | ||
| val nowMillis = now.toEpochMilli() | ||
| val hashOps = redis.opsForHash<String, String>() | ||
| hashOps.put(metaKey(sessionId), FIELD_SERVICE, service.name) | ||
| hashOps.put(metaKey(sessionId), FIELD_LAST_HEARTBEAT_AT, nowMillis.toString()) | ||
| redis.expire(metaKey(sessionId), SESSION_TTL) | ||
| touchWindow(sessionId, service, nowMillis) | ||
| return true |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -e
file="systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/redis/RedisSessionStoreAdapter.kt"
printf '%s\n' '--- file outline ---'
ast-grep outline "$file" || true
printf '%s\n' '--- relevant source ---'
sed -n '1,220p' "$file"
printf '%s\n' '--- related symbols and tests ---'
rg -n --glob '*.kt' 'RedisSessionStoreAdapter|heartbeat\(|leave\(|touchWindow|SESSION_TTL|DURATION_SUM_KEY|DURATION_COUNT_KEY' systemsRepository: EntryDSM/entrydsm-platform
Length of output: 11466
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- adapter module files ---'
git ls-files 'systems/observability/observability-adapter-out'
printf '%s\n' '--- adapter tests and Redis configuration ---'
rg -n --glob '*.kt' --glob '*.gradle*' --glob '*.kts' \
'RedisSessionStoreAdapter|StringRedisTemplate|RedisTemplate|`@DataRedisTest`|RedisConnectionFactory|observability-adapter-out' \
systems build.gradle* settings.gradle* gradle* 2>/dev/null || true
printf '%s\n' '--- application call sites ---'
sed -n '1,130p' systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/SessionCollectionService.kt
printf '%s\n' '--- port contract ---'
sed -n '1,100p' systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/observability/application/port/out/SessionStorePort.ktRepository: EntryDSM/entrydsm-platform
Length of output: 5557
🏁 Script executed:
#!/bin/bash
set -e
python3 - <<'PY'
from itertools import permutations
heartbeat = ["H.hasKey", "H.put(service)", "H.put(heartbeat)", "H.expire", "H.window"]
leave = ["L.entries", "L.duration", "L.delete", "L.removeWindows"]
bad = []
for order in permutations("H" * len(heartbeat) + "L" * len(leave)):
hi = li = 0
state = "present"
recreated = False
deleted_after_read = False
for actor in order:
if actor == "H":
cmd = heartbeat[hi]
hi += 1
if cmd == "H.hasKey":
heartbeat_seen = state == "present"
elif cmd == "H.put(service)" and heartbeat_seen and state == "deleted":
recreated = True
elif cmd == "H.put(heartbeat)" and heartbeat_seen and state == "deleted":
recreated = True
else:
cmd = leave[li]
li += 1
if cmd == "L.entries":
leave_seen = state == "present"
elif cmd == "L.delete" and leave_seen:
state = "deleted"
if recreated:
bad.append(order)
break
print("interleaving reproduces heartbeat-after-leave recreation:", bool(bad))
if bad:
print("example:", " ".join(bad[0]))
PYRepository: EntryDSM/entrydsm-platform
Length of output: 254
🏁 Script executed:
#!/bin/bash
set -e
python3 - <<'PY'
from itertools import permutations
# Model the command-level behavior in RedisSessionStoreAdapter.kt.
# A leave call reads the metadata before it increments duration and deletes the key.
commands = {
"A": ["entries", "increment_sum", "increment_count", "delete", "remove_windows"],
"B": ["entries", "increment_sum", "increment_count", "delete", "remove_windows"],
}
order = ["A"] * len(commands["A"]) + ["B"] * len(commands["B"])
duplicate_counted = False
example = None
for schedule in permutations(order):
positions = {"A": 0, "B": 0}
observed = {"A": False, "B": False}
increments = 0
for actor in schedule:
command = commands[actor][positions[actor]]
positions[actor] += 1
if command == "entries":
observed[actor] = True
elif command == "increment_sum" and observed[actor]:
increments += 1
if increments == 2:
duplicate_counted = True
example = schedule
break
print("two concurrent leave calls can both count one session:", duplicate_counted)
if example:
print("example:", " ".join(example))
PYRepository: EntryDSM/entrydsm-platform
Length of output: 253
🏁 Script executed:
#!/bin/bash
set -e
python3 - <<'PY'
from itertools import product
heartbeat = ["hasKey", "put_service", "put_heartbeat", "expire_meta", "window_updates"]
leave = ["entries", "duration_sum", "duration_count", "delete_meta", "window_removals"]
# Search all actor-level interleavings while preserving each method's command order.
def interleavings(a, b):
if not a:
yield b
elif not b:
yield a
else:
for rest in interleavings(a[1:], b):
yield ["H"] + rest
for rest in interleavings(a, b[1:]):
yield ["L"] + rest
recreate = False
duplicate_leave_count = False
recreate_example = None
duplicate_example = None
for schedule in interleavings(heartbeat, leave):
hi = li = 0
meta_exists = True
h_observed_exists = False
l_observed_meta = False
leave_duration_updates = 0
heartbeat_recreated = False
for actor in schedule:
if actor == "H":
command = heartbeat[hi]
hi += 1
if command == "hasKey":
h_observed_exists = meta_exists
elif command.startswith("put_") and h_observed_exists and not meta_exists:
meta_exists = True
heartbeat_recreated = True
else:
command = leave[li]
li += 1
if command == "entries":
l_observed_meta = meta_exists
elif command == "duration_sum" and l_observed_meta:
leave_duration_updates += 1
elif command == "delete_meta" and l_observed_meta:
meta_exists = False
if heartbeat_recreated and not recreate:
recreate = True
recreate_example = schedule
if leave_duration_updates == 2:
duplicate_leave_count = True
duplicate_example = schedule
print("heartbeat can recreate metadata after leave:", recreate)
print("example:", " ".join(recreate_example or []))
print("two leave calls can both count one session:", duplicate_leave_count)
print("example:", " ".join(duplicate_example or []))
PYRepository: EntryDSM/entrydsm-platform
Length of output: 274
🏁 Script executed:
#!/bin/bash
set -e
python3 - <<'PY'
def schedules(h, l, prefix=""):
if h == 0:
yield prefix + "L" * l
elif l == 0:
yield prefix + "H" * h
else:
yield from schedules(h - 1, l, prefix + "H")
yield from schedules(h, l - 1, prefix + "L")
heartbeat = ["hasKey", "put_service", "put_heartbeat", "expire_meta", "window_updates"]
leave = ["entries", "duration_sum", "duration_count", "delete_meta", "window_removals"]
for schedule in schedules(len(heartbeat), len(leave)):
hi = li = 0
exists = True
heartbeat_saw_exists = False
leave_saw_meta = False
recreated = False
for actor in schedule:
if actor == "H":
command = heartbeat[hi]
hi += 1
if command == "hasKey":
heartbeat_saw_exists = exists
elif command.startswith("put_") and heartbeat_saw_exists and not exists:
exists = True
recreated = True
else:
command = leave[li]
li += 1
if command == "entries":
leave_saw_meta = exists
elif command == "delete_meta" and leave_saw_meta:
exists = False
if recreated:
print("heartbeat can recreate metadata after leave: True")
print("example:", schedule)
break
else:
print("heartbeat can recreate metadata after leave: False")
for schedule in schedules(len(leave), len(leave)):
ai = bi = 0
a_saw = b_saw = False
duration_updates = 0
for actor in schedule:
if actor == "H":
command = leave[ai]
ai += 1
if command == "entries":
a_saw = True
elif command == "duration_sum" and a_saw:
duration_updates += 1
else:
command = leave[bi]
bi += 1
if command == "entries":
b_saw = True
elif command == "duration_sum" and b_saw:
duration_updates += 1
if duration_updates == 2:
print("two leave calls can both count one session: True")
print("example:", schedule)
break
else:
print("two leave calls can both count one session: False")
PYRepository: EntryDSM/entrydsm-platform
Length of output: 303
세션 lifecycle 전환을 원자적으로 처리하세요.
heartbeat()와 leave()의 Redis 명령이 분리되어 있습니다. leave() 이후 heartbeat()가 메타데이터와 window 멤버를 다시 생성할 수 있습니다. 두 leave() 요청이 같은 메타데이터를 읽으면 세션 시간이 중복 집계될 수 있습니다.
heartbeat()와 leave()를 Lua 스크립트 또는 WATCH/MULTI/EXEC 경계에서 조건부로 처리하세요. 동일 서브시스템에 두 경쟁 조건을 검증하는 결정적 동시성 테스트도 추가하세요.
📍 Affects 1 file
systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/redis/RedisSessionStoreAdapter.kt#L37-L45(this comment)systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/redis/RedisSessionStoreAdapter.kt#L48-L59
🤖 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/redis/RedisSessionStoreAdapter.kt`
around lines 37 - 45, Make the session lifecycle transition atomic across
RedisSessionStoreAdapter. In heartbeat(), conditionally update metadata, TTL,
and the activity window only if the session still exists; in leave(), atomically
verify and remove the session while preventing concurrent heartbeats or
duplicate leaves from recreating or double-counting it, using Lua or
WATCH/MULTI/EXEC. Add deterministic concurrency tests in the same subsystem
covering heartbeat-after-leave and two simultaneous leave requests; update the
anchor file at lines 37-45 and sibling location at lines 48-59 as required.
| hashOps.put(metaKey(sessionId), FIELD_SERVICE, service.name) | ||
| hashOps.put(metaKey(sessionId), FIELD_LAST_HEARTBEAT_AT, nowMillis.toString()) | ||
| redis.expire(metaKey(sessionId), SESSION_TTL) | ||
| touchWindow(sessionId, service, nowMillis) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- candidate files ---'
git ls-files | rg 'RedisSessionStoreAdapter|SessionStore|observability.*redis|redis' | head -200
printf '%s\n' '--- outline ---'
ast-grep outline systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/redis/RedisSessionStoreAdapter.kt
printf '%s\n' '--- implementation ---'
cat -n systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/redis/RedisSessionStoreAdapter.kt
printf '%s\n' '--- symbols/usages ---'
rg -n -C 3 'concurrentUsers|heartbeat\\(|touchWindow|FIELD_SERVICE|SESSION_TTL|RedisSessionStoreAdapter' systems/observabilityRepository: EntryDSM/entrydsm-platform
Length of output: 7846
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- files ---'
git ls-files | rg 'RedisSessionStoreAdapter|SessionStore|observability.*redis|redis' | head -200
printf '%s\n' '--- implementation ---'
cat -n systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/redis/RedisSessionStoreAdapter.kt
printf '%s\n' '--- usages ---'
rg -n -C 3 'concurrentUsers|heartbeat|touchWindow|FIELD_SERVICE|SESSION_TTL|RedisSessionStoreAdapter' systems/observabilityRepository: EntryDSM/entrydsm-platform
Length of output: 28979
🏁 Script executed:
#!/bin/bash
set -eu
cat -n systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/redis/RedisSessionStoreAdapter.kt
rg -n -C 5 'concurrentUsers|heartbeat|touchWindow|FIELD_SERVICE|SESSION_TTL|RedisSessionStoreAdapter' systems/observabilityRepository: EntryDSM/entrydsm-platform
Length of output: 36364
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- adapter ---'
sed -n '1,180p' systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/redis/RedisSessionStoreAdapter.kt
printf '%s\n' '--- related test files ---'
git ls-files | rg '(^|/)(test|tests)/|Test\\.kt$|Spec\\.kt$' | rg 'observ|redis|session' | head -200
printf '%s\n' '--- all relevant declarations ---'
rg -n -C 8 'fun (heartbeat|leave|concurrentUsers|touchWindow)|FIELD_SERVICE|ZADD|zadd|zrem|remove.*window|concurrentUsers' .Repository: EntryDSM/entrydsm-platform
Length of output: 39421
서비스 소속 변경을 차단하세요.
heartbeat()는 세션의 FIELD_SERVICE를 덮어쓰고 새 서비스 ZSET에만 추가합니다. 기존 서비스 ZSET의 멤버가 남아 동일 세션이 여러 서비스의 concurrentUsers()에 집계됩니다. 저장된 서비스와 요청 service가 다르면 heartbeat를 실패시키고, 일치할 때만 저장된 서비스 window를 갱신하세요. 이 동작을 검증하는 Kotlin 테스트도 추가하세요.
🤖 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/redis/RedisSessionStoreAdapter.kt`
around lines 41 - 44, RedisSessionStoreAdapter의 heartbeat()에서 저장된 FIELD_SERVICE를
먼저 조회해 요청 service와 다르면 실패시키고 기존 서비스 값이나 ZSET을 변경하지 않도록 하세요. 서비스가 일치할 때만
FIELD_LAST_HEARTBEAT_AT, TTL, touchWindow()를 갱신하고 저장된 서비스 기준으로 window를 처리하세요. 이
서비스 소속 변경 차단 동작을 검증하는 Kotlin 테스트를 추가하세요.
| val currentMax = redis.opsForValue().get(CONCURRENT_MAX_KEY)?.toIntOrNull() ?: 0 | ||
| if (current > currentMax) { | ||
| redis.opsForValue().set(CONCURRENT_MAX_KEY, current.toString()) | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -e
file='systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/redis/RedisSessionStoreAdapter.kt'
printf '%s\n' '--- outline ---'
ast-grep outline "$file" || true
printf '%s\n' '--- implementation ---'
cat -n "$file"
printf '%s\n' '--- related files ---'
rg -l --glob '*.kt' 'RedisSessionStoreAdapter|CONCURRENT_MAX_KEY|sampleConcurrency|concurrentMax' . | head -80
printf '%s\n' '--- Redis atomic API usage ---'
rg -n --glob '*.kt' 'increment\(|decrement\(|eval\(|compareAndSet|opsForValue\(\)\.(get|set)' . | head -160
printf '%s\n' '--- build dependencies ---'
rg -n 'spring-data-redis|lettuce|jedis|redis' --glob 'build.gradle*' --glob 'gradle.properties' --glob 'libs.versions.toml' --glob 'pom.xml' .Repository: EntryDSM/entrydsm-platform
Length of output: 10613
🏁 Script executed:
#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
path = Path("systems/observability/observability-adapter-out/src/main/kotlin/hs/kr/entrydsm/observability/adapterout/redis/RedisSessionStoreAdapter.kt")
source = path.read_text()
assert "val currentMax = redis.opsForValue().get(CONCURRENT_MAX_KEY)?.toIntOrNull() ?: 0" in source
assert "if (current > currentMax)" in source
assert 'redis.opsForValue().set(CONCURRENT_MAX_KEY, current.toString())' in source
# Model two concurrent sampleConcurrency calls:
# A observes 10, B observes 20, and both read the old Redis maximum 5.
initial_max = 5
a_current, b_current = 10, 20
a_observed, b_observed = initial_max, initial_max
# B completes its conditional SET first; A completes afterward.
redis_max = initial_max
if b_current > b_observed:
redis_max = b_current
if a_current > a_observed:
redis_max = a_current
assert redis_max == 10
assert redis_max != max(a_current, b_current)
print("The separate GET/conditional SET sequence can leave CONCURRENT_MAX_KEY at 10 instead of 20.")
PYRepository: EntryDSM/entrydsm-platform
Length of output: 256
CONCURRENT_MAX_KEY 갱신을 원자적으로 처리하세요.
두 sampleConcurrency 호출이 같은 값을 읽으면 더 큰 값이 저장된 뒤 더 작은 값이 이를 덮어쓸 수 있습니다. 비교와 갱신을 Lua 스크립트 등 하나의 Redis 원자 작업으로 묶으세요.
🤖 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/redis/RedisSessionStoreAdapter.kt`
around lines 83 - 86, Update the CONCURRENT_MAX_KEY handling in
sampleConcurrency to perform the read, comparison, and conditional write as one
atomic Redis operation, such as a Lua script, so concurrent calls cannot
overwrite a larger maximum with a smaller value. Preserve the existing behavior
of storing only when current exceeds the recorded maximum.
| import org.springframework.stereotype.Service | ||
|
|
||
| @Service | ||
| class SessionCollectionService( | ||
| private val sessionStorePort: SessionStorePort, | ||
| private val rateLimitPort: RateLimitPort, | ||
| private val clock: Clock, | ||
| ) : RecordSessionEventUseCase { |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 3 \
'org\.springframework|`@Service`|`@Component`|`@Configuration`|`@Bean`' \
systems/observability/observability-application/src/main/kotlin \
systems/observability/observability-bootstrap/src/main/kotlinRepository: EntryDSM/entrydsm-platform
Length of output: 4137
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- application and bootstrap files ---'
git ls-files 'systems/observability/observability-application/src/main/kotlin' \
'systems/observability/observability-bootstrap/src/main/kotlin' | sort
printf '%s\n' '--- service references and bean declarations ---'
rg -n -C 4 \
'SessionCollectionService|RecordSessionEventUseCase|`@Bean`|`@ComponentScan`|`@SpringBootApplication`' \
systems/observability
printf '%s\n' '--- module dependencies ---'
fd -i -t f 'build.gradle|build.gradle.kts|pom.xml|settings.gradle|settings.gradle.kts' systems/observability \
--exec sh -c 'echo "--- $1"; cat -n "$1"' sh {}Repository: EntryDSM/entrydsm-platform
Length of output: 15328
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- relevant build files ---'
git ls-files | rg '(^|/)(build\.gradle(\.kts)?|settings\.gradle(\.kts)?|pom\.xml)$' | sort
printf '%s\n' '--- observability module configuration ---'
git ls-files systems/observability | rg '(^|/)(build\.gradle(\.kts)?|settings\.gradle(\.kts)?|pom\.xml)$' |
while IFS= read -r file; do
echo "--- $file"
cat -n "$file"
done
printf '%s\n' '--- application ExampleApplication ---'
cat -n systems/observability/observability-application/src/main/kotlin/hs/kr/entrydsm/ExampleApplication.kt
printf '%s\n' '--- bootstrap Kotlin source ---'
git ls-files systems/observability/observability-bootstrap/src/main/kotlin |
while IFS= read -r file; do
echo "--- $file"
cat -n "$file"
doneRepository: EntryDSM/entrydsm-platform
Length of output: 193
애플리케이션 계층에서 Spring 의존성을 제거하세요.
SessionCollectionService에서 @Service와 org.springframework.stereotype.Service import를 제거하세요. Bean 등록은 observability-bootstrap의 설정에서 처리하세요.
🤖 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/SessionCollectionService.kt`
around lines 15 - 22, SessionCollectionService의 애플리케이션 계층 Spring 의존성을 제거하세요.
클래스에서 `@Service` 어노테이션과 org.springframework.stereotype.Service import를 삭제하고, 해당 빈
등록은 observability-bootstrap 설정에서 유지하세요.
Source: Coding guidelines
| @Test | ||
| fun heartbeatOnUnknownSessionThrowsSessionNotFound() { | ||
| val service = SessionCollectionService(sessionStore, FakeRateLimitPort(true), clock) | ||
|
|
||
| assertThrows(MonitorDomainException::class.java) { | ||
| service.record(SessionEventType.HEARTBEAT, "sess_unknown", ServiceName.APPLICATION, null, "127.0.0.1") | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| fun rateLimitExceededThrowsTooManyRequests() { | ||
| val service = SessionCollectionService(sessionStore, FakeRateLimitPort(false), clock) | ||
|
|
||
| assertThrows(MonitorDomainException::class.java) { | ||
| service.record(SessionEventType.ENTER, null, ServiceName.APPLICATION, null, "127.0.0.1") | ||
| } | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
HEARTBEAT 및 LEAVE 정상 흐름을 테스트하세요.
현재 테스트는 HEARTBEAT의 실패 경로만 검증합니다. LEAVE 경로는 검증하지 않습니다.
ENTER 후 반환된 session ID로 HEARTBEAT를 호출하는 테스트를 추가하세요. ENTER 후 LEAVE를 호출하고, 이후 HEARTBEAT가 SESSION_NOT_FOUND가 되는 상태 전이 테스트를 추가하세요.
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." As per path instructions, "Ask for deterministic tests and meaningful assertions, not only happy-path checks."
🤖 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/test/kotlin/hs/kr/entrydsm/observability/application/SessionCollectionServiceTest.kt`
around lines 30 - 46, Add deterministic tests for the normal HEARTBEAT and LEAVE
flows in SessionCollectionServiceTest. Capture the session ID returned by ENTER,
assert HEARTBEAT succeeds for that ID, and add a state-transition test that
performs ENTER then LEAVE and verifies a subsequent HEARTBEAT fails with
SESSION_NOT_FOUND using a meaningful exception assertion.
Sources: Coding guidelines, Path instructions
| import org.junit.runner.RunWith | ||
| import org.junit.runners.Suite | ||
|
|
||
| class ObservabilityApplicationModuleTest { | ||
| @Test | ||
| fun moduleLoads() { | ||
| assertTrue(true) | ||
| } | ||
| } | ||
| @RunWith(Suite::class) | ||
| @Suite.SuiteClasses( | ||
| SessionCollectionServiceTest::class, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
SessionCollectionServiceTest import를 추가하세요.
SessionCollectionServiceTest는 hs.kr.entrydsm.observability.application 패키지에 있습니다. 이 파일은 hs.kr.entrydsm 패키지에 있습니다. Kotlin은 하위 패키지의 클래스를 자동으로 import하지 않습니다.
현재 suite 선언은 Unresolved reference: SessionCollectionServiceTest로 컴파일에 실패합니다.
수정 예시
+import hs.kr.entrydsm.observability.application.SessionCollectionServiceTest
import org.junit.runner.RunWith
import org.junit.runners.Suite📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import org.junit.runner.RunWith | |
| import org.junit.runners.Suite | |
| class ObservabilityApplicationModuleTest { | |
| @Test | |
| fun moduleLoads() { | |
| assertTrue(true) | |
| } | |
| } | |
| @RunWith(Suite::class) | |
| @Suite.SuiteClasses( | |
| SessionCollectionServiceTest::class, | |
| import hs.kr.entrydsm.observability.application.SessionCollectionServiceTest | |
| import org.junit.runner.RunWith | |
| import org.junit.runners.Suite | |
| `@RunWith`(Suite::class) | |
| `@Suite.SuiteClasses`( | |
| SessionCollectionServiceTest::class, |
🤖 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/test/kotlin/hs/kr/entrydsm/TestMain.kt`
around lines 3 - 8, TestMain.kt의 suite 선언에서 사용하는 SessionCollectionServiceTest를
hs.kr.entrydsm.observability.application 패키지에서 명시적으로 import하도록 추가하세요.
| @Configuration(proxyBeanMethods = false) | ||
| class ClockConfig { | ||
| @Bean | ||
| fun clock(): Clock = Clock.systemUTC() |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
UTC Clock bean의 테스트를 추가하세요.
이 변경은 생산 Kotlin 로직을 추가합니다. 제공된 변경에는 observability-bootstrap subsystem의 대응 테스트가 없습니다.
ClockConfig().clock().zone이 UTC인지 검증하는 단위 테스트를 추가하세요. 테스트가 불필요하면 PR 설명에 근거를 명시하세요.
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." As per path instructions, "Highlight behavior-changing code that lacks corresponding unit/integration tests."
🤖 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-bootstrap/src/main/kotlin/hs/kr/entrydsm/observability/config/ClockConfig.kt`
around lines 7 - 10, ClockConfig의 clock()이 UTC를 반환하는지 검증하는
observability-bootstrap 단위 테스트를 추가하세요. ClockConfig().clock().zone을 확인해 UTC와
일치하는지 단언하고, 해당 테스트가 같은 subsystem의 기존 테스트 관례를 따르도록 구성하세요.
Sources: Coding guidelines, Path instructions
| fun heartbeat(now: Instant, service: ServiceName): Session = | ||
| copy(service = service, lastHeartbeatAt = now) | ||
|
|
||
| fun isExpired(now: Instant, windowSeconds: Long): Boolean = | ||
| lastHeartbeatAt.plusSeconds(windowSeconds).isBefore(now) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Session 동작 테스트를 추가하십시오.
heartbeat와 isExpired는 새 production logic입니다. 제공된 suite에는 이 동작을 검증하는 SessionTest가 없습니다. heartbeat의 필드 보존 및 갱신을 검증하십시오. 만료 직전, 정확한 만료 시각, 만료 직후의 경계 조건을 검증하십시오.
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.”
🤖 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-domain/src/main/kotlin/hs/kr/entrydsm/observability/domain/model/Session.kt`
around lines 12 - 16, Session의 새 동작을 검증하는 SessionTest를 추가하십시오. heartbeat 테스트에서는
service와 lastHeartbeatAt이 갱신되고 나머지 필드는 보존되는지 확인하십시오. isExpired 테스트에서는 만료 직전에는
false, 정확한 만료 시각에는 false, 만료 직후에는 true가 되는 경계 조건을 검증하십시오.
Source: Coding guidelines
Summary
Related Issue
Scope
Testing
bazel build //systems/observability/...,bazel test //systems/observability/...통과bazel test //systems/...(전체 시스템) 통과 확인 — 다른 시스템 영향 없음Checklist