From 2eebf2c83ed1f3dd3998be4e4d947be97eab0a2d Mon Sep 17 00:00:00 2001 From: HYOJIN Date: Mon, 13 Apr 2026 09:27:46 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20health=5Flog=20=EC=9D=BC=EB=B3=84=20?= =?UTF-8?q?=ED=8C=8C=ED=8B=B0=EC=85=94=EB=8B=9D=20+=20=EC=9D=B8=EB=8D=B1?= =?UTF-8?q?=EC=8A=A4=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PartitionService 범용화 (테이블명 파라미터) + 일별 파티션 메서드 추가 - PartitionManageScheduler에 health_log 일별 파티션 관리 추가 (7일 선행 생성, 90일 삭제) - DataCleanupScheduler health_log DELETE 제거 (파티션 DROP으로 대체) - SnpServiceHealthLog FK 제약 제거 (파티션 테이블 호환) - 복합 인덱스 추가 (service_id+checked_at, daily_uptime 최적화) - 마이그레이션 SQL 스크립트 추가 --- docs/schema/create_tables.sql | 2 + .../schema/health_log_partition_migration.sql | 72 +++++++++++++++++++ .../entity/SnpServiceHealthLog.java | 5 +- .../scheduler/DataCleanupScheduler.java | 21 +----- .../scheduler/PartitionManageScheduler.java | 44 ++++++++++++ .../monitoring/service/PartitionService.java | 46 +++++++++++- 6 files changed, 169 insertions(+), 21 deletions(-) create mode 100644 docs/schema/health_log_partition_migration.sql diff --git a/docs/schema/create_tables.sql b/docs/schema/create_tables.sql index ae5d74a..55344dc 100644 --- a/docs/schema/create_tables.sql +++ b/docs/schema/create_tables.sql @@ -81,6 +81,8 @@ CREATE TABLE IF NOT EXISTS snp_service_health_log ( CREATE INDEX idx_snp_health_log_service ON snp_service_health_log (service_id); CREATE INDEX idx_snp_health_log_checked ON snp_service_health_log (checked_at); +CREATE INDEX idx_health_log_svc_checked ON snp_service_health_log (service_id, checked_at DESC); +CREATE INDEX idx_health_log_daily_uptime ON snp_service_health_log (service_id, checked_at, current_status); -- ----------------------------------------------------------- -- 5. snp_service_api (서비스 API) diff --git a/docs/schema/health_log_partition_migration.sql b/docs/schema/health_log_partition_migration.sql new file mode 100644 index 0000000..884f4c4 --- /dev/null +++ b/docs/schema/health_log_partition_migration.sql @@ -0,0 +1,72 @@ +-- ============================================================= +-- SNP Connection Monitoring - Health Log 파티션 마이그레이션 +-- snp_service_health_log → 일별 Range 파티션 전환 +-- 스키마: common +-- ============================================================= + +-- 1. 백업 +CREATE TABLE common.snp_service_health_log_backup AS +SELECT * FROM common.snp_service_health_log; + +-- 2. 기존 테이블 삭제 +DROP TABLE common.snp_service_health_log; + +-- 3. 파티션 테이블 생성 +CREATE TABLE common.snp_service_health_log ( + log_id BIGSERIAL, + service_id BIGINT NOT NULL, + previous_status VARCHAR(10), + current_status VARCHAR(10) NOT NULL, + response_time INTEGER, + error_message TEXT, + checked_at TIMESTAMP NOT NULL DEFAULT NOW(), + PRIMARY KEY (log_id, checked_at) +) PARTITION BY RANGE (checked_at); + +-- 4. 인덱스 (파티션에 자동 상속) +CREATE INDEX idx_snp_health_log_service ON common.snp_service_health_log (service_id); +CREATE INDEX idx_snp_health_log_checked ON common.snp_service_health_log (checked_at); +CREATE INDEX idx_health_log_svc_checked ON common.snp_service_health_log (service_id, checked_at DESC); +CREATE INDEX idx_health_log_daily_uptime ON common.snp_service_health_log (service_id, checked_at, current_status); + +-- 5. 오늘 + 미래 7일 파티션 생성 (실행 시점에 맞게 날짜 수정) +-- 예시: 2026-04-13 실행 기준 +CREATE TABLE common.snp_service_health_log_20260413 + PARTITION OF common.snp_service_health_log + FOR VALUES FROM ('2026-04-13') TO ('2026-04-14'); +CREATE TABLE common.snp_service_health_log_20260414 + PARTITION OF common.snp_service_health_log + FOR VALUES FROM ('2026-04-14') TO ('2026-04-15'); +CREATE TABLE common.snp_service_health_log_20260415 + PARTITION OF common.snp_service_health_log + FOR VALUES FROM ('2026-04-15') TO ('2026-04-16'); +CREATE TABLE common.snp_service_health_log_20260416 + PARTITION OF common.snp_service_health_log + FOR VALUES FROM ('2026-04-16') TO ('2026-04-17'); +CREATE TABLE common.snp_service_health_log_20260417 + PARTITION OF common.snp_service_health_log + FOR VALUES FROM ('2026-04-17') TO ('2026-04-18'); +CREATE TABLE common.snp_service_health_log_20260418 + PARTITION OF common.snp_service_health_log + FOR VALUES FROM ('2026-04-18') TO ('2026-04-19'); +CREATE TABLE common.snp_service_health_log_20260419 + PARTITION OF common.snp_service_health_log + FOR VALUES FROM ('2026-04-19') TO ('2026-04-20'); +CREATE TABLE common.snp_service_health_log_20260420 + PARTITION OF common.snp_service_health_log + FOR VALUES FROM ('2026-04-20') TO ('2026-04-21'); + +-- 6. 데이터 복원 (컬럼 명시) +INSERT INTO common.snp_service_health_log ( + log_id, service_id, previous_status, current_status, response_time, error_message, checked_at +) SELECT + log_id, service_id, previous_status, current_status, response_time, error_message, checked_at +FROM common.snp_service_health_log_backup +WHERE checked_at >= '2026-04-13'; + +-- 7. 시퀀스 리셋 +SELECT setval(pg_get_serial_sequence('common.snp_service_health_log', 'log_id'), + (SELECT COALESCE(MAX(log_id), 0) FROM common.snp_service_health_log)); + +-- 8. 백업 삭제 (확인 후) +-- DROP TABLE common.snp_service_health_log_backup; diff --git a/src/main/java/com/gcsc/connection/monitoring/entity/SnpServiceHealthLog.java b/src/main/java/com/gcsc/connection/monitoring/entity/SnpServiceHealthLog.java index 887770e..c606e86 100644 --- a/src/main/java/com/gcsc/connection/monitoring/entity/SnpServiceHealthLog.java +++ b/src/main/java/com/gcsc/connection/monitoring/entity/SnpServiceHealthLog.java @@ -2,8 +2,10 @@ package com.gcsc.connection.monitoring.entity; import com.gcsc.connection.service.entity.SnpService; import jakarta.persistence.Column; +import jakarta.persistence.ConstraintMode; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -28,7 +30,8 @@ public class SnpServiceHealthLog { private Long logId; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "service_id", nullable = false) + @JoinColumn(name = "service_id", nullable = false, + foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) private SnpService service; @Column(name = "previous_status", length = 10) diff --git a/src/main/java/com/gcsc/connection/monitoring/scheduler/DataCleanupScheduler.java b/src/main/java/com/gcsc/connection/monitoring/scheduler/DataCleanupScheduler.java index 9d87cf4..e128031 100644 --- a/src/main/java/com/gcsc/connection/monitoring/scheduler/DataCleanupScheduler.java +++ b/src/main/java/com/gcsc/connection/monitoring/scheduler/DataCleanupScheduler.java @@ -1,38 +1,23 @@ package com.gcsc.connection.monitoring.scheduler; -import com.gcsc.connection.monitoring.repository.SnpServiceHealthLogRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDateTime; @Slf4j @Component @RequiredArgsConstructor public class DataCleanupScheduler { - private final SnpServiceHealthLogRepository healthLogRepository; - - @Value("${app.retention.health-log-days:90}") - private int healthLogRetentionDays; - /** * 매일 02:00 실행 - 오래된 데이터 정리 + * Health log는 PartitionManageScheduler에서 파티션 DROP으로 관리 */ @Scheduled(cron = "0 0 2 * * *") - @Transactional public void cleanupOldData() { log.info("데이터 정리 시작"); - - // Health log 정리 - LocalDateTime healthCutoff = LocalDateTime.now().minusDays(healthLogRetentionDays); - int deletedHealthLogs = healthLogRepository.deleteOlderThan(healthCutoff); - log.info("Health log 정리 완료: {}건 삭제 (기준: {}일 이전)", deletedHealthLogs, healthLogRetentionDays); - - log.info("데이터 정리 완료"); + // Health log는 PartitionManageScheduler에서 파티션 DROP으로 관리 + log.info("데이터 정리 완료 (health_log는 파티션으로 관리)"); } } diff --git a/src/main/java/com/gcsc/connection/monitoring/scheduler/PartitionManageScheduler.java b/src/main/java/com/gcsc/connection/monitoring/scheduler/PartitionManageScheduler.java index a356f67..22f14ba 100644 --- a/src/main/java/com/gcsc/connection/monitoring/scheduler/PartitionManageScheduler.java +++ b/src/main/java/com/gcsc/connection/monitoring/scheduler/PartitionManageScheduler.java @@ -7,6 +7,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; +import java.time.LocalDate; import java.time.YearMonth; import java.time.format.DateTimeFormatter; import java.util.List; @@ -27,6 +28,10 @@ public class PartitionManageScheduler { @Value("${app.retention.request-log-days:90}") private int retentionDays; + private static final String HEALTH_LOG_TABLE = "common.snp_service_health_log"; + private static final String HEALTH_LOG_PREFIX = "snp_service_health_log_"; + private static final int ADVANCE_DAYS = 7; + /** * 매월 1일 00:00 실행 - 파티션 생성/삭제 */ @@ -64,6 +69,45 @@ public class PartitionManageScheduler { log.info("파티션 관리 완료"); } + /** + * 매일 00:30 실행 - health_log 일별 파티션 관리 + */ + @Scheduled(cron = "0 30 0 * * *") + public void manageHealthLogPartitions() { + if (!partitionEnabled) { + return; + } + + log.info("Health log 파티션 관리 시작"); + LocalDate today = LocalDate.now(); + + // 미래 7일 파티션 생성 + for (int i = 0; i <= ADVANCE_DAYS; i++) { + partitionService.createDailyPartition(HEALTH_LOG_TABLE, today.plusDays(i)); + } + + // 보관기간 이전 파티션 삭제 + LocalDate cutoff = today.minusDays(retentionDays); + List existing = partitionService.getExistingPartitions(HEALTH_LOG_PREFIX); + + DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyyMMdd"); + String cutoffStr = HEALTH_LOG_PREFIX + cutoff.format(fmt); + + for (String partition : existing) { + if (partition.compareTo(cutoffStr) < 0) { + try { + LocalDate partDate = LocalDate.parse( + partition.replace(HEALTH_LOG_PREFIX, ""), fmt); + partitionService.dropDailyPartition(HEALTH_LOG_TABLE, partDate); + } catch (Exception e) { + log.warn("파티션 날짜 파싱 실패: {}", partition); + } + } + } + + log.info("Health log 파티션 관리 완료"); + } + private YearMonth parsePartitionYearMonth(String partitionName) { try { String suffix = partitionName.replace("snp_api_request_log_", ""); diff --git a/src/main/java/com/gcsc/connection/monitoring/service/PartitionService.java b/src/main/java/com/gcsc/connection/monitoring/service/PartitionService.java index 97119be..ca09d37 100644 --- a/src/main/java/com/gcsc/connection/monitoring/service/PartitionService.java +++ b/src/main/java/com/gcsc/connection/monitoring/service/PartitionService.java @@ -57,11 +57,53 @@ public class PartitionService { } /** - * 기존 파티션 목록 조회 + * 기존 파티션 목록 조회 (API 요청 로그용 - 하위 호환) */ public List getExistingPartitions() { + return getExistingPartitions(PARTITION_PREFIX); + } + + /** + * 기존 파티션 목록 조회 (테이블 접두사 지정) + */ + public List getExistingPartitions(String tablePrefix) { String sql = "SELECT tablename FROM pg_tables WHERE schemaname = 'common' AND tablename LIKE '" - + PARTITION_PREFIX + "%' ORDER BY tablename"; + + tablePrefix + "%' ORDER BY tablename"; return jdbcTemplate.queryForList(sql, String.class); } + + /** + * 일별 파티션 생성 + */ + public void createDailyPartition(String tableName, LocalDate date) { + String partitionName = tableName + "_" + date.format(DateTimeFormatter.ofPattern("yyyyMMdd")); + LocalDate nextDay = date.plusDays(1); + + String sql = String.format( + "CREATE TABLE IF NOT EXISTS %s PARTITION OF %s FOR VALUES FROM ('%s') TO ('%s')", + partitionName, tableName, date, nextDay); + + try { + jdbcTemplate.execute(sql); + log.info("일별 파티션 생성 완료: {}", partitionName); + } catch (Exception e) { + log.error("일별 파티션 생성 실패: {}", partitionName, e); + } + } + + /** + * 일별 파티션 삭제 + */ + public void dropDailyPartition(String tableName, LocalDate date) { + String partitionName = tableName + "_" + date.format(DateTimeFormatter.ofPattern("yyyyMMdd")); + + String sql = String.format("DROP TABLE IF EXISTS %s", partitionName); + + try { + jdbcTemplate.execute(sql); + log.info("일별 파티션 삭제 완료: {}", partitionName); + } catch (Exception e) { + log.error("일별 파티션 삭제 실패: {}", partitionName, e); + } + } }