Skip to content

Commit 176f33c

Browse files
committed
[YS-571] feature: 공고 매칭 전송 시, 디스코드 알림 채널로 daily Job 정보 전송하도록 설정 (#171)
* feature: add NOTIFY enums to seperate DiscordFeign channel * feature: apply MDC ThreadLocal pattern to capture matching summary logs to send discord message * style: apply ktlintFormat * fix: fix count units * style: format time info
1 parent 385be48 commit 176f33c

9 files changed

Lines changed: 160 additions & 25 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.dobby.gateway
2+
3+
enum class AlertChannel {
4+
ERRORS,
5+
NOTIFY
6+
}

domain/src/main/kotlin/com/dobby/gateway/AlertGateway.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ package com.dobby.gateway
22

33
interface AlertGateway {
44
fun sendError(e: Exception, requestUrl: String, clientIp: String?)
5+
fun sendNotify(title: String, description: String, content: String?)
56
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.dobby.config.properties
2+
3+
import org.springframework.boot.context.properties.ConfigurationProperties
4+
5+
@ConfigurationProperties("discord")
6+
data class DiscordProperties(
7+
val webhooks: Map<String, Webhook> = emptyMap()
8+
) {
9+
data class Webhook(
10+
val id: String,
11+
val token: String
12+
)
13+
}

infrastructure/src/main/kotlin/com/dobby/external/feign/discord/DiscordFeignClient.kt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,23 @@ package com.dobby.external.feign.discord
33
import com.dobby.api.dto.request.DiscordMessageRequest
44
import org.springframework.cloud.openfeign.FeignClient
55
import org.springframework.http.MediaType
6+
import org.springframework.web.bind.annotation.PathVariable
67
import org.springframework.web.bind.annotation.PostMapping
78
import org.springframework.web.bind.annotation.RequestBody
89

910
@FeignClient(
1011
name = "discord-alarm-feign-client",
11-
url = "\${discord.webhook-url}"
12+
url = "https://discord.com"
1213
)
1314
interface DiscordFeignClient {
1415

15-
@PostMapping(produces = [MediaType.APPLICATION_JSON_VALUE])
16-
fun sendMessage(@RequestBody discordMessageRequest: DiscordMessageRequest)
16+
@PostMapping(
17+
value = ["/api/webhooks/{id}/{token}"],
18+
produces = [MediaType.APPLICATION_JSON_VALUE]
19+
)
20+
fun sendMessage(
21+
@PathVariable id: String,
22+
@PathVariable token: String,
23+
@RequestBody discordMessageRequest: DiscordMessageRequest
24+
)
1725
}
Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.dobby.external.gateway.discord
22

33
import com.dobby.api.dto.request.DiscordMessageRequest
4+
import com.dobby.config.properties.DiscordProperties
45
import com.dobby.external.feign.discord.DiscordFeignClient
6+
import com.dobby.gateway.AlertChannel
57
import com.dobby.gateway.AlertGateway
68
import org.springframework.stereotype.Component
79
import java.io.PrintWriter
@@ -10,14 +12,35 @@ import java.time.LocalDateTime
1012

1113
@Component
1214
class AlertGatewayImpl(
15+
private val discordProperties: DiscordProperties,
1316
private val discordFeignClient: DiscordFeignClient
1417
) : AlertGateway {
1518

19+
fun send(channel: AlertChannel, body: DiscordMessageRequest) {
20+
val key = when (channel) {
21+
AlertChannel.ERRORS -> "errors"
22+
AlertChannel.NOTIFY -> "notify"
23+
}
24+
val hook = discordProperties.webhooks[key] ?: return
25+
runCatching {
26+
discordFeignClient.sendMessage(hook.id, hook.token, body)
27+
}.onFailure {
28+
}
29+
}
30+
1631
override fun sendError(e: Exception, requestUrl: String, clientIp: String?) {
17-
sendMessage(createMessage(e, requestUrl, clientIp))
32+
send(AlertChannel.ERRORS, createErrorMessage(e, requestUrl, clientIp))
33+
}
34+
35+
override fun sendNotify(
36+
title: String,
37+
description: String,
38+
content: String?
39+
) {
40+
send(AlertChannel.NOTIFY, createNotifyMessage(title, description, content))
1841
}
1942

20-
private fun createMessage(e: Exception, requestUrl: String, clientIp: String?): DiscordMessageRequest {
43+
private fun createErrorMessage(e: Exception, requestUrl: String, clientIp: String?): DiscordMessageRequest {
2144
return DiscordMessageRequest(
2245
content = "# 🚨 에러 발생 비이이이이사아아아앙",
2346
embeds = listOf(
@@ -26,13 +49,13 @@ class AlertGatewayImpl(
2649
description = """
2750
### 🕖 발생 시간
2851
${LocalDateTime.now()}
29-
52+
3053
### 🔗 요청 URL
3154
$requestUrl
32-
55+
3356
### 🖥️ 클라이언트 IP
3457
${clientIp ?: "알 수 없음"}
35-
58+
3659
### 📄 Stack Trace
3760
```
3861
${getStackTrace(e).substring(0, 1000)}
@@ -42,14 +65,26 @@ class AlertGatewayImpl(
4265
)
4366
)
4467
}
68+
fun createNotifyMessage(
69+
title: String,
70+
description: String,
71+
content: String? = null
72+
): DiscordMessageRequest {
73+
val embeds = listOf(
74+
DiscordMessageRequest.Embed(
75+
title = "📣 $title",
76+
description = description
77+
)
78+
)
79+
return DiscordMessageRequest(
80+
content = content,
81+
embeds = embeds
82+
)
83+
}
4584

4685
private fun getStackTrace(e: Exception): String {
4786
val stringWriter = StringWriter()
4887
e.printStackTrace(PrintWriter(stringWriter))
4988
return stringWriter.toString()
5089
}
51-
52-
private fun sendMessage(request: DiscordMessageRequest) {
53-
discordFeignClient.sendMessage(request)
54-
}
5590
}

infrastructure/src/main/kotlin/com/dobby/persistence/repository/ExperimentPostCustomRepositoryImpl.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@ import jakarta.persistence.EntityManager
3030
import jakarta.persistence.PersistenceContext
3131
import org.slf4j.Logger
3232
import org.slf4j.LoggerFactory
33+
import org.slf4j.MDC
3334
import org.springframework.stereotype.Repository
3435
import java.time.LocalDate
3536
import java.time.LocalDateTime
3637
import java.time.Period
38+
import java.time.format.DateTimeFormatter
3739

3840
@Repository
3941
class ExperimentPostCustomRepositoryImpl(
@@ -43,6 +45,9 @@ class ExperimentPostCustomRepositoryImpl(
4345
private lateinit var entityManager: EntityManager
4446

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

4752
override fun findExperimentPostsByCustomFilter(
4853
customFilter: CustomFilter,
@@ -263,7 +268,7 @@ class ExperimentPostCustomRepositoryImpl(
263268
)
264269
.fetch()
265270

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

268273
participants.take(10).forEachIndexed { index, tuple ->
269274
val participantEntity = tuple.get(participant)
@@ -307,6 +312,12 @@ class ExperimentPostCustomRepositoryImpl(
307312
}
308313
}.toMap()
309314

315+
MDC.put("match.windowStart", lastProcessedTime.format(LOG_TIME_FORMATTER))
316+
MDC.put("match.windowEnd", currentTime.format(LOG_TIME_FORMATTER))
317+
MDC.put("match.todayPosts", todayPosts.size.toString())
318+
MDC.put("match.consentParticipants", participants.size.toString())
319+
MDC.put("match.matchedRecipients", resultMap.size.toString())
320+
310321
logger.info("[최종 결과] 이메일을 받을 대상자 수: {}", resultMap.size)
311322

312323
return resultMap
Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.dobby.scheduler
22

3+
import com.dobby.gateway.AlertGateway
34
import com.dobby.service.EmailService
45
import com.dobby.usecase.member.email.GetMatchingExperimentPostsUseCase
56
import com.dobby.usecase.member.email.SendMatchingEmailUseCase
@@ -8,21 +9,27 @@ import org.quartz.Job
89
import org.quartz.JobExecutionContext
910
import org.slf4j.Logger
1011
import org.slf4j.LoggerFactory
12+
import org.slf4j.MDC
1113
import org.springframework.stereotype.Component
14+
import java.time.Duration
15+
import java.time.LocalDate
16+
import java.time.format.DateTimeFormatter
1217

1318
@Component
1419
class SendMatchingEmailJob(
15-
private val emailService: EmailService
20+
private val emailService: EmailService,
21+
private val alertGateway: AlertGateway
1622
) : Job {
1723
companion object {
1824
private val logger: Logger = LoggerFactory.getLogger(SendMatchingEmailJob::class.java)
1925
}
2026

2127
override fun execute(context: JobExecutionContext) {
22-
logger.info("BulkSendMatchingEmailJob started at ${TimeProvider.currentDateTime()}")
28+
val start = TimeProvider.currentDateTime()
29+
logger.info("BulkSendMatchingEmailJob started at $start")
2330

2431
val input = GetMatchingExperimentPostsUseCase.Input(
25-
requestTime = TimeProvider.currentDateTime()
32+
requestTime = start
2633
)
2734
val output = emailService.getMatchingInfo(input)
2835
val matchingExperimentPosts = output.matchingPosts
@@ -32,25 +39,67 @@ class SendMatchingEmailJob(
3239

3340
for ((contactEmail, jobList) in matchingExperimentPosts) {
3441
if (jobList.isEmpty()) continue
42+
3543
val emailInput = SendMatchingEmailUseCase.Input(
3644
contactEmail = contactEmail,
3745
experimentPosts = jobList,
3846
currentDateTime = TimeProvider.currentDateTime()
3947
)
40-
try {
41-
val emailOutput = emailService.sendMatchingEmail(emailInput)
42-
if (emailOutput.isSuccess) {
48+
49+
runCatching {
50+
emailService.sendMatchingEmail(emailInput)
51+
}.onSuccess { result ->
52+
if (result.isSuccess) {
4353
successCount++
4454
logger.info("Email sent successfully to $contactEmail")
4555
} else {
4656
failureCount++
47-
logger.error("Failed to send email to $contactEmail: ${emailOutput.message}")
57+
logger.error("Failed to send email to $contactEmail: ${result.message}")
4858
}
49-
} catch (e: Exception) {
59+
}.onFailure { e ->
5060
failureCount++
51-
logger.error("Exception occurred while sending email to $contactEmail", e)
61+
logger.error("Exception while sending email to $contactEmail", e)
5262
}
5363
}
54-
logger.info("SendMatchingEmailJob completed. Success: $successCount, Failures: $failureCount")
64+
65+
val rawEnd = TimeProvider.currentDateTime()
66+
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
67+
val end = rawEnd.format(formatter)
68+
val took = Duration.between(start, rawEnd).toSeconds()
69+
70+
val windowStart = MDC.get("match.windowStart") ?: start.toString()
71+
val windowEnd = MDC.get("match.windowEnd") ?: rawEnd.toString()
72+
val todayCount = MDC.get("match.todayPosts")?.toIntOrNull() ?: 0
73+
val consentParticipants = MDC.get("match.consentParticipants")?.toIntOrNull() ?: 0
74+
val matchedRecipients = MDC.get("match.matchedRecipients")?.toIntOrNull() ?: matchingExperimentPosts.size
75+
76+
listOf(
77+
"match.windowStart",
78+
"match.windowEnd",
79+
"match.todayPosts",
80+
"match.consentParticipants",
81+
"match.matchedRecipients"
82+
).forEach { MDC.remove(it) }
83+
84+
logger.info("SendMatchingEmailJob completed. Success=$successCount, Failures=$failureCount, Took=${took}s")
85+
86+
val today = LocalDate.now()
87+
88+
alertGateway.sendNotify(
89+
title = "# 📤 $today 매칭 메일 발송 결과",
90+
description = """
91+
**집계 구간**: $windowStart ~ $windowEnd (KST)
92+
🗓️ **오늘 올라온 공고 수**: **$todayCount** 건
93+
👤 **알림 동의한 전체 수**: **$consentParticipants** 명
94+
✉️ **매칭 발송된 대상자(이메일)**: **$matchedRecipients** 명
95+
96+
---
97+
✅ **발송 성공**: **$successCount** 건
98+
❌ **발송 실패**: **$failureCount** 건
99+
⏰ **실행 시간**: $took
100+
🕒 **완료 시각**: $end
101+
""".trimIndent(),
102+
content = "@here"
103+
)
55104
}
56105
}

infrastructure/src/main/resources/application.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,13 @@ cloud:
7878
secret-key: ${S3_SECRET_KEY}
7979

8080
discord:
81-
webhook-url: ${DISCORD_WEBHOOK_URL}
81+
webhooks:
82+
errors:
83+
id: ${DISCORD_ERRORS_ID}
84+
token: ${DISCORD_ERRORS_TOKEN}
85+
notify:
86+
id: ${DISCORD_NOTIFY_ID}
87+
token: ${DISCORD_NOTIFY_TOKEN}
8288

8389
cors:
8490
allowed-origins:

infrastructure/src/main/resources/template-application-local.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,13 @@ cloud:
8181
secret-key: ${S3_SECRET_KEY}
8282

8383
discord:
84-
webhook-url: ${DISCORD_WEBHOOK_URL}
84+
webhooks:
85+
errors:
86+
id: ${DISCORD_ERRORS_ID}
87+
token: ${DISCORD_ERRORS_TOKEN}
88+
notify:
89+
id: ${DISCORD_NOTIFY_ID}
90+
token: ${DISCORD_NOTIFY_TOKEN}
8591

8692
cors:
8793
allowed-origins:

0 commit comments

Comments
 (0)