Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/main/kotlin/com/moa/common/config/MetricsConfig.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package com.moa.common.config

import io.micrometer.core.aop.TimedAspect
import io.micrometer.core.instrument.MeterRegistry
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.micrometer.metrics.autoconfigure.MeterRegistryCustomizer
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.EnableAspectJAutoProxy

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
class MetricsConfig(
@Value("\${spring.application.name}") private val appName: String,
@Value("\${app.deploy-color}") private val deployColor: String,
Expand All @@ -20,4 +23,7 @@ class MetricsConfig(
"app_version", appVersion,
)
}

@Bean
fun timedAspect(registry: MeterRegistry) = TimedAspect(registry)
}
33 changes: 30 additions & 3 deletions src/main/kotlin/com/moa/service/FcmService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,32 @@ package com.moa.service
import com.google.firebase.messaging.*
import com.moa.repository.FcmTokenRepository
import com.moa.service.dto.FcmRequest
import io.micrometer.core.instrument.Counter
import io.micrometer.core.instrument.MeterRegistry
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service

@Service
class FcmService(
private val fcmTokenRepository: FcmTokenRepository,
private val meterRegistry: MeterRegistry,
) {
private val log = LoggerFactory.getLogger(javaClass)

private val sendSuccess: Counter = Counter.builder(METRIC_SEND)
.description("FCM 메시지 단위 결과")
.tag("result", "success")
.register(meterRegistry)
private val sendFailure: Counter = Counter.builder(METRIC_SEND)
.description("FCM 메시지 단위 결과")
.tag("result", "failure")
.register(meterRegistry)

private fun tokenInvalidated(errorCode: String): Counter = Counter.builder(METRIC_TOKEN_INVALIDATED)
.description("FCM 에러로 자동 삭제된 토큰 수")
.tag("error_code", errorCode)
.register(meterRegistry)

fun sendEach(requests: List<FcmRequest>): List<Boolean> {
if (requests.isEmpty()) return emptyList()
val results = ArrayList<Boolean>(requests.size)
Expand All @@ -20,13 +37,17 @@ class FcmService(
val messages = batch.map { buildMessage(it.token, it.data) }
val response = FirebaseMessaging.getInstance().sendEach(messages)
response.responses.forEachIndexed { i, sendResponse ->
if (!sendResponse.isSuccessful) {
if (sendResponse.isSuccessful) {
sendSuccess.increment()
} else {
sendFailure.increment()
handleFcmException(sendResponse.exception, batch[i].token)
}
results.add(sendResponse.isSuccessful)
}
} catch (ex: Exception) {
log.error("FCM batch send failed for {} messages", batch.size, ex)
sendFailure.increment(batch.size.toDouble())
repeat(batch.size) { results.add(false) }
}
}
Expand Down Expand Up @@ -56,17 +77,23 @@ class FcmService(
return
}
when (fcmEx.messagingErrorCode) {
MessagingErrorCode.UNREGISTERED,
MessagingErrorCode.UNREGISTERED -> {
log.warn("유효하지 않은 FCM 토큰 삭제: {}", token)
fcmTokenRepository.deleteByToken(token)
tokenInvalidated("unregistered").increment()
}
MessagingErrorCode.INVALID_ARGUMENT -> {
log.warn("유효하지 않은 FCM 토큰 삭제: {}", token)
fcmTokenRepository.deleteByToken(token)
tokenInvalidated("invalid_argument").increment()
}
Comment on lines 85 to 89

else -> log.error("FCM 전송 실패: {}", token, fcmEx)
}
}

companion object {
private const val MAX_BATCH_SIZE = 500
private const val METRIC_SEND = "moa.fcm.send"
private const val METRIC_TOKEN_INVALIDATED = "moa.fcm.token_invalidated"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import com.moa.repository.NotificationLogRepository
import com.moa.service.FcmService
import com.moa.service.PublicHolidayService
import com.moa.service.dto.FcmRequest
import io.micrometer.core.annotation.Timed
import io.micrometer.core.instrument.Counter
import io.micrometer.core.instrument.MeterRegistry
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.time.LocalDate
Expand All @@ -20,9 +23,22 @@ class NotificationDispatchService(
private val fcmService: FcmService,
private val notificationMessageBuilder: NotificationMessageBuilder,
private val publicHolidayService: PublicHolidayService,
private val meterRegistry: MeterRegistry,
) {
private val log = LoggerFactory.getLogger(javaClass)

private fun attemptsCounter(type: String): Counter = Counter.builder(METRIC_ATTEMPTS)
.description("디스패치 시도된 알림의 누적 수 (이전에 PENDING 상태였던 행을 처리할 때마다 증가)")
.tag("notification_type", type)
.register(meterRegistry)

private fun failedCounter(type: String, reason: String): Counter = Counter.builder(METRIC_FAILED)
.description("발송 실패한 알림 수")
.tag("notification_type", type)
.tag("reason", reason)
.register(meterRegistry)
Comment on lines +35 to +39

@Timed(value = "moa.notification.dispatch", histogram = true)
fun processNotifications(date: LocalDate, currentTime: LocalTime) {
val pendingLogs = notificationLogRepository
.findAllByScheduledDateAndScheduledTimeLessThanEqualAndStatus(
Expand All @@ -33,6 +49,8 @@ class NotificationDispatchService(
if (pendingLogs.isEmpty()) return

log.info("Dispatching {} pending notifications", pendingLogs.size)
pendingLogs.groupingBy { it.notificationType.name }.eachCount()
.forEach { (type, n) -> attemptsCounter(type).increment(n.toDouble()) }

val memberIds = pendingLogs.map { it.memberId }.distinct()
val tokensByMemberId = fcmTokenRepository.findAllByMemberIdIn(memberIds)
Expand All @@ -45,9 +63,11 @@ class NotificationDispatchService(

val dispatchItems = mutableListOf<DispatchItem>()
for (notification in pendingLogs) {
val typeName = notification.notificationType.name
val tokens = tokensByMemberId[notification.memberId].orEmpty()
if (tokens.isEmpty()) {
notification.status = NotificationStatus.FAILED
failedCounter(typeName, REASON_NO_TOKEN).increment()
log.warn("No FCM tokens for member {}, marking as FAILED", notification.memberId)
continue
}
Expand All @@ -57,7 +77,13 @@ class NotificationDispatchService(
tokens.forEach { dispatchItems.add(DispatchItem(notification, it.token, data)) }
} catch (e: Exception) {
notification.status = NotificationStatus.FAILED
log.error("Failed to build message for notification {}, member {}", notification.id, notification.memberId, e)
failedCounter(typeName, REASON_BUILD).increment()
log.error(
"Failed to build message for notification {}, member {}",
notification.id,
notification.memberId,
e
)
}
}

Expand All @@ -67,11 +93,22 @@ class NotificationDispatchService(
if (success) dispatchItems[i].notification.status = NotificationStatus.SENT
}
pendingLogs.filter { it.status == NotificationStatus.PENDING }
.forEach { it.status = NotificationStatus.FAILED }
.forEach {
it.status = NotificationStatus.FAILED
failedCounter(it.notificationType.name, REASON_FCM).increment()
}
}

notificationLogRepository.saveAll(pendingLogs)
}

companion object {
private const val METRIC_ATTEMPTS = "moa.notification.dispatch.attempts"
private const val METRIC_FAILED = "moa.notification.dispatch.failed"
private const val REASON_NO_TOKEN = "no_token"
private const val REASON_BUILD = "build"
private const val REASON_FCM = "fcm"
}
}

private data class DispatchItem(
Expand Down
Loading
Loading