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 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 {
}
}

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,27 @@ 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
import java.time.format.DateTimeFormatter

@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 +39,67 @@ 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 rawEnd = TimeProvider.currentDateTime()
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
val end = rawEnd.format(formatter)
val took = Duration.between(start, rawEnd).toSeconds()

val windowStart = MDC.get("match.windowStart") ?: start.toString()
val windowEnd = MDC.get("match.windowEnd") ?: rawEnd.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