diff --git a/.gitignore b/.gitignore index db9bd29a..5970b8e8 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,7 @@ Thumbs.db .env.*.local application-local.yml application-secret.yml +metrics-collector/.env ### Docker (local testing) ### .dockerignore @@ -72,3 +73,8 @@ docker-compose.override.yml *.swo *~ !/CLAUDE.md + +### Metrics Collector ### +metrics-collector/venv/ +metrics-collector/*.pid +metrics-collector/wheels/ diff --git a/src/main/java/com/kamco/cd/kamcoback/common/enums/LayerType.java b/src/main/java/com/kamco/cd/kamcoback/common/enums/LayerType.java index 99816656..4cff54f7 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/enums/LayerType.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/enums/LayerType.java @@ -13,7 +13,9 @@ public enum LayerType implements EnumType { TILE("배경지도"), GEOJSON("객체데이터"), WMTS("타일레이어"), - WMS("지적도"); + WMS("지적도"), + KAMCO_WMS("국유인WMS"), + KAMCO_WMTS("국유인WMTS"); private final String desc; diff --git a/src/main/java/com/kamco/cd/kamcoback/common/inference/utils/GeoJsonValidator.java b/src/main/java/com/kamco/cd/kamcoback/common/inference/utils/GeoJsonValidator.java index ec0b9fa4..53c97490 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/inference/utils/GeoJsonValidator.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/inference/utils/GeoJsonValidator.java @@ -5,11 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -151,7 +147,7 @@ public class GeoJsonValidator { Set foundUnique = new HashSet<>(); // 중복된 scene_id 목록 (샘플 로그 출력용이라 순서 유지 가능한 LinkedHashSet 사용) - Set duplicates = new LinkedHashSet<>(); + // Set duplicates = new LinkedHashSet<>(); // scene_id가 null 또는 blank인 feature의 개수 (데이터 이상) int nullIdCount = 0; @@ -168,48 +164,24 @@ public class GeoJsonValidator { // "geometry": {...} // } // --------------------------------------------------------- - - int sampleIdx = 0; - for (JsonNode feature : features) { JsonNode props = feature.get("properties"); // properties가 있고 scene_id가 null이 아니면 텍스트로 읽음 // 없으면 null 처리 String sceneId = - (props != null && props.hasNonNull("scene_id")) - ? props.get("scene_id").asText().trim() - : null; - - log.info("========== sceneId : {}", sceneId); - - if (sampleIdx < 5) { - log.info( - "[DEBUG] feature sample idx={}, propsExists={}, sceneId={}", - sampleIdx, - props != null, - sceneId); - sampleIdx++; - } + (props != null && props.hasNonNull("scene_id")) ? props.get("scene_id").asText() : null; // scene_id가 없거나 빈값이면 "정상적으로 도엽번호가 들어오지 않은 feature"로 카운트 if (sceneId == null || sceneId.isBlank()) { - log.info("========== sceneId 가 없어 continue"); nullIdCount++; // 도엽번호가 없으면 증가 continue; } // foundUnique.add(sceneId)가 false면 "이미 같은 값이 있었다"는 뜻 => 중복 - boolean added = foundUnique.add(sceneId); - - if (sampleIdx <= 5) { - log.info("[DEBUG] sceneId={}, added={}", sceneId, added); - } - - if (!added) { - log.info("========== foundUnique add가 안되어 duplicates 에 add"); - duplicates.add(sceneId); - } + // if (!foundUnique.add(sceneId)) { + // duplicates.add(sceneId); + // } } // ========================================================= @@ -237,38 +209,33 @@ public class GeoJsonValidator { // ========================================================= log.info( """ - ===== GeoJSON Validation ===== - file: {} - features(total): {} - requested(unique): {} - found(unique scene_id): {} - scene_id null/blank: {} - duplicates(scene_id): {} - missing(requested - found): {} - extra(found - requested): {} - ============================== - """, + ===== GeoJSON Validation ===== + file: {} + features(total): {} + requested(unique): {} + found(unique scene_id): {} + scene_id null/blank: {} + duplicates(scene_id): {} + missing(requested - found): {} + extra(found - requested): {} + ============================== + """, geojsonPath, featureCount, // 중복 포함한 전체 feature 수 requested.size(), // 요청 도엽 유니크 수 foundUnique.size(), // GeoJSON에서 발견된 scene_id 유니크 수 nullIdCount, // scene_id가 비어있는 feature 수 - duplicates.size(), // 중복 scene_id 종류 수 + 0, // 중복 scene_id 종류 수 missing.size(), // 요청했지만 빠진 도엽 수 extra.size()); // 요청하지 않았는데 들어온 도엽 수 // 중복/누락/추가 항목은 전체를 다 찍으면 로그 폭발하므로 샘플만 - if (!duplicates.isEmpty()) { - log.warn("duplicates sample: {}", duplicates.stream().limit(20).toList()); - } + // if (!duplicates.isEmpty()) + // log.warn("duplicates sample: {}", duplicates.stream().limit(20).toList()); - if (!missing.isEmpty()) { - log.warn("missing sample: {}", missing.stream().limit(50).toList()); - } + if (!missing.isEmpty()) log.warn("missing sample: {}", missing.stream().limit(50).toList()); - if (!extra.isEmpty()) { - log.warn("extra sample: {}", extra.stream().limit(50).toList()); - } + if (!extra.isEmpty()) log.warn("extra sample: {}", extra.stream().limit(50).toList()); // ========================================================= // 6) 실패 조건 판정 @@ -283,12 +250,12 @@ public class GeoJsonValidator { // - 요청 문법은 맞지만(파일은 있고 JSON도 읽힘), // 내용(정합성)이 요구사항을 만족하지 못하는 경우에 적합. // ========================================================= - if (!missing.isEmpty() || !extra.isEmpty() || !duplicates.isEmpty() || nullIdCount > 0) { + if (!missing.isEmpty() || !extra.isEmpty() || nullIdCount > 0) { throw new ResponseStatusException( HttpStatus.UNPROCESSABLE_ENTITY, String.format( "GeoJSON validation failed: missing=%d, extra=%d, duplicates=%d, nullId=%d", - missing.size(), extra.size(), duplicates.size(), nullIdCount)); + missing.size(), extra.size(), 0, nullIdCount)); } // 모든 조건을 통과하면 정상 diff --git a/src/main/java/com/kamco/cd/kamcoback/common/service/ExternalJarRunner.java b/src/main/java/com/kamco/cd/kamcoback/common/service/ExternalJarRunner.java index 94bc2cb7..5caaa5ba 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/service/ExternalJarRunner.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/service/ExternalJarRunner.java @@ -28,7 +28,8 @@ public class ExternalJarRunner { * @param mode *

MERGED - batch-ids 에 해당하는 **모든 데이터를 하나의 Shapefile로 병합 생성, *

MAP_IDS - 명시적으로 전달한 map-ids만 대상으로 Shapefile 생성, - *

RESOLVE - batch-ids 기준으로 **JAR 내부에서 map_ids를 조회**한 뒤 Shapefile 생성 + *

RESOLVE - batch-ids 기준으로 **JAR 내부에서 map_ids를 조회**한 뒤 Shapefile 생성 java -jar + * build/libs/shp-exporter.jar --spring.profiles.active=prod */ public void run(String jarPath, String batchIds, String inferenceId, String mapIds, String mode) { List args = new ArrayList<>(); diff --git a/src/main/java/com/kamco/cd/kamcoback/layer/service/LayerService.java b/src/main/java/com/kamco/cd/kamcoback/layer/service/LayerService.java index e486b446..a6849862 100644 --- a/src/main/java/com/kamco/cd/kamcoback/layer/service/LayerService.java +++ b/src/main/java/com/kamco/cd/kamcoback/layer/service/LayerService.java @@ -62,7 +62,7 @@ public class LayerService { .orElseThrow(() -> new CustomApiException("BAD_REQUEST", HttpStatus.BAD_REQUEST)); switch (layerType) { - case TILE -> { + case TILE, KAMCO_WMS, KAMCO_WMTS -> { return mapLayerCoreService.saveTile(dto); } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/GpuMetricEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/GpuMetricEntity.java index 23f74a52..064553ac 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/GpuMetricEntity.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/GpuMetricEntity.java @@ -11,37 +11,94 @@ import lombok.Getter; import lombok.Setter; import org.hibernate.annotations.ColumnDefault; +/** + * GPU 메트릭 엔티티 + * + *

서버의 GPU 성능 및 자원 사용량 메트릭 데이터를 저장하는 JPA 엔티티입니다. GPU 연산 사용률 및 메모리 사용량 등 GPU 리소스 모니터링 데이터를 관리합니다. + * + *

데이터 소스: nvidia-smi 명령어 또는 NVML (NVIDIA Management Library) + * + *

활용 사례: + * + *

    + *
  • AI/ML 학습 모니터링: 딥러닝 작업 중 GPU 활용도 추적 + *
  • 리소스 최적화: GPU 메모리 부족 또는 유휴 상태 감지 + *
  • 용량 계획: GPU 추가 필요 시점 예측 + *
  • 알림 설정: gpuUtil > 95% 또는 gpuMemUsed/gpuMemTotal > 90% 시 경고 + *
+ */ @Getter @Setter @Entity @Table(name = "gpu_metrics") public class GpuMetricEntity { + /** 기본 키 (UUID, 자동 생성) */ @Id @ColumnDefault("gen_random_uuid()") @Column(name = "uuid", nullable = false) private UUID id; + /** 시퀀스 기반 보조 ID */ @NotNull @ColumnDefault("nextval('gpu_metrics_id_seq')") @Column(name = "id", nullable = false) private Integer id1; + /** 메트릭 수집 시각 (시간대 포함, 기본값: 현재 시각) */ @NotNull @ColumnDefault("now()") @Column(name = "\"timestamp\"", nullable = false) private OffsetDateTime timestamp; + /** 모니터링 대상 서버 이름 */ @NotNull @Column(name = "server_name", nullable = false, length = Integer.MAX_VALUE) private String serverName; + /** + * GPU 연산 사용률 (백분율) + * + *

GPU 코어의 연산 처리 활용도를 나타냅니다. + * + *

범위: 0.0 ~ 100.0 + * + *

예시: 85.5 = GPU가 85.5% 활용되어 연산 중 + * + *

데이터 소스: nvidia-smi의 'utilization.gpu' 또는 NVML의 nvmlDeviceGetUtilizationRates + * + *

참고: 높은 사용률(>90%)은 GPU가 충분히 활용되고 있음을 의미하며, 낮은 사용률은 병목 지점이 다른 곳(CPU, I/O)에 있을 수 있음 + */ @Column(name = "gpu_util") private Float gpuUtil; + /** + * GPU 메모리 사용량 (MB 단위) + * + *

현재 GPU에 할당되어 사용 중인 메모리 양 + * + *

예시: 10240.0 = 약 10GB의 GPU 메모리 사용 중 + * + *

데이터 소스: nvidia-smi의 'memory.used' 또는 NVML의 nvmlDeviceGetMemoryInfo + * + *

용도: 딥러닝 모델 크기, 배치 사이즈 최적화, OOM(Out Of Memory) 에러 예측 + */ @Column(name = "gpu_mem_used") private Float gpuMemUsed; + /** + * GPU 총 메모리 용량 (MB 단위) + * + *

GPU에 장착된 전체 메모리 용량 + * + *

예시: 16384.0 = 16GB VRAM 장착 + * + *

데이터 소스: nvidia-smi의 'memory.total' 또는 NVML의 nvmlDeviceGetMemoryInfo + * + *

계산식: 메모리 사용률(%) = (gpuMemUsed / gpuMemTotal) × 100 + * + *

활용: 여유 메모리 = gpuMemTotal - gpuMemUsed + */ @Column(name = "gpu_mem_total") private Float gpuMemTotal; } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/SystemMetricEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/SystemMetricEntity.java index 05e5bb02..27e8d67b 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/SystemMetricEntity.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/SystemMetricEntity.java @@ -11,48 +11,101 @@ import lombok.Getter; import lombok.Setter; import org.hibernate.annotations.ColumnDefault; +/** + * 시스템 메트릭 엔티티 + * + *

서버 시스템의 성능 메트릭 데이터를 저장하는 JPA 엔티티입니다. CPU 및 메모리 사용량 등 시스템 리소스 모니터링 데이터를 관리합니다. + * + *

데이터 소스: Linux sar 명령어 또는 /proc/meminfo 파일 + * + *

활용 사례: + * + *

    + *
  • 용량 계획: 메모리 추가 필요 시점 예측 + *
  • 성능 모니터링: 메모리 부족 상황 감지 + *
  • 트렌드 분석: 시간대별 메모리 사용 패턴 파악 + *
  • 알림 설정: memused > 90% 시 경고 + *
+ */ @Getter @Setter @Entity @Table(name = "system_metrics") public class SystemMetricEntity { + /** 기본 키 (UUID, 자동 생성) */ @Id @ColumnDefault("gen_random_uuid()") @Column(name = "uuid", nullable = false) private UUID id; + /** 시퀀스 기반 보조 ID */ @NotNull @ColumnDefault("nextval('system_metrics_id_seq')") @Column(name = "id", nullable = false) private Integer id1; + /** 메트릭 수집 시각 (시간대 포함) */ @NotNull @Column(name = "\"timestamp\"", nullable = false) private OffsetDateTime timestamp; + /** 모니터링 대상 서버 이름 */ @NotNull @Column(name = "server_name", nullable = false, length = Integer.MAX_VALUE) private String serverName; + /** 사용자 프로세스가 사용한 CPU 사용률 (%) - 응용 프로그램 실행 */ @Column(name = "cpu_user") private Float cpuUser; + /** 시스템 프로세스가 사용한 CPU 사용률 (%) - 커널 작업 */ @Column(name = "cpu_system") private Float cpuSystem; + /** I/O 대기로 소모된 CPU 사용률 (%) - 디스크/네트워크 대기 */ @Column(name = "cpu_iowait") private Float cpuIowait; + /** 유휴 상태 CPU 사용률 (%) - 사용 가능한 여유 CPU */ @Column(name = "cpu_idle") private Float cpuIdle; + /** + * 사용 가능한 여유 메모리 (KB 단위) + * + *

시스템에서 즉시 사용 가능한 물리 메모리 양 + * + *

예시: 4194304 = 약 4GB의 여유 메모리 + * + *

데이터 소스: /proc/meminfo의 MemFree + */ @Column(name = "kbmemfree") private Long kbmemfree; + /** + * 현재 사용 중인 메모리 (KB 단위) + * + *

시스템이 현재 할당하여 사용 중인 물리 메모리 양 + * + *

예시: 8388608 = 약 8GB의 사용 중인 메모리 + * + *

계산: MemTotal - MemFree + */ @Column(name = "kbmemused") private Long kbmemused; + /** + * 메모리 사용률 (백분율) + * + *

전체 메모리 대비 사용 중인 메모리 비율 + * + *

계산식: (kbmemused / (kbmemused + kbmemfree)) × 100 + * + *

예시: 66.7 = 전체 메모리의 66.7% 사용 중 + * + *

관계식: 총 메모리 = kbmemused + kbmemfree + */ @Column(name = "memused") private Float memused; }