From e56167bd7f7401f7efb19834391fbd4a7ba70046 Mon Sep 17 00:00:00 2001 From: GoGradually Date: Thu, 1 Jan 2026 21:59:43 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EB=94=94=EC=8A=A4=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9B=B9=ED=9B=85=EC=9D=84=20=ED=86=B5=ED=95=9C=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EB=A1=9C=EA=B7=B8=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../logging/DiscordWebhookAppender.java | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 src/main/java/me/gg/pinit/infrastructure/logging/DiscordWebhookAppender.java diff --git a/src/main/java/me/gg/pinit/infrastructure/logging/DiscordWebhookAppender.java b/src/main/java/me/gg/pinit/infrastructure/logging/DiscordWebhookAppender.java new file mode 100644 index 0000000..af2ae4b --- /dev/null +++ b/src/main/java/me/gg/pinit/infrastructure/logging/DiscordWebhookAppender.java @@ -0,0 +1,134 @@ +package me.gg.pinit.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 { + + // logback-spring.xml 에서 주입 + @Setter + private String webhookUrl; + @Setter + private String username = "pinit-auth-log"; + + // Discord content 제한 (2000자) + @Setter + private int maxContentLength = 1900; + + @Setter + private int connectTimeoutMillis = 2000; + @Setter + private int requestTimeoutMillis = 3000; + + private HttpClient client; + private final ObjectMapper om = new ObjectMapper(); + + @Override + public void start() { + if (this.webhookUrl == null || this.webhookUrl.isEmpty()) { + addWarn("디스코드 웹훅 URL이 설정되지 않았습니다. DiscordWebhookAppender가 시작되지 않습니다."); + return; + } + client = HttpClient.newBuilder() + .connectTimeout(Duration.ofMillis(connectTimeoutMillis)) + .build(); + + super.start(); + } + + @Override + protected void append(ILoggingEvent event) { + if (!isStarted()) return; + + try { + String content = format(event); + content = truncate(content, maxContentLength); + + Map payload = Map.of( + "username", username, + "content", content, + "allowed_mentions", Map.of("parse", List.of()) + ); + + String json = om.writeValueAsString(payload); + + HttpRequest req = HttpRequest.newBuilder() + .uri(URI.create(webhookUrl)) + .timeout(Duration.ofMillis(requestTimeoutMillis)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(json)) + .build(); + + HttpResponse resp = client.send(req, HttpResponse.BodyHandlers.ofString()); + + if (resp.statusCode() == 429) { + long waitMs = parseRetryAfterMillis(resp); + if (waitMs > 0) + Thread.sleep(waitMs); + + HttpResponse retry = client.send(req, HttpResponse.BodyHandlers.ofString()); + if (retry.statusCode() >= 200 && retry.statusCode() < 300) { + return; // 성공 + } + addWarn("DiscordWebhookAppender: 재시도 후에도 실패, 상태 코드: " + retry.statusCode()); + return; + } + if (resp.statusCode() < 200 || resp.statusCode() >= 300) { + addWarn("DiscordWebhookAppender: HTTP 요청 실패, 상태 코드: " + resp.statusCode()); + } + } catch (JsonProcessingException e) { + addWarn("DiscordWebhookAppender: JSON 직렬화 실패", e); + } catch (IOException e) { + addWarn("DiscordWebhookAppender: HTTP 요청 실패", e); + } catch (InterruptedException e) { + addWarn("DiscordWebhookAppender: HTTP 요청이 중단됨", e); + } + } + + private String format(ILoggingEvent event) { + 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; + } + + private String truncate(String s, int max) { + if (s == null) return ""; + if (s.length() <= max) return s; + return s.substring(0, max) + "\n...(truncated)"; + } + + private long parseRetryAfterMillis(HttpResponse resp) { + String ra = resp.headers().firstValue("Retry-After").orElse(null); + if (ra == null || ra.isBlank()) return 0; + + try { + double v = Double.parseDouble(ra.trim()); + return (long) (v * 1000); + } catch (NumberFormatException ignore) { + return 0; + } + } +} From 4615babc3cf0dac4b63a79187d26a94b8759e5f6 Mon Sep 17 00:00:00 2001 From: GoGradually Date: Thu, 1 Jan 2026 22:00:03 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=EB=94=94=EC=8A=A4=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9B=B9=ED=9B=85=EC=9D=84=20=ED=86=B5=ED=95=9C=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EB=A1=9C=EA=B7=B8=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/logback-spring.xml | 30 +++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/main/resources/logback-spring.xml diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..1211c47 --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,30 @@ + + + + + + + ${DISCORD_WEBHOOK_URL} + backend-log + + + + + [%d{yyyy-MM-dd HH:mm:ss.SSS}] %-5level [%X{traceId:-}] %logger{36} - %msg%n```%ex{full}``` + + + + + + + + + ERROR + + + + + + + + \ No newline at end of file From 6e94c1d68a269356e7326ab39301150da7d6c2c7 Mon Sep 17 00:00:00 2001 From: GoGradually Date: Thu, 1 Jan 2026 22:11:55 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20Discord=20=EC=9B=B9=ED=9B=85?= =?UTF-8?q?=EC=9D=84=20=ED=86=B5=ED=95=9C=20=EC=97=90=EB=9F=AC=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=95=8C=EB=A6=BC=20=EC=84=A4=EC=A0=95=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/logback-spring.xml | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index 1211c47..38856de 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -1,29 +1,35 @@ - + + + + [%d{yyyy-MM-dd HH:mm:ss.SSS}] %-5level [%X{traceId:-}] %logger{36} - %msg%n%ex{full} + + + + ${DISCORD_WEBHOOK_URL} backend-log - - - - [%d{yyyy-MM-dd HH:mm:ss.SSS}] %-5level [%X{traceId:-}] %logger{36} - %msg%n```%ex{full}``` + [%d{yyyy-MM-dd HH:mm:ss.SSS}] %-5level [%X{traceId:-}] %logger{36} - %msg%n```%ex{full}``` - - - - + ERROR + + + + +