diff --git a/src/main/java/LuckyVicky/backend/global/lifecycle/InstanceShutdownHandler.java b/src/main/java/LuckyVicky/backend/global/lifecycle/InstanceShutdownHandler.java index 982bf34..5c23d7b 100644 --- a/src/main/java/LuckyVicky/backend/global/lifecycle/InstanceShutdownHandler.java +++ b/src/main/java/LuckyVicky/backend/global/lifecycle/InstanceShutdownHandler.java @@ -17,5 +17,6 @@ public class InstanceShutdownHandler { public void onShutdown() { log.info("Application is shutting down. Uploading today's log to S3..."); logService.uploadDailyLog("today"); + log.info("Uploaded Log to S3 with PreDestroy"); } } \ No newline at end of file diff --git a/src/main/java/LuckyVicky/backend/global/s3/LogFileManager.java b/src/main/java/LuckyVicky/backend/global/s3/LogFileManager.java deleted file mode 100644 index 2d34504..0000000 --- a/src/main/java/LuckyVicky/backend/global/s3/LogFileManager.java +++ /dev/null @@ -1,69 +0,0 @@ -package LuckyVicky.backend.global.s3; - -import static LuckyVicky.backend.global.util.Constant.LOG_LOCAL_ERROR_FILE_NAME; -import static LuckyVicky.backend.global.util.Constant.LOG_LOCAL_FILE_DIRECTORY; -import static LuckyVicky.backend.global.util.Constant.LOG_LOGBACK_ERROR_FILE_NAME; -import static LuckyVicky.backend.global.util.Constant.LOG_LOGBACK_FILE_DIRECTORY; - -import ch.qos.logback.classic.LoggerContext; -import ch.qos.logback.classic.joran.JoranConfigurator; -import ch.qos.logback.core.joran.spi.JoranException; -import java.io.File; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; - -@Slf4j -@Component -@RequiredArgsConstructor -public class LogFileManager { - - public File getLogFile() { - return new File(LOG_LOCAL_FILE_DIRECTORY + LOG_LOCAL_ERROR_FILE_NAME); - } - - public void recreateLogFile(boolean releaseLogContext) { - File logFile = getLogFile(); - - if (releaseLogContext) { - releaseLogFile(); // API나 스케줄러 호출 시 잠금 해제 - } - - deleteLogFile(logFile); - - resetLogbackContext(); - - log.error("This is a dummy log to recreate the log file."); - } - - public LoggerContext releaseLogFile() { - LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); - loggerContext.stop(); // Logback 파일 핸들러 잠금 해제 - return loggerContext; - } - - public void resetLogbackContext() { - LoggerContext loggerContext = releaseLogFile(); - - try { - loggerContext.reset(); - JoranConfigurator configurator = new JoranConfigurator(); - configurator.setContext(loggerContext); - configurator.doConfigure(LOG_LOGBACK_FILE_DIRECTORY + LOG_LOGBACK_ERROR_FILE_NAME); - loggerContext.start(); - log.error("Logback context reset. Log file recreated."); - } catch (JoranException e) { - log.error("Failed to reset Logback context", e); - } - } - - public void deleteLogFile(File file) { - if (!file.delete()) { - file.deleteOnExit(); // 삭제 실패 시 JVM 종료 시 삭제 예약 - log.warn("Marked log file for deletion on JVM exit: {}", file.getAbsolutePath()); - } else { - log.warn("Already deleted OR Log file does not exist"); - } - } -} \ No newline at end of file diff --git a/src/main/java/LuckyVicky/backend/global/s3/S3LogService.java b/src/main/java/LuckyVicky/backend/global/s3/S3LogService.java index 8a84ef0..3cdebd9 100644 --- a/src/main/java/LuckyVicky/backend/global/s3/S3LogService.java +++ b/src/main/java/LuckyVicky/backend/global/s3/S3LogService.java @@ -1,13 +1,27 @@ package LuckyVicky.backend.global.s3; +import static LuckyVicky.backend.global.util.Constant.LOG_DATE_FORMAT; +import static LuckyVicky.backend.global.util.Constant.LOG_LOGBACK_ERROR_FILE_NAME; +import static LuckyVicky.backend.global.util.Constant.LOG_LOGBACK_FILE_DIRECTORY; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.joran.JoranConfigurator; +import ch.qos.logback.core.joran.spi.JoranException; import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.GetObjectRequest; import com.amazonaws.services.s3.model.PutObjectRequest; +import com.amazonaws.services.s3.model.S3Object; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.nio.file.Files; import java.time.LocalDate; -import java.time.format.DateTimeFormatter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.env.Environment; import org.springframework.stereotype.Service; @@ -24,50 +38,96 @@ public class S3LogService { private String bucket; public void uploadDailyLog(String day) { - File targetFile = getLocalLogFile(day); - - if (!targetFile.exists()) { + File localFile = getLocalLogFile(day); + if (!localFile.exists()) { log.warn("No local log file found for day={}", day); return; } String s3Key = buildS3Key(day); + File tempFile = new File("temp-" + localFile.getName()); try { - // S3에 업로드 - amazonS3.putObject(new PutObjectRequest(bucket, s3Key, targetFile)); - log.info("Uploaded log to S3: {} -> s3://{}/{}", targetFile.getName(), bucket, s3Key); + // S3에 기존 로그 파일이 있는 경우 다운로드하여 병합 + if (amazonS3.doesObjectExist(bucket, s3Key)) { + log.info("Existing log found in S3. Merging logs..."); + mergeLogsFromS3(localFile, tempFile, s3Key); + } else { + Files.copy(localFile.toPath(), tempFile.toPath()); + } - // 업로드 후 로컬 파일 삭제 - Files.deleteIfExists(targetFile.toPath()); + // 병합된 로그 파일을 S3에 업로드 + amazonS3.putObject(new PutObjectRequest(bucket, s3Key, tempFile)); + log.info("Uploaded merged log to S3: {} -> s3://{}/{}", tempFile.getName(), bucket, s3Key); + + // 업로드 후 로컬 로그 파일 삭제 및 Logback 초기화 + Files.deleteIfExists(localFile.toPath()); + log.info("Deleted local log file: {}", localFile.getName()); + resetLogbackContext(); } catch (Exception e) { - log.error("Failed to upload log file to S3. day={}, file={}, key={}", day, targetFile, s3Key, e); + log.error("Failed to upload or merge log file to S3. day={}, file={}, key={}", day, localFile, s3Key, e); + } finally { + tempFile.delete(); // 임시 파일 삭제 + } + } + + private void resetLogbackContext() { + LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); + loggerContext.stop(); // 기존 컨텍스트 정지 + + try { + loggerContext.reset(); // 컨텍스트 초기화 + JoranConfigurator configurator = new JoranConfigurator(); + configurator.setContext(loggerContext); + configurator.doConfigure(LOG_LOGBACK_FILE_DIRECTORY + LOG_LOGBACK_ERROR_FILE_NAME); + loggerContext.start(); // 컨텍스트 다시 시작 + log.info("Logback context has been reset. New log file will be created."); + } catch (JoranException e) { + log.error("Failed to reset Logback context", e); + } + } + + private void mergeLogsFromS3(File localFile, File tempFile, String s3Key) throws IOException { + try (S3Object s3Object = amazonS3.getObject(new GetObjectRequest(bucket, s3Key)); + InputStream s3InputStream = s3Object.getObjectContent(); + OutputStream tempOutputStream = new FileOutputStream(tempFile, true); + InputStream localInputStream = new FileInputStream(localFile)) { + + // S3 로그 내용을 임시 파일에 복사 + copyContent(s3InputStream, tempOutputStream); + + // 로컬 로그 내용을 임시 파일에 이어서 복사 + copyContent(localInputStream, tempOutputStream); + } + } + + private void copyContent(InputStream input, OutputStream output) throws IOException { + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = input.read(buffer)) > 0) { + output.write(buffer, 0, bytesRead); } } private File getLocalLogFile(String day) { - // LOG_HOME=/var/app/current/logs String logHome = System.getProperty("LOG_HOME", "./logs"); LocalDate targetDate = "yesterday".equals(day) ? LocalDate.now().minusDays(1) : LocalDate.now(); - String dateStr = targetDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + String dateStr = targetDate.format(LOG_DATE_FORMAT); if ("yesterday".equals(day)) { - // 롤오버된 파일: /var/app/current/logs/error-2025-01-01.log return new File(logHome, "error-" + dateStr + ".log"); } else { - // 아직 열려 있는 로그: /var/app/current/logs/error.log return new File(logHome, "error.log"); } } private String buildS3Key(String day) { - // 원하는 규칙대로 S3에 업로드될 파일 키 생성 LocalDate targetDate = "yesterday".equals(day) ? LocalDate.now().minusDays(1) : LocalDate.now(); - String dateStr = targetDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + String dateStr = targetDate.format(LOG_DATE_FORMAT); String activeProfile = environment.getProperty("spring.profiles.active", "default"); - return String.format("logs/" + activeProfile + "/" + activeProfile + "-error-%s.log", dateStr); + return String.format("logs/%s/%s-error-%s.log", activeProfile, activeProfile, dateStr); } } diff --git a/src/main/java/LuckyVicky/backend/global/scheduler/LogUploadScheduler.java b/src/main/java/LuckyVicky/backend/global/scheduler/LogUploadScheduler.java index 93c05ec..c6b0f88 100644 --- a/src/main/java/LuckyVicky/backend/global/scheduler/LogUploadScheduler.java +++ b/src/main/java/LuckyVicky/backend/global/scheduler/LogUploadScheduler.java @@ -15,8 +15,8 @@ public class LogUploadScheduler { @Scheduled(cron = "0 0 0 * * ?", zone = "Asia/Seoul") public void uploadDailyLogToS3() { - log.error("Uploading Log to S3 with Scheduler"); + log.info("Uploading Log to S3 with Scheduler"); logService.uploadDailyLog("yesterday"); - log.error("Uploaded Log to S3 with Scheduler"); + log.info("Uploaded Log to S3 with Scheduler"); } } \ No newline at end of file diff --git a/src/main/java/LuckyVicky/backend/global/util/Constant.java b/src/main/java/LuckyVicky/backend/global/util/Constant.java index 83e0114..c0e085a 100644 --- a/src/main/java/LuckyVicky/backend/global/util/Constant.java +++ b/src/main/java/LuckyVicky/backend/global/util/Constant.java @@ -11,9 +11,6 @@ public class Constant { private String fcmProjectId; // Log - public static final String LOG_S3_DIRECTORY = "logs/"; - public static final String LOG_LOCAL_FILE_DIRECTORY = "src/main/resources/logs/"; - public static final String LOG_LOCAL_ERROR_FILE_NAME = "error.log"; public static final String LOG_LOGBACK_FILE_DIRECTORY = "src/main/resources/"; public static final String LOG_LOGBACK_ERROR_FILE_NAME = "logback-spring.xml";