Skip to content

Commit db2c343

Browse files
authored
Merge pull request #61 from Nexters/feature/discord-notification
feat: 디스코드 로그 알림 설정
2 parents 003e0a0 + 837c650 commit db2c343

File tree

16 files changed

+481
-9
lines changed

16 files changed

+481
-9
lines changed

build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ configurations {
2121

2222
repositories {
2323
mavenCentral()
24+
maven { url 'https://jitpack.io' }
2425
}
2526

2627
dependencies {
@@ -49,6 +50,9 @@ dependencies {
4950

5051
// ncp
5152
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.470'
53+
54+
// discord
55+
implementation 'com.github.napstr:logback-discord-appender:1.0.0'
5256
}
5357

5458
tasks.named('test') {

docker-compose.dev.yaml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,12 @@ services:
3737
- JWT_SECRET_KEY=${JWT_SECRET_KEY}
3838
- OAUTH_AUTHORIZED_REDIRECT_URIS=${OAUTH_AUTHORIZED_REDIRECT_URIS:-http://localhost:5173/oauth2/redirect,https://holdy.kr/oauth2/redirect}
3939
- OAUTH_DEFAULT_REDIRECT_URI=${OAUTH_DEFAULT_REDIRECT_URI:-http://localhost:5173/oauth2/redirect}
40-
- NCP_ACCESS_KEY=${NCP_ACCESS_KEY:-ncp_iam_BPAMKR621TrFvcw4G8ek}
41-
- NCP_SECRET_KEY=${NCP_SECRET_KEY:-ncp_iam_BPKMKRW1ET86Hvx1z74EaVQRqjjfQSCcj4}
40+
- NCP_ACCESS_KEY=${NCP_ACCESS_KEY}
41+
- NCP_SECRET_KEY=${NCP_SECRET_KEY}
42+
- SPRING_PROFILES_ACTIVE=${SPRING_PROFILE_ACTIVE}
43+
- DISCORD_ENABLED=${DISCORD_ENABLED}
44+
- DISCORD_ERROR_WEBHOOK_URL=${DISCORD_DEV_ERROR_WEBHOOK_URL}
45+
- DISCORD_NOTIFICATION_WEBHOOK_URL=${DISCORD_DEV_NOTIFICATION_WEBHOOK_URL}
4246
ports:
4347
- "9090:9090"
4448
depends_on:

docker-compose.prod.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ services:
3939
- OAUTH_DEFAULT_REDIRECT_URI=${OAUTH_DEFAULT_REDIRECT_URI:-https://holdy.kr/oauth2/redirect}
4040
- NCP_ACCESS_KEY=${NCP_ACCESS_KEY}
4141
- NCP_SECRET_KEY=${NCP_SECRET_KEY}
42+
- SPRING_PROFILES_ACTIVE=${SPRING_PROFILE_ACTIVE}
43+
- DISCORD_ENABLED=${DISCORD_ENABLED}
44+
- DISCORD_ERROR_WEBHOOK_URL=${DISCORD_DEV_ERROR_WEBHOOK_URL}
45+
- DISCORD_NOTIFICATION_WEBHOOK_URL=${DISCORD_DEV_NOTIFICATION_WEBHOOK_URL}
4246
ports:
4347
- "9090:9090"
4448
depends_on:

src/main/java/com/climbup/climbup/attempt/controller/AttemptController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
import java.util.UUID;
3636

3737
@RestController
38-
@RequestMapping("/attempts")
38+
@RequestMapping("/api/attempts")
3939
@Tag(name = "Attempts", description = "루트 미션 도전 관련 API")
4040
@RequiredArgsConstructor
4141
public class AttemptController {

src/main/java/com/climbup/climbup/auth/service/CustomOAuth2UserService.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,20 @@
44
import com.climbup.climbup.auth.dto.KakaoOAuth2UserInfo;
55
import com.climbup.climbup.auth.exception.NicknameGenerationException;
66
import com.climbup.climbup.auth.util.RandomNicknameGenerator;
7+
import com.climbup.climbup.global.discord.SignUpEvent;
78
import com.climbup.climbup.user.entity.User;
89
import com.climbup.climbup.user.repository.UserRepository;
910
import lombok.RequiredArgsConstructor;
1011
import lombok.extern.slf4j.Slf4j;
1112
import org.springframework.beans.factory.annotation.Value;
13+
import org.springframework.context.ApplicationEventPublisher;
1214
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
1315
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
1416
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
1517
import org.springframework.security.oauth2.core.user.OAuth2User;
1618
import org.springframework.stereotype.Service;
1719
import org.springframework.transaction.annotation.Transactional;
1820

19-
import java.util.concurrent.ThreadLocalRandom;
20-
2121
@Service
2222
@RequiredArgsConstructor
2323
@Slf4j
@@ -27,6 +27,7 @@ public class CustomOAuth2UserService extends DefaultOAuth2UserService {
2727
private int maxRetries;
2828

2929
private final UserRepository userRepository;
30+
private final ApplicationEventPublisher eventPublisher;
3031

3132
@Override
3233
@Transactional
@@ -65,7 +66,11 @@ private User createUser(KakaoOAuth2UserInfo userInfo) {
6566
.gym(null)
6667
.build();
6768

68-
return userRepository.save(user);
69+
User savedUser = userRepository.save(user);
70+
71+
eventPublisher.publishEvent(new SignUpEvent(this, nickname, savedUser.getId()));
72+
73+
return savedUser;
6974
}
7075

7176
private String generateUniqueNickname() {

src/main/java/com/climbup/climbup/common/exception/GlobalExceptionHandler.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import com.climbup.climbup.auth.exception.InvalidTokenException;
55
import com.climbup.climbup.auth.exception.TokenExpiredException;
66
import com.climbup.climbup.common.dto.ErrorResponse;
7+
import com.climbup.climbup.global.discord.DiscordService;
78
import jakarta.servlet.http.HttpServletRequest;
9+
import lombok.RequiredArgsConstructor;
810
import lombok.extern.slf4j.Slf4j;
911
import org.springframework.http.ResponseEntity;
1012
import org.springframework.http.converter.HttpMessageNotReadableException;
@@ -18,13 +20,18 @@
1820
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
1921
import org.springframework.web.servlet.NoHandlerFoundException;
2022

23+
import java.io.PrintWriter;
24+
import java.io.StringWriter;
2125
import java.util.HashMap;
2226
import java.util.Map;
2327

2428
@Slf4j
2529
@RestControllerAdvice
30+
@RequiredArgsConstructor
2631
public class GlobalExceptionHandler {
2732

33+
private final DiscordService discordService;
34+
2835
@ExceptionHandler(BusinessException.class)
2936
public ResponseEntity<ErrorResponse> handleBusinessException(
3037
BusinessException ex, HttpServletRequest request) {
@@ -139,6 +146,7 @@ public ResponseEntity<ErrorResponse> handleGenericException(
139146
Exception ex, HttpServletRequest request) {
140147

141148
log.error("Unexpected exception occurred", ex);
149+
sendDiscordErrorNotification(ex, "handleGenericException");
142150
return ResponseEntity
143151
.status(ErrorCode.INTERNAL_SERVER_ERROR.getHttpStatus())
144152
.body(ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR, request.getRequestURI()));
@@ -184,4 +192,39 @@ public ResponseEntity<ErrorResponse> handleAuthHeaderMissingException(
184192
.status(ex.getHttpStatus())
185193
.body(ErrorResponse.of(ex.getErrorCode(), request.getRequestURI(), ex.getMessageArgs()));
186194
}
195+
196+
private void sendDiscordErrorNotification(Exception ex, String methodName) {
197+
if (discordService == null) {
198+
return;
199+
}
200+
201+
try {
202+
String stackTrace = getStackTrace(ex);
203+
String className = ex.getStackTrace().length > 0 ?
204+
ex.getStackTrace()[0].getClassName() : "Unknown";
205+
String actualMethodName = ex.getStackTrace().length > 0 ?
206+
ex.getStackTrace()[0].getMethodName() : methodName;
207+
208+
discordService.sendErrorNotification(
209+
ex.getMessage() + "\n\n" + stackTrace,
210+
className,
211+
actualMethodName
212+
);
213+
214+
} catch (Exception discordEx) {
215+
log.error("Discord 알림 전송 중 오류 발생", discordEx);
216+
}
217+
}
218+
219+
private String getStackTrace(Exception ex) {
220+
StringWriter sw = new StringWriter();
221+
PrintWriter pw = new PrintWriter(sw);
222+
ex.printStackTrace(pw);
223+
224+
String fullStackTrace = sw.toString();
225+
if (fullStackTrace.length() > 1500) {
226+
return fullStackTrace.substring(0, 1500) + "\n... (truncated)";
227+
}
228+
return fullStackTrace;
229+
}
187230
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.climbup.climbup.global.config;
2+
3+
import lombok.extern.slf4j.Slf4j;
4+
import org.springframework.context.annotation.Bean;
5+
import org.springframework.context.annotation.Configuration;
6+
import org.springframework.scheduling.annotation.EnableAsync;
7+
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
8+
9+
import java.util.concurrent.Executor;
10+
11+
@Configuration
12+
@EnableAsync
13+
@Slf4j
14+
public class AsyncConfig {
15+
16+
@Bean(name = "discordNotificationExecutor")
17+
public Executor discordNotificationExecutor() {
18+
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
19+
executor.setCorePoolSize(2);
20+
executor.setMaxPoolSize(5);
21+
executor.setQueueCapacity(100);
22+
executor.setThreadNamePrefix("Discord-Notification-");
23+
executor.setRejectedExecutionHandler((r, exec) ->
24+
log.warn("Discord 알림 작업이 거부되었습니다. 큐가 가득참."));
25+
executor.initialize();
26+
return executor;
27+
}
28+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.climbup.climbup.global.discord;
2+
3+
import lombok.Getter;
4+
import lombok.Setter;
5+
import org.springframework.boot.context.properties.ConfigurationProperties;
6+
import org.springframework.context.annotation.Configuration;
7+
8+
@Configuration
9+
@ConfigurationProperties(prefix = "discord")
10+
@Getter
11+
@Setter
12+
public class DiscordConfig {
13+
14+
private Webhook webhook = new Webhook();
15+
private boolean enabled = false;
16+
17+
@Getter
18+
@Setter
19+
public static class Webhook {
20+
private String errorUrl;
21+
private String notificationUrl;
22+
}
23+
}

0 commit comments

Comments
 (0)