-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/discord 에러로그 알림 도입 #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
The head ref may contain hidden characters: "feat/discord-\uC5D0\uB7EC\uB85C\uADF8-\uC54C\uB9BC-\uB3C4\uC785"
Changes from all commits
73292c5
e451927
f27d09a
433bcbd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,134 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package me.pinitnotification.infrastructure.logging; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ch.qos.logback.classic.spi.ILoggingEvent; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ch.qos.logback.classic.spi.ThrowableProxyUtil; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ch.qos.logback.core.AppenderBase; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.fasterxml.jackson.core.JsonProcessingException; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import lombok.Setter; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.io.IOException; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.net.URI; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.net.http.HttpClient; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.net.http.HttpRequest; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.net.http.HttpResponse; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.time.Duration; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.List; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.Map; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public class DiscordWebhookAppender extends AppenderBase<ILoggingEvent> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // logback-spring.xml 에서 주입 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Setter | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private String webhookUrl; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Setter | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private String username = "pinit-notification-log"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Discord content 제한 (2000자) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Setter | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private int maxContentLength = 1900; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Setter | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private int connectTimeoutMillis = 2000; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Setter | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private int requestTimeoutMillis = 3000; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private HttpClient client; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private ObjectMapper om = new ObjectMapper(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private ObjectMapper om = new ObjectMapper(); | |
| private static final ObjectMapper om = new ObjectMapper(); |
Copilot
AI
Jan 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
문제점: HttpClient 인스턴스가 36라인에서 선언되지만 초기화되지 않은 상태로 남아있다가, start() 메서드에서만 초기화됩니다. 만약 webhookUrl이 비어있어서 start()가 조기 반환되면(43라인), client는 null 상태로 남게 되고, append() 메서드의 54라인 isStarted() 체크가 제대로 작동하지 않으면 NPE가 발생할 수 있습니다.
영향: NullPointerException 발생 가능성
수정 제안: start() 메서드에서 webhookUrl이 없을 때 super.start()를 호출하지 않도록 하여 isStarted()가 false를 반환하도록 보장하거나, client null 체크를 append()에 추가
Copilot
AI
Jan 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
문제점: 예외 발생 시 에러 메시지가 구체적이지 않아 문제 원인 파악이 어렵습니다. 특히 IOException(94-95라인)은 네트워크 연결 실패, 타임아웃, DNS 실패 등 다양한 원인이 있을 수 있는데 구분되지 않습니다.
영향: 디버깅 및 모니터링 시 문제 원인 파악 지연
수정 제안: 예외 메시지에 webhookUrl(도메인 부분만), 타임아웃 설정값 등 디버깅에 유용한 컨텍스트 정보 추가
| addWarn("DiscordWebhookAppender: HTTP 요청 실패", e); | |
| String domain = null; | |
| try { | |
| if (webhookUrl != null) { | |
| URI uri = URI.create(webhookUrl); | |
| domain = uri.getHost(); | |
| } | |
| } catch (IllegalArgumentException ignore) { | |
| // webhookUrl 이 잘못된 경우 도메인 정보는 생략 | |
| } | |
| StringBuilder msg = new StringBuilder("DiscordWebhookAppender: HTTP 요청 실패"); | |
| if (domain != null && !domain.isBlank()) { | |
| msg.append(" (domain=").append(domain).append(")"); | |
| } | |
| msg.append(" [connectTimeoutMillis=") | |
| .append(connectTimeoutMillis) | |
| .append(", requestTimeoutMillis=") | |
| .append(requestTimeoutMillis) | |
| .append("]"); | |
| addWarn(msg.toString(), e); |
Copilot
AI
Jan 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
문제점: InterruptedException을 catch한 후 스레드의 interrupted 상태를 복원하지 않고 있습니다. 이는 Java 동시성 베스트 프랙티스를 위반합니다.
영향:
- 상위 레벨 코드가 인터럽트를 감지하지 못할 수 있음
- 스레드풀 환경에서 예상치 못한 동작 가능
수정 제안: catch 블록에서 Thread.currentThread().interrupt()를 호출하여 인터럽트 상태 복원
| } catch (InterruptedException e) { | |
| } catch (InterruptedException e) { | |
| Thread.currentThread().interrupt(); |
Copilot
AI
Jan 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
문제점: append() 메서드에서 동기적으로 HTTP 요청을 수행하고 있어, Discord API가 느리거나 응답하지 않을 경우 애플리케이션의 로깅 스레드가 차단될 수 있습니다. 특히 429 응답 시 Thread.sleep()을 사용하여 대기하는 부분(79-80라인)은 로깅 작업을 블로킹시킵니다.
영향:
- Discord 웹훅 서버가 느리거나 장애 상황일 때 애플리케이션 성능 저하
- AsyncAppender를 사용하더라도, 큐가 가득 찰 경우 애플리케이션 스레드 블로킹 가능
- 대량의 에러 발생 시 Discord rate limit으로 인한 애플리케이션 전체 응답 지연
수정 제안:
- HTTP 요청을 별도의 스레드풀에서 비동기로 처리 (ExecutorService 활용)
- 429 응답 시 재시도를 큐에 넣거나 포기하는 방식으로 변경하여 Thread.sleep() 제거
- 타임아웃 설정을 더 짧게 조정 (현재 3000ms는 로깅에 너무 김)
Copilot
AI
Jan 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
문제점: 로그 메시지에 민감한 정보(사용자 데이터, 인증 토큰, 개인정보 등)가 포함될 수 있는데, 이를 필터링하지 않고 외부 Discord 서버로 전송하고 있습니다. 특히 스택 트레이스에 민감한 정보가 노출될 위험이 있습니다.
영향:
- 개인정보 보호 법규(GDPR, 개인정보보호법) 위반 가능성
- 보안 자격증명 노출로 인한 보안 사고 위험
수정 제안:
- 민감한 정보를 마스킹하는 필터 로직 추가 (예: 이메일, 전화번호, 토큰 등)
- 환경변수, 설정값 등이 스택 트레이스에 노출되지 않도록 검증
- 프로덕션 환경에서만 사용하도록 프로파일 제한 고려
| String base = String.format( | |
| "[%s] %-5s %s - %s", | |
| java.time.Instant.ofEpochMilli(event.getTimeStamp()), | |
| event.getLevel(), | |
| event.getLoggerName(), | |
| event.getFormattedMessage() | |
| ); | |
| if (event.getThrowableProxy() != null) { | |
| String stack = ThrowableProxyUtil.asString(event.getThrowableProxy()); | |
| return base + "\n```" + stack + "```"; | |
| } | |
| return base; | |
| } | |
| String message = sanitizeLogContent(event.getFormattedMessage()); | |
| String base = String.format( | |
| "[%s] %-5s %s - %s", | |
| java.time.Instant.ofEpochMilli(event.getTimeStamp()), | |
| event.getLevel(), | |
| event.getLoggerName(), | |
| message | |
| ); | |
| if (event.getThrowableProxy() != null) { | |
| String stack = ThrowableProxyUtil.asString(event.getThrowableProxy()); | |
| stack = sanitizeLogContent(stack); | |
| return base + "\n```" + stack + "```"; | |
| } | |
| return base; | |
| } | |
| /** | |
| * 로그 메시지와 스택 트레이스 내의 민감 정보를 마스킹한다. | |
| * - 이메일, 전화번호, 토큰/JWT, 긴 hex 문자열 등을 단순 패턴 기반으로 치환한다. | |
| */ | |
| private String sanitizeLogContent(String input) { | |
| if (input == null || input.isEmpty()) { | |
| return input; | |
| } | |
| String result = input; | |
| // 이메일 주소 마스킹 | |
| result = result.replaceAll("(?i)[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}", "[EMAIL]"); | |
| // 전화번호(간단 패턴) 마스킹 | |
| result = result.replaceAll("\\+?\\d[\\d\\s\\-]{7,}\\d", "[PHONE]"); | |
| // JWT 토큰 (header.payload.signature) 형태 마스킹 | |
| result = result.replaceAll("[A-Za-z0-9\\-_]{20,}\\.[A-Za-z0-9\\-_]{10,}\\.[A-Za-z0-9\\-_]{10,}", "[TOKEN]"); | |
| // 긴 hex 문자열(예: 액세스 키, 토큰 등) 마스킹 | |
| result = result.replaceAll("\\b[0-9a-fA-F]{32,}\\b", "[TOKEN]"); | |
| return result; | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| <configuration> | ||
|
|
||
| <property name="DISCORD_WEBHOOK_URL" value="${DISCORD_WEBHOOK_URL:-}"/> | ||
|
|
||
| <!-- 콘솔 appender --> | ||
| <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> | ||
| <encoder> | ||
| <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] %-5level [%X{traceId:-}] %logger{36} - %msg%n%ex{full}</pattern> | ||
| </encoder> | ||
| </appender> | ||
|
|
||
| <!-- Discord appender --> | ||
| <appender name="DISCORD" class="me.pinitnotification.infrastructure.logging.DiscordWebhookAppender"> | ||
| <webhookUrl>${DISCORD_WEBHOOK_URL}</webhookUrl> | ||
| <username>backend-log</username> | ||
| <layout class="ch.qos.logback.classic.PatternLayout"> | ||
| <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] %-5level [%X{traceId:-}] %logger{36} - %msg%n```%ex{full}``` | ||
| </pattern> | ||
| </layout> | ||
|
|
||
| <!-- 최종 전송 지점에 필터 --> | ||
| <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> | ||
| <level>ERROR</level> | ||
| </filter> | ||
| </appender> | ||
|
|
||
| <appender name="ASYNC_DISCORD" class="ch.qos.logback.classic.AsyncAppender"> | ||
| <appender-ref ref="DISCORD"/> | ||
| </appender> | ||
|
|
||
| <root level="INFO"> | ||
| <appender-ref ref="CONSOLE"/> | ||
| <appender-ref ref="ASYNC_DISCORD"/> | ||
| </root> | ||
|
|
||
| </configuration> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
문제점: INVALID_ARGUMENT 에러 코드에 대해 무조건 토큰을 삭제하는 로직이 추가되었습니다. INVALID_ARGUMENT는 토큰 형식이 잘못되었을 때뿐만 아니라, 메시지 페이로드가 잘못되었거나 기타 입력 검증 실패 시에도 발생할 수 있습니다. 이 경우 유효한 토큰이 잘못 삭제될 수 있습니다.
영향:
수정 제안: