Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 domain/src/main/kotlin/com/dobby/gateway/AlertChannel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.dobby.gateway

enum class AlertChannel {
ERRORS,
NOTIFY
}
1 change: 1 addition & 0 deletions domain/src/main/kotlin/com/dobby/gateway/AlertGateway.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ package com.dobby.gateway

interface AlertGateway {
fun sendError(e: Exception, requestUrl: String, clientIp: String?)
fun sendNotify(title: String, description: String, content: String?)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.dobby.config.properties

import org.springframework.boot.context.properties.ConfigurationProperties

@ConfigurationProperties("discord")
data class DiscordProperties(
val webhooks: Map<String, Webhook> = emptyMap()
) {
data class Webhook(
val id: String,
val token: String
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,23 @@ package com.dobby.external.feign.discord
import com.dobby.api.dto.request.DiscordMessageRequest
import org.springframework.cloud.openfeign.FeignClient
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody

@FeignClient(
name = "discord-alarm-feign-client",
url = "\${discord.webhook-url}"
url = "https://discord.com"
)
interface DiscordFeignClient {

@PostMapping(produces = [MediaType.APPLICATION_JSON_VALUE])
fun sendMessage(@RequestBody discordMessageRequest: DiscordMessageRequest)
@PostMapping(
value = ["/api/webhooks/{id}/{token}"],
produces = [MediaType.APPLICATION_JSON_VALUE]
)
fun sendMessage(
@PathVariable id: String,
@PathVariable token: String,
@RequestBody discordMessageRequest: DiscordMessageRequest
)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.dobby.external.gateway.discord

import com.dobby.api.dto.request.DiscordMessageRequest
import com.dobby.config.properties.DiscordProperties
import com.dobby.external.feign.discord.DiscordFeignClient
import com.dobby.gateway.AlertChannel
import com.dobby.gateway.AlertGateway
import org.springframework.stereotype.Component
import java.io.PrintWriter
Expand All @@ -10,14 +12,35 @@ import java.time.LocalDateTime

@Component
class AlertGatewayImpl(
private val discordProperties: DiscordProperties,
private val discordFeignClient: DiscordFeignClient
) : AlertGateway {

fun send(channel: AlertChannel, body: DiscordMessageRequest) {
val key = when (channel) {
AlertChannel.ERRORS -> "errors"
AlertChannel.NOTIFY -> "notify"
}
val hook = discordProperties.webhooks[key] ?: return
runCatching {
discordFeignClient.sendMessage(hook.id, hook.token, body)
}.onFailure {
}
}
Comment on lines +19 to +29
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

웹훅 전송 실패 시 로깅을 추가하세요.

onFailure 블록이 비어있어 Discord 메시지 전송 실패가 조용히 무시됩니다. 디버깅과 모니터링을 위해 실패 시 로그를 남겨야 합니다.

다음과 같이 로깅을 추가하는 것을 권장합니다:

         runCatching {
             discordFeignClient.sendMessage(hook.id, hook.token, body)
         }.onFailure {
+            logger.error("Failed to send Discord message to channel: $channel", it)
         }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
in
infrastructure/src/main/kotlin/com/dobby/external/gateway/discord/AlertGatewayImpl.kt
around lines 19-29, the onFailure block swallowing exceptions causes webhook
send failures to be silent; update the onFailure to log the failure with context
(e.g., channel name and hook id but not tokens) and the exception stacktrace
(e.g., logger.error("Failed to send Discord webhook for channel=$channel
hookId=${hook.id}", it)), so failures are visible for debugging/monitoring while
preserving current control flow.


override fun sendError(e: Exception, requestUrl: String, clientIp: String?) {
sendMessage(createMessage(e, requestUrl, clientIp))
send(AlertChannel.ERRORS, createErrorMessage(e, requestUrl, clientIp))
}

override fun sendNotify(
title: String,
description: String,
content: String?
) {
send(AlertChannel.NOTIFY, createNotifyMessage(title, description, content))
}

private fun createMessage(e: Exception, requestUrl: String, clientIp: String?): DiscordMessageRequest {
private fun createErrorMessage(e: Exception, requestUrl: String, clientIp: String?): DiscordMessageRequest {
return DiscordMessageRequest(
content = "# 🚨 에러 발생 비이이이이사아아아앙",
embeds = listOf(
Expand All @@ -26,13 +49,13 @@ class AlertGatewayImpl(
description = """
### 🕖 발생 시간
${LocalDateTime.now()}

### 🔗 요청 URL
$requestUrl

### 🖥️ 클라이언트 IP
${clientIp ?: "알 수 없음"}

### 📄 Stack Trace
```
${getStackTrace(e).substring(0, 1000)}
Expand All @@ -42,14 +65,26 @@ class AlertGatewayImpl(
)
)
}
fun createNotifyMessage(
title: String,
description: String,
content: String? = null
): DiscordMessageRequest {
val embeds = listOf(
DiscordMessageRequest.Embed(
title = "📣 $title",
description = description
)
)
return DiscordMessageRequest(
content = content,
embeds = embeds
)
}

private fun getStackTrace(e: Exception): String {
val stringWriter = StringWriter()
e.printStackTrace(PrintWriter(stringWriter))
return stringWriter.toString()
}

private fun sendMessage(request: DiscordMessageRequest) {
discordFeignClient.sendMessage(request)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ import jakarta.persistence.EntityManager
import jakarta.persistence.PersistenceContext
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.MDC
import org.springframework.stereotype.Repository
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.Period
import java.time.format.DateTimeFormatter

@Repository
class ExperimentPostCustomRepositoryImpl(
Expand All @@ -43,6 +45,9 @@ class ExperimentPostCustomRepositoryImpl(
private lateinit var entityManager: EntityManager

private val logger: Logger = LoggerFactory.getLogger(this::class.java)
companion object {
private val LOG_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
}

override fun findExperimentPostsByCustomFilter(
customFilter: CustomFilter,
Expand Down Expand Up @@ -263,7 +268,7 @@ class ExperimentPostCustomRepositoryImpl(
)
.fetch()

logger.info("[쿼리 결과] participants count: {}", participants.size)
logger.info("[쿼리 결과] 알림 수신에 동의한 participants count: {}", participants.size)

participants.take(10).forEachIndexed { index, tuple ->
val participantEntity = tuple.get(participant)
Expand Down Expand Up @@ -307,6 +312,12 @@ class ExperimentPostCustomRepositoryImpl(
}
}.toMap()

MDC.put("match.windowStart", lastProcessedTime.format(LOG_TIME_FORMATTER))
MDC.put("match.windowEnd", currentTime.format(LOG_TIME_FORMATTER))
MDC.put("match.todayPosts", todayPosts.size.toString())
MDC.put("match.consentParticipants", participants.size.toString())
MDC.put("match.matchedRecipients", resultMap.size.toString())

logger.info("[최종 결과] 이메일을 받을 대상자 수: {}", resultMap.size)

return resultMap
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.dobby.scheduler

import com.dobby.gateway.AlertGateway
import com.dobby.service.EmailService
import com.dobby.usecase.member.email.GetMatchingExperimentPostsUseCase
import com.dobby.usecase.member.email.SendMatchingEmailUseCase
Expand All @@ -8,21 +9,26 @@ import org.quartz.Job
import org.quartz.JobExecutionContext
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.MDC
import org.springframework.stereotype.Component
import java.time.Duration
import java.time.LocalDate

@Component
class SendMatchingEmailJob(
private val emailService: EmailService
private val emailService: EmailService,
private val alertGateway: AlertGateway
) : Job {
companion object {
private val logger: Logger = LoggerFactory.getLogger(SendMatchingEmailJob::class.java)
}

override fun execute(context: JobExecutionContext) {
logger.info("BulkSendMatchingEmailJob started at ${TimeProvider.currentDateTime()}")
val start = TimeProvider.currentDateTime()
logger.info("BulkSendMatchingEmailJob started at $start")

val input = GetMatchingExperimentPostsUseCase.Input(
requestTime = TimeProvider.currentDateTime()
requestTime = start
)
val output = emailService.getMatchingInfo(input)
val matchingExperimentPosts = output.matchingPosts
Expand All @@ -32,25 +38,65 @@ class SendMatchingEmailJob(

for ((contactEmail, jobList) in matchingExperimentPosts) {
if (jobList.isEmpty()) continue

val emailInput = SendMatchingEmailUseCase.Input(
contactEmail = contactEmail,
experimentPosts = jobList,
currentDateTime = TimeProvider.currentDateTime()
)
try {
val emailOutput = emailService.sendMatchingEmail(emailInput)
if (emailOutput.isSuccess) {

runCatching {
emailService.sendMatchingEmail(emailInput)
}.onSuccess { result ->
if (result.isSuccess) {
successCount++
logger.info("Email sent successfully to $contactEmail")
} else {
failureCount++
logger.error("Failed to send email to $contactEmail: ${emailOutput.message}")
logger.error("Failed to send email to $contactEmail: ${result.message}")
}
} catch (e: Exception) {
}.onFailure { e ->
failureCount++
logger.error("Exception occurred while sending email to $contactEmail", e)
logger.error("Exception while sending email to $contactEmail", e)
}
}
logger.info("SendMatchingEmailJob completed. Success: $successCount, Failures: $failureCount")

val end = TimeProvider.currentDateTime()
val took = Duration.between(start, end).toSeconds()

val windowStart = MDC.get("match.windowStart") ?: start.toString()
val windowEnd = MDC.get("match.windowEnd") ?: end.toString()
val todayCount = MDC.get("match.todayPosts")?.toIntOrNull() ?: 0
val consentParticipants = MDC.get("match.consentParticipants")?.toIntOrNull() ?: 0
val matchedRecipients = MDC.get("match.matchedRecipients")?.toIntOrNull() ?: matchingExperimentPosts.size

listOf(
"match.windowStart",
"match.windowEnd",
"match.todayPosts",
"match.consentParticipants",
"match.matchedRecipients"
).forEach { MDC.remove(it) }

logger.info("SendMatchingEmailJob completed. Success=$successCount, Failures=$failureCount, Took=${took}s")

val today = LocalDate.now()

alertGateway.sendNotify(
title = "# 📤 $today 매칭 메일 발송 결과",
description = """
**집계 구간**: $windowStart ~ $windowEnd (KST)
🗓️ **오늘 올라온 공고 수**: **$todayCount** 명
👤 **알림 동의한 전체 수**: **$consentParticipants** 명
✉️ **매칭 발송된 대상자(이메일)**: **$matchedRecipients** 명

---
✅ **발송 성공**: **$successCount** 건
❌ **발송 실패**: **$failureCount** 건
⏰ **실행 시간**: $took 초
🕒 **완료 시각**: $end
""".trimIndent(),
content = "@here"
)
}
}
8 changes: 7 additions & 1 deletion infrastructure/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,13 @@ cloud:
secret-key: ${S3_SECRET_KEY}

discord:
webhook-url: ${DISCORD_WEBHOOK_URL}
webhooks:
errors:
id: ${DISCORD_ERRORS_ID}
token: ${DISCORD_ERRORS_TOKEN}
notify:
id: ${DISCORD_NOTIFY_ID}
token: ${DISCORD_NOTIFY_TOKEN}

cors:
allowed-origins:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,13 @@ cloud:
secret-key: ${S3_SECRET_KEY}

discord:
webhook-url: ${DISCORD_WEBHOOK_URL}
webhooks:
errors:
id: ${DISCORD_ERRORS_ID}
token: ${DISCORD_ERRORS_TOKEN}
notify:
id: ${DISCORD_NOTIFY_ID}
token: ${DISCORD_NOTIFY_TOKEN}

cors:
allowed-origins:
Expand Down
Loading