"""
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}...")
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")
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(
[ "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
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()
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()
print(f"\n💾 [정산 마감] 분석 데이터 자산 output 레이어로 내보내기 진행 중...")
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"
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()