Skip to content

Commit

Permalink
Merge branch 'dev' of github.com:innovationacademy-kr/42cabi into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
Z1Park committed Apr 21, 2024
2 parents 185da2d + 22e6ba3 commit 41a06d3
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package org.ftclub.cabinet.alarm.discord;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import lombok.Builder;
import lombok.Getter;
import org.springframework.web.util.ContentCachingRequestWrapper;

@Builder
@Getter
public class DiscordWebAlarmMessage {

private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(
"yyyy-MM-dd HH:mm:ss");
private final String subject;
private final String requestURI;
private final String httpMethod;
private final String headers;
private final String body;
private final String parameters;
private final String responseBody;

public static DiscordWebAlarmMessage fromHttpServletRequest(HttpServletRequest request,
String subject,
String responseBody) {
String requestBody = "";
if (request instanceof ContentCachingRequestWrapper) {
byte[] buf = ((ContentCachingRequestWrapper) request).getContentAsByteArray();
requestBody = new String(buf, 0, buf.length, StandardCharsets.UTF_8).trim();
}

String headers = Collections.list(request.getHeaderNames())
.stream()
.collect(Collectors.toMap(h -> h, request::getHeader))
.entrySet()
.stream()
.map(entry -> "\"" + entry.getKey() + "\":\"" + entry.getValue() + "\"")
.collect(Collectors.joining(", ", "{", "}"));

String params = request.getParameterMap().entrySet()
.stream()
.map(entry -> "\"" + entry.getKey() + "\":\"" + Arrays.toString(entry.getValue())
+ "\"")
.collect(Collectors.joining(", ", "{", "}"));

return DiscordWebAlarmMessage.builder()
.subject(subject)
.requestURI(request.getRequestURI())
.httpMethod(request.getMethod())
.headers(headers)
.body(requestBody)
.parameters(params)
.responseBody(responseBody)
.build();
}

public static DiscordWebAlarmMessage fromWebRequest(WebRequest request, String subject,
String responseBody) {
if (request instanceof ServletWebRequest) {
HttpServletRequest servletRequest = ((ServletWebRequest) request).getRequest();
return fromHttpServletRequest(servletRequest, subject, responseBody);
} else {
String params = request.getParameterMap().entrySet().stream()
.map(entry -> entry.getKey() + "=" + String.join(", ", entry.getValue()))
.reduce((p1, p2) -> p1 + "; " + p2)
.orElse("No parameters");

String method = request.getHeader("X-HTTP-Method-Override"); // 클라이언트가 메서드를 오버라이드 했을 경우
if (method == null) {
method = request.getHeader("Method");
}

return DiscordWebAlarmMessage.builder()
.subject(subject)
.requestURI(request.getContextPath())
.httpMethod(method)
.parameters(params)
.responseBody(responseBody)
.build();
}
}

@Override
public String toString() {
return "```java\n" +
"Subject: \"" + subject + "\"\n" +
"Issued at: \"" + LocalDateTime.now().format(formatter) + "\"\n" +
"Request URI: \"" + requestURI + "\"\n" +
"HTTP Method: \"" + httpMethod + "\"\n" +
"Request Headers: \"" + headers + "\"\n" +
"Request Body: \"" + body + "\"\n" +
"Request Parameters: \"" + parameters + "\"\n" +
"Response Body: \"" + responseBody + "\"\n" +
"```";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

@Component
public class DiscordWebHookMessenger {

private final static String DISCORD_WEBHOOK_MESSAGE_KEY = "content";
private final String discordWebHookUrl;

Expand All @@ -27,4 +28,16 @@ public void sendMessage(DiscordAlarmMessage message) {
.bodyToMono(String.class)
.block();
}

public void sendMessage(DiscordWebAlarmMessage message) {
Map<String, String> body = new HashMap<>();
body.put(DISCORD_WEBHOOK_MESSAGE_KEY, message.toString());

WebClient.create().post()
.uri(discordWebHookUrl)
.bodyValue(body)
.retrieve()
.bodyToMono(String.class)
.block();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.ftclub.cabinet.auth.filter;

import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.util.ContentCachingRequestWrapper;

@Component
public class CachingRequestBodyFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
HttpServletRequest wrappedRequest = new ContentCachingRequestWrapper(request);
filterChain.doFilter(wrappedRequest, response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
@Log4j2
@RequiredArgsConstructor
public class AdminOauthService {

@Qualifier(OauthConfig.GOOGLE_OAUTH_20_SERVICE)
private final OAuth20Service googleOAuth20Service;
private final ObjectMapper objectMapper;
Expand All @@ -46,21 +47,24 @@ public void requestLogin(HttpServletResponse res) throws IOException {
* @throws ExecutionException 비동기 처리시 스레드에서 발생한 오류 처리 예외
* @throws InterruptedException 비동기 처리시 스레드 종료를 위한 예외
*/
public GoogleProfile getProfileByCode(String code) throws IOException, ExecutionException, InterruptedException {
public GoogleProfile getProfileByCode(String code)
throws IOException, ExecutionException, InterruptedException {
OAuth2AccessToken accessToken = googleOAuth20Service.getAccessToken(code);
OAuthRequest oAuthRequest = new OAuthRequest(Verb.GET, "https://www.googleapis.com/oauth2/v2/userinfo");
OAuthRequest oAuthRequest = new OAuthRequest(Verb.GET,
"https://www.googleapis.com/oauth2/v2/userinfo");
googleOAuth20Service.signRequest(accessToken, oAuthRequest);
try {
Response response = googleOAuth20Service.execute(oAuthRequest);
return convertJsonStringToProfile(response.getBody());
} catch (Exception e) {
if (e instanceof IOException)
if (e instanceof IOException) {
log.error("42 API 서버에서 프로필 정보를 가져오는데 실패했습니다."
+ "code: {}, message: {}", code, e.getMessage());
if (e instanceof ExecutionException || e instanceof InterruptedException)
}
if (e instanceof ExecutionException || e instanceof InterruptedException) {
log.error("42 API 서버에서 프로필 정보를 비동기적으로 가져오는데 실패했습니다."
+ "code: {}, message: {}", code, e.getMessage());
e.printStackTrace();
}
throw ExceptionStatus.INTERNAL_SERVER_ERROR.asServiceException();
}
}
Expand All @@ -71,7 +75,8 @@ public GoogleProfile getProfileByCode(String code) throws IOException, Execution
* @param jsonString
* @return
* @throws IOException
* @see <a href="https://developers.google.com/identity/protocols/oauth2/openid-connect#obtainuserinfo">참고</a>
* @see <a
* href="https://developers.google.com/identity/protocols/oauth2/openid-connect#obtainuserinfo">참고</a>
*/
private GoogleProfile convertJsonStringToProfile(String jsonString) throws IOException {
JsonNode jsonNode = objectMapper.readTree(jsonString);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
package org.ftclub.cabinet.exception;

import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.ftclub.cabinet.alarm.discord.DiscordWebAlarmMessage;
import org.ftclub.cabinet.alarm.discord.DiscordWebHookMessenger;
import org.jetbrains.annotations.NotNull;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@Log4j2
@RestControllerAdvice
public class ExceptionController {
@RequiredArgsConstructor
public class ExceptionController extends ResponseEntityExceptionHandler {

private final DiscordWebHookMessenger discordWebHookMessenger;
private static final String DEFAULT_ERROR_MESSAGE_VALUE = "까비 서버에서 예기치 않은 오류가 발생했어요.🥲";
private static final String SPRING_MVC_ERROR_MESSAGE_VALUE = "Spring MVC 에서 예기치 않은 오류가 발생했어요.🥲";

@ExceptionHandler(ControllerException.class)
public ResponseEntity<?> controllerExceptionHandler(ControllerException e) {
log.info("[ControllerException] {} : {}", e.status.getError(), e.status.getMessage());
e.printStackTrace();
if (log.isDebugEnabled()) {
log.debug("Exception stack trace: ", e);
}
return ResponseEntity
.status(e.status.getStatusCode())
.body(e.status);
Expand All @@ -21,7 +37,9 @@ public ResponseEntity<?> controllerExceptionHandler(ControllerException e) {
@ExceptionHandler(ServiceException.class)
public ResponseEntity<?> serviceExceptionHandler(ServiceException e) {
log.info("[ServiceException] {} : {}", e.status.getError(), e.status.getMessage());
e.printStackTrace();
if (log.isDebugEnabled()) {
log.debug("Exception stack trace: ", e);
}
return ResponseEntity
.status(e.status.getStatusCode())
.body(e.status);
Expand All @@ -30,6 +48,9 @@ public ResponseEntity<?> serviceExceptionHandler(ServiceException e) {
@ExceptionHandler(CustomServiceException.class)
public ResponseEntity<?> customServiceExceptionHandler(CustomServiceException e) {
log.info("[CustomServiceException] {} : {}", e.status.getError(), e.status.getMessage());
if (log.isDebugEnabled()) {
log.debug("Exception stack trace: ", e);
}
return ResponseEntity
.status(e.status.getStatusCode())
.body(e.status);
Expand All @@ -38,6 +59,9 @@ public ResponseEntity<?> customServiceExceptionHandler(CustomServiceException e)
@ExceptionHandler(DomainException.class)
public ResponseEntity<?> domainExceptionHandler(DomainException e) {
log.warn("[DomainException] {} : {}", e.status.getError(), e.status.getMessage());
if (log.isDebugEnabled()) {
log.debug("Exception stack trace: ", e);
}
return ResponseEntity
.status(e.status.getStatusCode())
.body(e.status);
Expand All @@ -46,9 +70,65 @@ public ResponseEntity<?> domainExceptionHandler(DomainException e) {
@ExceptionHandler(UtilException.class)
public ResponseEntity<?> utilExceptionHandler(UtilException e) {
log.warn("[UtilException] {} : {}", e.status.getError(), e.status.getMessage());
if (log.isDebugEnabled()) {
log.debug("Exception stack trace: ", e);
}
return ResponseEntity
.status(e.status.getStatusCode())
.body(e.status);
}

@NotNull
@Override
protected ResponseEntity<Object> handleExceptionInternal(
@NotNull Exception e, Object body,
@NotNull org.springframework.http.HttpHeaders headers,
@NotNull org.springframework.http.HttpStatus status,
@NotNull org.springframework.web.context.request.WebRequest request) {
Map<String, Object> responseBody = new LinkedHashMap<>();
String requestUri = request.getContextPath(); // requestURI 정보 설정
responseBody.put("statusCode", status.value());
responseBody.put("message", e.getMessage());
responseBody.put("error", status.getReasonPhrase());

if (status.is5xxServerError()) {
responseBody.put("message", DEFAULT_ERROR_MESSAGE_VALUE);
log.error("[SpringMVCException] {} : {} at {}",
status.getReasonPhrase(), e.getMessage(), requestUri);
log.error("Exception stack trace: ", e);
discordWebHookMessenger.sendMessage(
DiscordWebAlarmMessage.fromWebRequest(
request,
SPRING_MVC_ERROR_MESSAGE_VALUE,
responseBody.toString()
)
);
} else {
log.warn("[SpringMVCException] {} : {} at {}",
status.getReasonPhrase(), e.getMessage(), requestUri);
}
return ResponseEntity.status(status).headers(headers).body(responseBody);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<Object> handleInternalServerErrorException(Exception e,
HttpServletRequest request) {
log.error("[UncheckedException] {} for request URL: {}", e.getMessage(),
request.getRequestURL());
log.error("Exception stack trace: ", e);

Map<String, Object> responseBody = new LinkedHashMap<>();
responseBody.put("statusCode", HttpStatus.INTERNAL_SERVER_ERROR.value());
responseBody.put("message", DEFAULT_ERROR_MESSAGE_VALUE);
responseBody.put("error", HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase());

discordWebHookMessenger.sendMessage(
DiscordWebAlarmMessage.fromHttpServletRequest(
request,
DEFAULT_ERROR_MESSAGE_VALUE,
responseBody.toString()
)
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(responseBody);
}
}
11 changes: 0 additions & 11 deletions backend/src/main/resources/log4j2-prod.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,6 @@
</Delete>
</DefaultRolloverStrategy>
</RollingFile>

<Http name="discordWebhook" url="${spring:webhook.discord-logging}" method="POST">
<Property name="Content-Type">application/json</Property>
<PatternLayout>
<Pattern>
{"content": "Profile: Main | %encode{%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level: %m%n}{JSON}"}
</Pattern>
</PatternLayout>
<ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/>
</Http>
</Appenders>

<Loggers>
Expand All @@ -38,7 +28,6 @@
<AppenderRef ref="rollingFile"/>
<AppenderRef ref="discordWebhook"/>
</Logger>

<Logger name="org.ftclub.cabinet" level="info" additivity="false">
<AppenderRef ref="console"/>
<AppenderRef ref="rollingFile"/>
Expand Down

0 comments on commit 41a06d3

Please sign in to comment.