From 423800db39b869ed224f7780b3dc5e0acf33e77f Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Thu, 7 Aug 2025 18:52:02 +0900 Subject: [PATCH 01/50] =?UTF-8?q?[chore]=20gitignore=EC=97=90=20logs=20?= =?UTF-8?q?=ED=8F=B4=EB=8D=94=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index a48ed1cf..7f1e2213 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,8 @@ out/ ### application.yml ### application.yml + +### Logs ### +logs/ +*.log +*.log.gz From 020860311d119cc39599e4b3a8ef1b9da681bff9 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Thu, 7 Aug 2025 18:57:25 +0900 Subject: [PATCH 02/50] =?UTF-8?q?[feat]=20=EC=98=88=EC=99=B8,=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EB=B0=9C=EC=83=9D=20=EC=8B=9C=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EB=A1=9C=20=EA=B8=B0=EB=A1=9D=ED=95=98=EB=8A=94=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/admin/service/LogService.java | 108 ++++++++++++++++++ .../exception/GlobalExceptionHandler.java | 28 +++++ 2 files changed, 136 insertions(+) create mode 100644 src/main/java/com/wayble/server/admin/service/LogService.java diff --git a/src/main/java/com/wayble/server/admin/service/LogService.java b/src/main/java/com/wayble/server/admin/service/LogService.java new file mode 100644 index 00000000..82a1cabf --- /dev/null +++ b/src/main/java/com/wayble/server/admin/service/LogService.java @@ -0,0 +1,108 @@ +package com.wayble.server.admin.service; + +import com.wayble.server.admin.dto.log.ErrorLogDto; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; + +@Slf4j +@Service +public class LogService { + + private static final String ERROR_LOG_PATH = "logs/wayble-error.log"; + private static final int MAX_LINES = 1000; // 최대 읽을 라인 수 + + /** + * 최근 에러 로그를 조회합니다 + */ + public List getRecentErrorLogs(int limit) { + try { + Path logPath = Paths.get(ERROR_LOG_PATH); + + if (!Files.exists(logPath)) { + log.warn("에러 로그 파일이 존재하지 않습니다: {}", ERROR_LOG_PATH); + return List.of(); + } + + List lines = Files.readAllLines(logPath); + List errorLogs = new ArrayList<>(); + + // 마지막 라인부터 역순으로 처리 (최신 로그가 아래쪽) + int startIndex = Math.max(0, lines.size() - MAX_LINES); + for (int i = lines.size() - 1; i >= startIndex && errorLogs.size() < limit; i--) { + String line = lines.get(i); + ErrorLogDto errorLog = ErrorLogDto.from(line); + if (errorLog != null) { + errorLogs.add(errorLog); + } + } + + return errorLogs.stream() + .sorted(Comparator.comparing(ErrorLogDto::timestamp).reversed()) + .limit(limit) + .toList(); + + } catch (IOException e) { + log.error("에러 로그 파일 읽기 실패", e); + return List.of(); + } + } + + /** + * 에러 로그 통계를 조회합니다 + */ + public ErrorLogStats getErrorLogStats() { + try { + Path logPath = Paths.get(ERROR_LOG_PATH); + + if (!Files.exists(logPath)) { + return new ErrorLogStats(0, 0, 0, LocalDateTime.now()); + } + + try (Stream lines = Files.lines(logPath)) { + List errorLogs = lines + .map(ErrorLogDto::from) + .filter(log -> log != null) + .toList(); + + long totalErrors = errorLogs.size(); + long todayErrors = errorLogs.stream() + .filter(log -> log.timestamp().toLocalDate().equals(LocalDateTime.now().toLocalDate())) + .count(); + long lastHourErrors = errorLogs.stream() + .filter(log -> log.timestamp().isAfter(LocalDateTime.now().minusHours(1))) + .count(); + + LocalDateTime lastErrorTime = errorLogs.stream() + .map(ErrorLogDto::timestamp) + .max(Comparator.naturalOrder()) + .orElse(null); + + return new ErrorLogStats(totalErrors, todayErrors, lastHourErrors, lastErrorTime); + } + + } catch (IOException e) { + log.error("에러 로그 통계 조회 실패", e); + return new ErrorLogStats(0, 0, 0, LocalDateTime.now()); + } + } + + /** + * 에러 로그 통계 정보 + */ + public record ErrorLogStats( + long totalErrors, + long todayErrors, + long lastHourErrors, + LocalDateTime lastErrorTime + ) {} +} \ No newline at end of file diff --git a/src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java b/src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java index 32e7ea6b..b5aeee65 100644 --- a/src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java @@ -38,6 +38,11 @@ public class GlobalExceptionHandler { @ExceptionHandler(ApplicationException.class) public ResponseEntity handleApplicationException(ApplicationException e, WebRequest request) { + // 에러 로그 기록 + String path = ((ServletWebRequest) request).getRequest().getRequestURI(); + log.error("ApplicationException 발생 - Path: {}, ErrorCode: {}, Message: {}", + path, e.getErrorCase(), e.getMessage(), e); + CommonResponse commonResponse = CommonResponse.error(e.getErrorCase()); HttpStatus status = HttpStatus.valueOf(e.getErrorCase().getHttpStatusCode()); @@ -53,6 +58,11 @@ public ResponseEntity handleValidException(BindingResult binding MethodArgumentNotValidException ex, WebRequest request) { String message = bindingResult.getAllErrors().get(0).getDefaultMessage(); + + // 에러 로그 기록 + String path = ((ServletWebRequest) request).getRequest().getRequestURI(); + log.error("Validation Exception 발생 - Path: {}, Message: {}", path, message, ex); + CommonResponse commonResponse = CommonResponse.error(400, message); sendToDiscord(ex, request, HttpStatus.BAD_REQUEST); @@ -61,6 +71,24 @@ public ResponseEntity handleValidException(BindingResult binding .body(commonResponse); } + /** + * 모든 예상하지 못한 예외 처리 + */ + @ExceptionHandler(Exception.class) + public ResponseEntity handleGeneralException(Exception ex, WebRequest request) { + String path = ((ServletWebRequest) request).getRequest().getRequestURI(); + log.error("Unexpected Exception 발생 - Path: {}, Exception: {}, Message: {}", + path, ex.getClass().getSimpleName(), ex.getMessage(), ex); + + CommonResponse commonResponse = CommonResponse.error(500, "서버 내부 오류가 발생했습니다."); + + sendToDiscord(ex, request, HttpStatus.INTERNAL_SERVER_ERROR); + + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(commonResponse); + } + private void sendToDiscord(Exception ex, WebRequest request, HttpStatus status) { String path = ((ServletWebRequest) request).getRequest().getRequestURI(); String timestamp = Instant.now().toString(); From 6de88d88b834107f85699d74d870e5ad07ffd619 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Thu, 7 Aug 2025 19:02:04 +0900 Subject: [PATCH 03/50] =?UTF-8?q?[feat]=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=97=90=EB=9F=AC=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/controller/AdminLogController.java | 55 +++++++++++++++++++ .../server/admin/dto/log/ErrorLogDto.java | 54 ++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 src/main/java/com/wayble/server/admin/controller/AdminLogController.java create mode 100644 src/main/java/com/wayble/server/admin/dto/log/ErrorLogDto.java diff --git a/src/main/java/com/wayble/server/admin/controller/AdminLogController.java b/src/main/java/com/wayble/server/admin/controller/AdminLogController.java new file mode 100644 index 00000000..df3f290f --- /dev/null +++ b/src/main/java/com/wayble/server/admin/controller/AdminLogController.java @@ -0,0 +1,55 @@ +package com.wayble.server.admin.controller; + +import com.wayble.server.admin.dto.log.ErrorLogDto; +import com.wayble.server.admin.service.LogService; +import com.wayble.server.admin.service.LogService.ErrorLogStats; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Controller +@RequestMapping("/admin/logs") +@RequiredArgsConstructor +public class AdminLogController { + + private final LogService logService; + + /** + * 에러 로그 관리 페이지 + */ + @GetMapping("/error") + public String errorLogPage( + @RequestParam(defaultValue = "100") int limit, + Model model + ) { + List errorLogs = logService.getRecentErrorLogs(limit); + ErrorLogStats stats = logService.getErrorLogStats(); + + model.addAttribute("errorLogs", errorLogs); + model.addAttribute("stats", stats); + model.addAttribute("limit", limit); + + return "admin/log/error-logs"; + } + + /** + * 에러 로그 Ajax 조회 + */ + @GetMapping("/error/data") + @ResponseBody + public List getErrorLogs(@RequestParam(defaultValue = "100") int limit) { + return logService.getRecentErrorLogs(limit); + } + + /** + * 에러 로그 통계 Ajax 조회 + */ + @GetMapping("/error/stats") + @ResponseBody + public ErrorLogStats getErrorLogStats() { + return logService.getErrorLogStats(); + } +} \ No newline at end of file diff --git a/src/main/java/com/wayble/server/admin/dto/log/ErrorLogDto.java b/src/main/java/com/wayble/server/admin/dto/log/ErrorLogDto.java new file mode 100644 index 00000000..db68723a --- /dev/null +++ b/src/main/java/com/wayble/server/admin/dto/log/ErrorLogDto.java @@ -0,0 +1,54 @@ +package com.wayble.server.admin.dto.log; + +import java.time.LocalDateTime; + +public record ErrorLogDto( + LocalDateTime timestamp, + String level, + String logger, + String message, + String exception +) { + public static ErrorLogDto from(String logLine) { + try { + // 로그 파싱: [2025-08-07 15:30:45] [main] ERROR com.wayble.server.SomeClass - Error message + if (!logLine.contains("ERROR")) { + return null; + } + + // 타임스탬프 추출 + int timestampEnd = logLine.indexOf("]"); + if (timestampEnd == -1) return null; + + String timestampStr = logLine.substring(1, timestampEnd); + LocalDateTime timestamp = LocalDateTime.parse(timestampStr.replace(" ", "T")); + + // 레벨 추출 + int levelStart = logLine.indexOf("ERROR"); + int levelEnd = levelStart + 5; + String level = "ERROR"; + + // 로거명 추출 + int loggerStart = levelEnd + 1; + int loggerEnd = logLine.indexOf(" - "); + if (loggerEnd == -1) return null; + + String logger = logLine.substring(loggerStart, loggerEnd).trim(); + + // 메시지 추출 + String message = logLine.substring(loggerEnd + 3); + + // 예외 정보는 별도 처리 (여러 줄일 수 있음) + String exception = ""; + if (message.contains("Exception") || message.contains("Error")) { + exception = message; + } + + return new ErrorLogDto(timestamp, level, logger, message, exception); + + } catch (Exception e) { + // 파싱 실패시 null 반환 + return null; + } + } +} \ No newline at end of file From ea676bdb752e64e7a9fe45c26ebfb41143e1c42b Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Thu, 7 Aug 2025 19:02:12 +0900 Subject: [PATCH 04/50] =?UTF-8?q?[feat]=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=97=90=EB=9F=AC=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EA=B4=80=EB=A6=AC=20view=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/logback-spring.xml | 56 ++++ .../resources/templates/admin/dashboard.html | 23 ++ .../templates/admin/log/error-logs.html | 256 ++++++++++++++++++ 3 files changed, 335 insertions(+) create mode 100644 src/main/resources/logback-spring.xml create mode 100644 src/main/resources/templates/admin/log/error-logs.html diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 00000000..9a7caec3 --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,56 @@ + + + + + + [%d{yyyy-MM-dd HH:mm:ss}] [%thread] %-5level %logger{36} - %msg%n + + + + + + logs/wayble-app.log + + logs/wayble-app.%d{yyyy-MM-dd}.%i.log.gz + 100MB + 30 + 3GB + + + [%d{yyyy-MM-dd HH:mm:ss}] [%thread] %-5level %logger{36} - %msg%n + + + + + + logs/wayble-error.log + + ERROR + ACCEPT + DENY + + + logs/wayble-error.%d{yyyy-MM-dd}.%i.log.gz + 50MB + 90 + 1GB + + + [%d{yyyy-MM-dd HH:mm:ss}] [%thread] %-5level %logger{36} - %msg%n%ex + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/admin/dashboard.html b/src/main/resources/templates/admin/dashboard.html index 57302794..56d26118 100644 --- a/src/main/resources/templates/admin/dashboard.html +++ b/src/main/resources/templates/admin/dashboard.html @@ -223,6 +223,29 @@

+
+
+
+
+ + + +
+

에러 로그 관리

+

시스템 에러 로그 조회 및 모니터링

+
+
+ 관리하기 + + + +
+
+
+ +
diff --git a/src/main/resources/templates/admin/log/error-logs.html b/src/main/resources/templates/admin/log/error-logs.html new file mode 100644 index 00000000..fe4a4abc --- /dev/null +++ b/src/main/resources/templates/admin/log/error-logs.html @@ -0,0 +1,256 @@ + + + + + + 에러 로그 관리 - Wayble 관리자 + + + + + + + + +
+
+

에러 로그 관리

+

시스템 에러 로그 조회 및 모니터링

+
+ + +
+
+

전체 에러

+
0
+
+
+

오늘 에러

+
0
+
+
+

최근 1시간

+
0
+
+
+

마지막 에러

+
-
+
-
+
+
+ + +
+
+ + +
+ +
+ + +
+
+
+ 에러 로그가 없습니다. +
+
+
+ + +
+
+
+
+
+
+
+ + + + + \ No newline at end of file From 08258532864691d613bc8d3e11a5a00d75e8b8fe Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Thu, 7 Aug 2025 19:18:08 +0900 Subject: [PATCH 05/50] =?UTF-8?q?[feat]=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EB=A1=9C=EA=B7=B8=20=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 2 + .../server/admin/dto/log/ErrorLogDto.java | 27 +++++++++-- .../exception/GlobalExceptionHandler.java | 9 ++-- .../templates/admin/log/error-logs.html | 46 ++++++++++--------- 4 files changed, 54 insertions(+), 30 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 1874a331..cee49e4d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,8 @@ services: - SPRING_PROFILES_ACTIVE=develop - ELASTICSEARCH_HOST=elasticsearch - ELASTICSEARCH_PORT=9200 + volumes: + - ./logs:/app/logs # 로그 파일 영속성을 위한 볼륨 마운트 depends_on: - elasticsearch networks: diff --git a/src/main/java/com/wayble/server/admin/dto/log/ErrorLogDto.java b/src/main/java/com/wayble/server/admin/dto/log/ErrorLogDto.java index db68723a..5e49cfe8 100644 --- a/src/main/java/com/wayble/server/admin/dto/log/ErrorLogDto.java +++ b/src/main/java/com/wayble/server/admin/dto/log/ErrorLogDto.java @@ -7,7 +7,10 @@ public record ErrorLogDto( String level, String logger, String message, - String exception + String exception, + String method, + String path, + String stackTrace ) { public static ErrorLogDto from(String logLine) { try { @@ -36,15 +39,29 @@ public static ErrorLogDto from(String logLine) { String logger = logLine.substring(loggerStart, loggerEnd).trim(); // 메시지 추출 - String message = logLine.substring(loggerEnd + 3); + String fullMessage = logLine.substring(loggerEnd + 3); + + // HTTP Method와 Path 추출 + String method = ""; + String path = ""; + if (fullMessage.contains("Method:") && fullMessage.contains("Path:")) { + String[] parts = fullMessage.split(", "); + for (String part : parts) { + if (part.contains("Method:")) { + method = part.substring(part.indexOf("Method:") + 8).trim(); + } else if (part.contains("Path:")) { + path = part.substring(part.indexOf("Path:") + 6).trim(); + } + } + } // 예외 정보는 별도 처리 (여러 줄일 수 있음) String exception = ""; - if (message.contains("Exception") || message.contains("Error")) { - exception = message; + if (fullMessage.contains("Exception") || fullMessage.contains("Error")) { + exception = fullMessage; } - return new ErrorLogDto(timestamp, level, logger, message, exception); + return new ErrorLogDto(timestamp, level, logger, fullMessage, exception, method, path, ""); } catch (Exception e) { // 파싱 실패시 null 반환 diff --git a/src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java b/src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java index b5aeee65..16893de5 100644 --- a/src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java @@ -38,10 +38,13 @@ public class GlobalExceptionHandler { @ExceptionHandler(ApplicationException.class) public ResponseEntity handleApplicationException(ApplicationException e, WebRequest request) { - // 에러 로그 기록 + // 에러 로그 기록 (상세 정보 포함) String path = ((ServletWebRequest) request).getRequest().getRequestURI(); - log.error("ApplicationException 발생 - Path: {}, ErrorCode: {}, Message: {}", - path, e.getErrorCase(), e.getMessage(), e); + String method = ((ServletWebRequest) request).getRequest().getMethod(); + String userAgent = ((ServletWebRequest) request).getRequest().getHeader("User-Agent"); + + log.error("ApplicationException 발생 - Method: {}, Path: {}, ErrorCode: {}, Message: {}, UserAgent: {}", + method, path, e.getErrorCase(), e.getMessage(), userAgent, e); CommonResponse commonResponse = CommonResponse.error(e.getErrorCase()); diff --git a/src/main/resources/templates/admin/log/error-logs.html b/src/main/resources/templates/admin/log/error-logs.html index fe4a4abc..a7cf660c 100644 --- a/src/main/resources/templates/admin/log/error-logs.html +++ b/src/main/resources/templates/admin/log/error-logs.html @@ -104,9 +104,9 @@

Wayble 관리자 페이지

@@ -155,17 +155,15 @@

마지막 에러

-
-
- - -
- +
+ + + 5분마다 자동 새로고침됩니다
@@ -179,6 +177,13 @@

마지막 에러

+ + +
+ GET + /api/v1/example +
+
@@ -189,10 +194,6 @@

마지막 에러