Skip to content
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

[FEAT] network-api - 발생한 error를 디스코드로 전송 #76

Merged
merged 5 commits into from
Dec 4, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

@Configuration
@PropertySource("classpath:/env.properties")
@Configuration("logWriterPropertiesConfig")
@PropertySource("classpath:/env-log-writer.properties")
public class PropertiesConfig {
}
23 changes: 23 additions & 0 deletions modules/infrastructure/logging/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
plugins {
id 'java'
}

group = 'com.whoz_in'
version = '0.0.1-SNAPSHOT'

repositories {
mavenCentral()
}

dependencies {
implementation 'org.apache.httpcomponents:httpclient:4.5.14'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.2'
implementation 'ch.qos.logback:logback-classic:1.5.11'

testImplementation platform('org.junit:junit-bom:5.9.1')
testImplementation 'org.junit.jupiter:junit-jupiter'
}

test {
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package com.whoz_in.logging;

import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Layout;
import ch.qos.logback.core.UnsynchronizedAppenderBase;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;

public class DiscordAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
private final CloseableHttpClient httpClient = HttpClients.createDefault();
//null인 필드는 body에 추가하지 않도록 함
private final ObjectMapper objectMapper = new ObjectMapper().setSerializationInclusion(
JsonInclude.Include.NON_NULL);

private String webhookUri;
private Layout<ILoggingEvent> layout;
private PatternLayoutEncoder encoder;
private String username;
private String avatarUrl;

@Override
public void start() {
if (webhookUri == null || webhookUri.isBlank()) {
addError("Webhook URI가 없음");
return;
}

if (layout == null){
if (encoder != null) {
layout = encoder.getLayout();
} else {
addError("layout이나 encoder가 설정되지 않음");
return;
}
}
super.start();
}

@Override
protected void append(ILoggingEvent eventObject) {
try {
// layout 적용
String message = layout.doLayout(eventObject);
// 메세지 가공
message = cleanseMessage(message);
// 디스코드 웹훅 명세에 맞게 json으로 직렬화
String jsonPayload = makeJsonPayload(message);
// 디스코드로 전송
send(jsonPayload);
} catch (Exception e) {
addError("디스코드 웹훅 요청 실패: ", e);
}
}

private String makeJsonPayload(String message) throws JsonProcessingException {
// body 만들기
Map<String, Object> payload = new HashMap<>();
payload.put("content", message);
payload.put("username", username);
payload.put("avatar_url", avatarUrl);

return objectMapper.writeValueAsString(payload);
}

private void send(String jsonPayload) throws IOException {
HttpPost post = new HttpPost(webhookUri);
post.setHeader("Content-Type", "application/json");
post.setEntity(new StringEntity(jsonPayload, "UTF-8"));

try (CloseableHttpResponse response = httpClient.execute(post)) {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != 204 && statusCode != 200) { //성공 시 204를 보내긴 하던데 일단 200도 추가
addError("" + statusCode);
}
}
}

// 디스코드는 2000자 이하만 보낼 수 있음
private String cleanseMessage(String message) {
message = message.replaceAll("```\\s```", "");
if (message.length() > 2000) {
message = message.substring(0, 1997) + "...";
}
return message;
}

//Logback XML 설정을 위한 Getter, Setter
//이 Appender 재사용할 수 있으니 Lombok 안썼음
public Layout<ILoggingEvent> getLayout() {
return layout;
}

public void setLayout(Layout<ILoggingEvent> layout) {
this.layout = layout;
}

public PatternLayoutEncoder getEncoder() {
return encoder;
}

public void setEncoder(PatternLayoutEncoder encoder) {
this.encoder = encoder;
}

public String getWebhookUri() {
return webhookUri;
}

public void setWebhookUri(String webhookUri) {
this.webhookUri = webhookUri;
}

public void setUsername(String username) {
this.username = username;
}

public String getAvatarUrl() {
return avatarUrl;
}

public void setAvatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
}
}
1 change: 1 addition & 0 deletions modules/network-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ repositories {
}

dependencies {
implementation project(':modules:infrastructure:logging')
implementation project(':modules:infrastructure:log-writer')

implementation 'org.springframework.boot:spring-boot-starter'
Expand Down
7 changes: 7 additions & 0 deletions modules/network-api/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
spring:
application:
name: WhozIn-Network
#@PropertySource는 스프링 컨텍스트가 초기화된 이후 처리되기 때문에 여기서 env를 미리 로드
config:
import: "classpath:env-network-api.properties"

profiles:
group:
local:
prod:
active: local
include: log-writer

logging:
discord:
webhook-url: ${DISCORD_WEBHOOK_URL}
69 changes: 69 additions & 0 deletions modules/network-api/src/main/resources/logback-spring.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<!-- 스프링 컨텍스트가 완전히 초기화되기 전에 읽으므로 @PropertyConfig가 아닌 yml에서 env를 로드해야 함-->

<configuration>
<springProfile name="!prod">
<root level="INFO">
<appender-ref ref="COLOR_CONSOLE"/>
</root>
</springProfile>

<springProfile name="prod">
<root level="INFO">
<appender-ref ref="NO_COLOR_CONSOLE"/>
<appender-ref ref="FILE"/>
<appender-ref ref="ASYNC_DISCORD_ERROR"/>
</root>
</springProfile>


<property name="COLOR_LOG_PATTERN" value="[%d{yyyy-MM-dd HH:mm:ss}] %boldWhite([%15.15thread]) %boldWhite([%36.36logger{36}]) %highlight(%-5level) - %msg%n"/>
<appender name="COLOR_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${COLOR_LOG_PATTERN}</pattern>
</encoder>
</appender>

<property name="NO_COLOR_LOG_PATTERN" value="[%d{yyyy-MM-dd HH:mm:ss}] [%15.15thread] [%36.36logger{36}] %-5level - %msg%n"/>
<appender name="NO_COLOR_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${NO_COLOR_LOG_PATTERN}</pattern>
</encoder>
</appender>

<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<!-- 작성 완료되기 전의 로그 파일 이름 -->
<file>${user.home}/whozin_log/temp.log</file>
<!-- 시간을 기준으로 파일을 관리함 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 하루 단위(%d)로 log를 남기며 최대 용량을 넘어가면 %i로 구별하여 저장 -->
<fileNamePattern>log_%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 한 파일은 최대 10MB -->
<maxFileSize>10MB</maxFileSize>
<!-- 로그 파일들은 30일(%d)까지 유지-->
<maxHistory>30</maxHistory>
<!-- 모든 로그 파일은 5GB를 넘기면 안됨 -->
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>[%d{HH:mm:ss}] [%thread] %level %logger{0} - %msg %n</pattern>
</encoder>
</appender>

<!-- AsyncAppender가 Appender를 감싸서 디스코드 로깅을 비동기로 처리할 수 있도록 함. -->
<appender name="ASYNC_DISCORD_ERROR" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="DISCORD_ERROR"/>
</appender>
<!-- Web hook url은 외부에 공개하면 안되므로 스프링 설정에서 읽어옴 -->
<springProperty name="DISCORD_WEBHOOK_URL" source="logging.discord.webhook-url"/>
<!-- 웹훅을 과도하게 사용하면 디스코드에서 제한을 거니까 ERROR 수준만 로깅 -->
<appender name="DISCORD_ERROR" class="com.whoz_in.logging.DiscordAppender">
<webhookUri>${DISCORD_WEBHOOK_URL}</webhookUri>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<encoder>
<pattern>${NO_COLOR_LOG_PATTERN}</pattern>
</encoder>
</appender>

</configuration>
4 changes: 3 additions & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ findProject(':modules:infrastructure:spring')?.name = 'spring'
include 'modules:infrastructure:api-query-jpa'
findProject(':modules:infrastructure:api-query-jpa')?.name = 'api-query-jpa'
include 'modules:infrastructure:log-writer'
findProject(':modules:infrastructure:log-writer')?.name = 'log-writer'
findProject(':modules:infrastructure:log-writer')?.name = 'log-writer'
include 'modules:infrastructure:logging'
findProject(':modules:infrastructure:logging')?.name = 'logging'
Loading