feat(ranking): 랭킹 조회 관측 로그를 추가한다
This commit is contained in:
@@ -4,6 +4,7 @@ import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingItem
|
|||||||
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingBlockPort
|
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingBlockPort
|
||||||
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotPort
|
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotPort
|
||||||
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotRecord
|
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotRecord
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
@@ -15,28 +16,59 @@ class CreatorRankingQueryService(
|
|||||||
@Value("\${cloud.aws.cloud-front.host}")
|
@Value("\${cloud.aws.cloud-front.host}")
|
||||||
private val cloudFrontHost: String
|
private val cloudFrontHost: String
|
||||||
) {
|
) {
|
||||||
|
private val log = LoggerFactory.getLogger(javaClass)
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun getCreatorRankings(viewerMemberId: Long?): CreatorRankingResult {
|
fun getCreatorRankings(viewerMemberId: Long?): CreatorRankingResult {
|
||||||
val latestItems = snapshotPort.findLatestSnapshots().toRankedItems()
|
val startedAt = System.currentTimeMillis()
|
||||||
if (latestItems.isEmpty()) {
|
return runCatching {
|
||||||
return CreatorRankingResult(showRankChange = false, items = emptyList())
|
val latestItems = snapshotPort.findLatestSnapshots().toRankedItems()
|
||||||
}
|
if (latestItems.isEmpty()) {
|
||||||
|
return@runCatching QueryLogResult(
|
||||||
|
result = CreatorRankingResult(showRankChange = false, items = emptyList()),
|
||||||
|
blockedCreatorCount = 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val previousItems = snapshotPort.findPreviousCompletedSnapshots().toRankedItems()
|
val previousItems = snapshotPort.findPreviousCompletedSnapshots().toRankedItems()
|
||||||
val previousRankByCreatorId = previousItems.associate { it.creatorId to it.rank }
|
val previousRankByCreatorId = previousItems.associate { it.creatorId to it.rank }
|
||||||
val showRankChange = previousRankByCreatorId.isNotEmpty()
|
val showRankChange = previousRankByCreatorId.isNotEmpty()
|
||||||
val blockedCreatorIds = findBlockedCreatorIds(viewerMemberId = viewerMemberId, items = latestItems)
|
val blockedCreatorIds = findBlockedCreatorIds(viewerMemberId = viewerMemberId, items = latestItems)
|
||||||
val items = latestItems.map { item ->
|
val items = latestItems.map { item ->
|
||||||
val previousRank = previousRankByCreatorId[item.creatorId]
|
val previousRank = previousRankByCreatorId[item.creatorId]
|
||||||
item.copy(
|
item.copy(
|
||||||
rankChange = if (showRankChange && previousRank != null) previousRank - item.rank else null,
|
rankChange = if (showRankChange && previousRank != null) previousRank - item.rank else null,
|
||||||
isNew = showRankChange && previousRank == null
|
isNew = showRankChange && previousRank == null
|
||||||
).maskIfBlocked(blockedCreatorIds)
|
).maskIfBlocked(blockedCreatorIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
return CreatorRankingResult(showRankChange = showRankChange, items = items)
|
QueryLogResult(
|
||||||
|
result = CreatorRankingResult(showRankChange = showRankChange, items = items),
|
||||||
|
blockedCreatorCount = blockedCreatorIds.size
|
||||||
|
)
|
||||||
|
}.onSuccess { logResult ->
|
||||||
|
log.info(
|
||||||
|
"event=creator_ranking_query_success showRankChange={} itemCount={} blockedCreatorCount={} elapsedMs={}",
|
||||||
|
logResult.result.showRankChange,
|
||||||
|
logResult.result.items.size,
|
||||||
|
logResult.blockedCreatorCount,
|
||||||
|
System.currentTimeMillis() - startedAt
|
||||||
|
)
|
||||||
|
}.onFailure { ex ->
|
||||||
|
log.warn(
|
||||||
|
"event=creator_ranking_query_failure elapsedMs={} error={}",
|
||||||
|
System.currentTimeMillis() - startedAt,
|
||||||
|
ex.message,
|
||||||
|
ex
|
||||||
|
)
|
||||||
|
}.getOrThrow().result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class QueryLogResult(
|
||||||
|
val result: CreatorRankingResult,
|
||||||
|
val blockedCreatorCount: Int
|
||||||
|
)
|
||||||
|
|
||||||
private fun List<CreatorRankingSnapshotRecord>.toRankedItems(): List<CreatorRankingItem> {
|
private fun List<CreatorRankingSnapshotRecord>.toRankedItems(): List<CreatorRankingItem> {
|
||||||
return groupBy { it.finalScore }
|
return groupBy { it.finalScore }
|
||||||
.toSortedMap(compareByDescending { it })
|
.toSortedMap(compareByDescending { it })
|
||||||
|
|||||||
@@ -8,11 +8,16 @@ import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotRecord
|
|||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertFalse
|
import org.junit.jupiter.api.Assertions.assertFalse
|
||||||
import org.junit.jupiter.api.Assertions.assertNull
|
import org.junit.jupiter.api.Assertions.assertNull
|
||||||
|
import org.junit.jupiter.api.Assertions.assertThrows
|
||||||
import org.junit.jupiter.api.Assertions.assertTrue
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
import org.junit.jupiter.api.DisplayName
|
import org.junit.jupiter.api.DisplayName
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
|
import org.springframework.boot.test.system.CapturedOutput
|
||||||
|
import org.springframework.boot.test.system.OutputCaptureExtension
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
@ExtendWith(OutputCaptureExtension::class)
|
||||||
class CreatorRankingQueryServiceTest {
|
class CreatorRankingQueryServiceTest {
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("스냅샷 후보와 조회 item 내부 모델은 순위 변화와 신규 진입 값을 담을 수 있다")
|
@DisplayName("스냅샷 후보와 조회 item 내부 모델은 순위 변화와 신규 진입 값을 담을 수 있다")
|
||||||
@@ -177,6 +182,37 @@ class CreatorRankingQueryServiceTest {
|
|||||||
assertEquals("profile-1.png", result.items.single().profileImageUrl)
|
assertEquals("profile-1.png", result.items.single().profileImageUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("크리에이터 랭킹 조회 성공은 순위 변화 노출 여부와 반환 수를 로그로 남긴다")
|
||||||
|
fun shouldLogCreatorRankingQuerySuccessWithResultCounts(output: CapturedOutput) {
|
||||||
|
val snapshotPort = FakeCreatorRankingQuerySnapshotPort()
|
||||||
|
snapshotPort.latestSnapshots = listOf(snapshot(creatorId = 1L, finalScore = 100.0))
|
||||||
|
val service = service(snapshotPort = snapshotPort)
|
||||||
|
|
||||||
|
service.getCreatorRankings(viewerMemberId = null)
|
||||||
|
|
||||||
|
assertTrue(output.out.contains("event=creator_ranking_query_success"))
|
||||||
|
assertTrue(output.out.contains("showRankChange=false"))
|
||||||
|
assertTrue(output.out.contains("itemCount=1"))
|
||||||
|
assertTrue(output.out.contains("blockedCreatorCount=0"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("크리에이터 랭킹 조회 실패는 에러를 로그로 남기고 예외를 전파한다")
|
||||||
|
fun shouldLogCreatorRankingQueryFailureWithError(output: CapturedOutput) {
|
||||||
|
val snapshotPort = FakeCreatorRankingQuerySnapshotPort()
|
||||||
|
snapshotPort.latestFailure = IllegalStateException("latest snapshots failed")
|
||||||
|
val service = service(snapshotPort = snapshotPort)
|
||||||
|
|
||||||
|
val exception = assertThrows(IllegalStateException::class.java) {
|
||||||
|
service.getCreatorRankings(viewerMemberId = 99L)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("latest snapshots failed", exception.message)
|
||||||
|
assertTrue(output.out.contains("event=creator_ranking_query_failure"))
|
||||||
|
assertTrue(output.out.contains("error=latest snapshots failed"))
|
||||||
|
}
|
||||||
|
|
||||||
private fun service(
|
private fun service(
|
||||||
snapshotPort: CreatorRankingSnapshotPort = FakeCreatorRankingQuerySnapshotPort(),
|
snapshotPort: CreatorRankingSnapshotPort = FakeCreatorRankingQuerySnapshotPort(),
|
||||||
blockPort: CreatorRankingBlockPort = FakeCreatorRankingBlockPort()
|
blockPort: CreatorRankingBlockPort = FakeCreatorRankingBlockPort()
|
||||||
@@ -219,13 +255,17 @@ class CreatorRankingQueryServiceTest {
|
|||||||
private class FakeCreatorRankingQuerySnapshotPort : CreatorRankingSnapshotPort {
|
private class FakeCreatorRankingQuerySnapshotPort : CreatorRankingSnapshotPort {
|
||||||
var latestSnapshots: List<CreatorRankingSnapshotRecord> = emptyList()
|
var latestSnapshots: List<CreatorRankingSnapshotRecord> = emptyList()
|
||||||
var previousSnapshots: List<CreatorRankingSnapshotRecord> = emptyList()
|
var previousSnapshots: List<CreatorRankingSnapshotRecord> = emptyList()
|
||||||
|
var latestFailure: RuntimeException? = null
|
||||||
|
|
||||||
override fun findSnapshotsByAggregationPeriod(
|
override fun findSnapshotsByAggregationPeriod(
|
||||||
aggregationStartAtUtc: LocalDateTime,
|
aggregationStartAtUtc: LocalDateTime,
|
||||||
aggregationEndAtUtc: LocalDateTime
|
aggregationEndAtUtc: LocalDateTime
|
||||||
): List<CreatorRankingSnapshotRecord> = emptyList()
|
): List<CreatorRankingSnapshotRecord> = emptyList()
|
||||||
|
|
||||||
override fun findLatestSnapshots(): List<CreatorRankingSnapshotRecord> = latestSnapshots
|
override fun findLatestSnapshots(): List<CreatorRankingSnapshotRecord> {
|
||||||
|
latestFailure?.let { throw it }
|
||||||
|
return latestSnapshots
|
||||||
|
}
|
||||||
|
|
||||||
override fun findPreviousCompletedSnapshots(): List<CreatorRankingSnapshotRecord> = previousSnapshots
|
override fun findPreviousCompletedSnapshots(): List<CreatorRankingSnapshotRecord> = previousSnapshots
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user