26Y03b4

Young-Kyoo Kim·2026년 7월 3일
"""
step4_governance_analyzer.py — Adaptive Multi-Cluster FinOps Governance & Risk Analyzer (Partition-Isolated Version)
"""
import os
import glob
import argparse
import pandas as pd
import numpy as np
from pathlib import Path

# ─── 📂 경로 표준화 설정 ──────────────────────────────────────────────────
BASE_DATA_DIR = Path("./data")
MERGED_DIR    = BASE_DATA_DIR / "merged"
OUTPUT_DIR    = BASE_DATA_DIR / "output"

# 출력 디렉토리 자동 생성
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)

def parse_arguments():
    parser = argparse.ArgumentParser(description="FinOps Governance & Anomaly Analyzer")
    # 💡 [교정] INTEGRATED/ALL 단계를 완전 폐기하고 타 스택과 완벽 대칭을 이루도록 대상 클러스터를 필수 요구
    parser.add_argument("--cluster", type=str, required=True, choices=["COMPUTE", "STORAGE"], help="Target cluster type")
    return parser.parse_args()

def load_partitioned_data(cluster_target):
    """
    개편된 2단계 분할 정복 하위 폴더 위계를 스캔하여 일별 Parquet 파일들을 동적으로 결합합니다.
    """
    # 💡 [교정] 타겟 클러스터 폴더 내부에 격리되어 저장된 일별 자산들만 정밀 타격 스캔
    cl_dir = MERGED_DIR / cluster_target
    if not cl_dir.exists():
        return pd.DataFrame(), pd.DataFrame(), pd.DataFrame()
        
    pod_files    = glob.glob(str(cl_dir / "daily_enriched_*.parquet"))
    ns_files     = glob.glob(str(cl_dir / "daily_ns_usage_*.parquet"))
    pareto_files = glob.glob(str(cl_dir / "pareto_ns_*.parquet"))
            
    print(f"🔍 [스캔 완료] {cluster_target} 파티션 하방 -> Enriched: {len(pod_files)}개 | NS요약: {len(ns_files)}개 | Pareto: {len(pareto_files)}개")
    
    # 파일이 단 하나도 없을 경우의 빈 데이터프레임 예외 예방 가드레일
    df_pod = pd.concat([pd.read_parquet(f) for f in pod_files], ignore_index=True) if pod_files else pd.DataFrame()
    df_ns  = pd.concat([pd.read_parquet(f) for f in ns_files], ignore_index=True) if ns_files else pd.DataFrame()
    df_pt  = pd.concat([pd.read_parquet(f) for f in pareto_files], ignore_index=True) if pareto_files else pd.DataFrame()
    
    return df_pod, df_ns, df_pt

def main():
    args = parse_arguments()
    cluster_target = args.cluster.upper()
    print(f"🚀 [Step4] Starting FinOps Governance Analyzer for Cluster: {cluster_target}...")
    
    # 1. 쪼개져 있는 2단계 결과물 격리 로드
    df_pod, df_ns, df_pt = load_partitioned_data(cluster_target)
    
    if df_pod.empty:
        print(f"❌ [중단] '{cluster_target}' 클러스터에 정산된 2단계 가공 원부가 존재하지 않습니다. 앞단 배치를 확인하세요.")
        return
        
    print(f"✅ 총 {len(df_pod):,}개의 컨테이너 타임라인 원부 통합 바인딩 완료.\n")

    # 2. 🚨 [거버넌스 분석 레이어 1] 자원부족 및 OOM Killed 위험군 추출 (시트4 매핑용)
    print("🚨 [Analysis 1] Extracting Critical Risk & OOM Killed Pods...")
    df_risk = df_pod[
        (df_pod["status"] == "💥 OOM장애발생") | 
        (df_pod["status"] == "⚠️ Request부족") | 
        (df_pod["oom_strike_sum"] > 0)
    ].copy()
    
    # 💡 [교정] 인덱스 미스매칭 크래시 방지를 위해 np.where 벡터화 구문으로 안전하게 조치 가이드 주입
    if not df_risk.empty:
        df_risk["recommendation"] = np.where(
            df_risk["is_oom_killed"],
            "Upsize Memory Limit (OOM Detected)",
            "Upsize CPU Request/Limit (Throttling Detected)"
        )
    else:
        df_risk["recommendation"] = None

    # 3. 📉 [거버넌스 분석 레이어 2] 과다 할당 및 유휴 자산 추출 (시트2, 3 하향 후보용)
    print("📉 [Analysis 2] Extracting Over-Allocated & Idle Infrastructure Assets...")
    df_waste = df_pod[
        (df_pod["status"] == "📉 과다할당") & 
        (df_pod["cpu_waste_core_hours"] > 24)  # 하루 이상 코어 하나 통째로 낭비한 기준
    ].sort_values(by="cpu_waste_core_hours", ascending=False).copy()

    # 4. 🛡️ [거버넌스 분석 레이어 3] 자원 미설정 배포 위반군 추출 (시트5 매핑용)
    print("🛡️ [Analysis 3] Scanning Non-Compliant Missing Resource Specification Pods...")
    df_violations = df_pod[
        (df_pod["has_no_request"] == True) | 
        (df_pod["has_no_limit"] == True)
    ].copy()

    # 5. 💾 [결과 마감] 6단계 엑셀 빌더가 다이렉트로 인식할 수 있도록 산출물 저장
    print(f"\n💾 [정산 마감] 분석 데이터 자산 output 레이어로 내보내기 진행 중...")
    
    # 💡 [교정] 모놀리식 접미사를 전면 제거하고 명확한 클러스터 고유 타겟 식별자로 파일명 고정
    master_gov_file = OUTPUT_DIR / f"governance_master_{cluster_target}.parquet"
    risk_file       = OUTPUT_DIR / f"gov_risk_oom_{cluster_target}.parquet"
    waste_file      = OUTPUT_DIR / f"gov_waste_candidates_{cluster_target}.parquet"
    viol_file       = OUTPUT_DIR / f"gov_violations_{cluster_target}.parquet"
    
    # Parquet 압축 저장
    df_pod.to_parquet(master_gov_file, index=False)
    df_risk.to_parquet(risk_file, index=False)
    df_waste.to_parquet(waste_file, index=False)
    df_violations.to_parquet(viol_file, index=False)
    
    # 검증 서머리 출력
    print(f"  -> 📦 [저장완료] 전사 {cluster_target} 마스터 원부 : {master_gov_file.name}")
    print(f"  -> 💥 [저장완료] 고위험군/OOM 리스트    : {risk_file.name} (결과: {len(df_risk)}건)")
    print(f"  -> 📉 [저장완료] 자원 하향조정 후보군  : {waste_file.name} (결과: {len(df_waste)}건)")
    print(f"  -> 🛡️ [저장완료] 스펙 미설정 규격위반군: {viol_file.name} (결과: {len(df_violations)}건)")
    
    print(f"\n🏁 === [Step4 완수] '{cluster_target}' 거버넌스 가공 원부가 무결하게 갱신되었습니다. ===")

if __name__ == "__main__":
    main()

0개의 댓글