26Y02m2

Young-Kyoo Kim·4일 전
"""
step4_governance_analyzer.py — Adaptive Multi-Cluster FinOps Governance & Risk Analyzer (Pandas Standard)
"""
import os
import glob
import argparse
import pandas as pd
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")
    parser.add_argument("--cluster", type=str, default="ALL", choices=["COMPUTE", "STORAGE", "ALL"])
    return parser.parse_args()

def load_partitioned_data(cluster_target):
    """
    개편된 2단계 분할 정복 하위 폴더 위계를 스캔하여 일별 Parquet 파일들을 동적으로 결합합니다.
    """
    target_clusters = ["COMPUTE", "STORAGE"] if cluster_target.upper() == "ALL" else [cluster_target.upper()]
    
    pod_files = []
    ns_files = []
    pareto_files = []
    
    # 💡 [적응형 픽업 핵심 구역] 지정된 클러스터 폴더 하방의 모든 일별 파일들을 수집
    for cl in target_clusters:
        cl_dir = MERGED_DIR / cl
        if cl_dir.exists():
            pod_files.extend(glob.glob(str(cl_dir / "daily_enriched_*.parquet")))
            ns_files.extend(glob.glob(str(cl_dir / "daily_ns_usage_*.parquet")))
            pareto_files.extend(glob.glob(str(cl_dir / "pareto_ns_*.parquet")))
            
    print(f"🔍 [스캔 완료] 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()
    print(f"🚀 [Step4] Starting FinOps Governance Analyzer for Cluster: {args.cluster}...")
    
    # 1. 쪼개져 있는 2단계 결과물 전체 로드 (적응형 복구)
    df_pod, df_ns, df_pt = load_partitioned_data(args.cluster)
    
    if df_pod.empty:
        print(f"❌ [중단] '{args.cluster}' 클러스터에 정산된 2단계 가공 원부가 존재하지 않습니다. 앞단 배치를 확인하세요.")
        return
        
    cluster_suffix = args.cluster.upper() if args.cluster.upper() != "ALL" else "INTEGRATED"
    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()
    
    # 조치 가이드 컬럼 인젝션
    if not df_risk.empty:
        df_risk["recommendation"] = pd.DataFrame(
            # OOM일 경우 Limit 상향, 쓰로틀링/자원부족일 경우 Request 상향 가이드 명시
            [ "Upsize Memory Limit (OOM Detected)" if x else "Upsize CPU Request/Limit (Throttling Detected)" for x in df_risk["is_oom_killed"] ]
        )
    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 레이어로 내보내기 진행 중...")
    
    # 6단계 엑셀 빌더 경로 명명 규칙 통합 바인딩
    master_gov_file = OUTPUT_DIR / f"governance_master_{cluster_suffix}.parquet"
    risk_file       = OUTPUT_DIR / f"gov_risk_oom_{cluster_suffix}.parquet"
    waste_file      = OUTPUT_DIR / f"gov_waste_candidates_{cluster_suffix}.parquet"
    viol_file       = OUTPUT_DIR / f"gov_violations_{cluster_suffix}.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"  -> 📦 [저장완료] 전사 통합 마스터 원부 : {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_suffix}' 거버넌스 가공 원부가 무결하게 갱신되었습니다. ===")

if __name__ == "__main__":
    main()

0개의 댓글