26Y02m1

Young-Kyoo Kim·3일 전
"""
step3_analytics.py — Long-term Time-series Infrastructure Data Visualization Engine (Partitioned & Safe Version)
"""
import os
import re
import argparse
import pandas as pd
import numpy as np
from pathlib import Path

# ─── 🛡️ [Headless Environment Guard] Prevents crashes in K8s pods without display servers ───
import matplotlib
matplotlib.use('Agg') 
import matplotlib.pyplot as plt
import seaborn as sns

# ─── 📂 Directory Path Standardization (./data) ──────────────────────────
BASE_DATA_DIR = Path("./data")
MERGED_DIR    = BASE_DATA_DIR / "merged"
BASE_PLOT_DIR = BASE_DATA_DIR / "output" / "plots"

# Ensure base plot directory exists
BASE_PLOT_DIR.mkdir(parents=True, exist_ok=True)

# ─── 🔤 [No More Tofu] Use bulletproof built-in standard fonts ───
plt.rcParams['font.family'] = 'sans-serif'
plt.rcParams['axes.unicode_minus'] = False  # Prevents minus sign (-) corruption
sns.set_theme(style="whitegrid")

# ─── 🛡️ [완치 가드레일] 빈 데이터셋 감지 시 플레이스홀더 PNG 생성 함수 ───
def check_and_handle_empty(df, output_path, chart_name):
    """
    데이터가 비어있으면 True를 반환하고, 6단계 엑셀 빌더가 터지지 않도록
    'No Data Available' 문구가 적힌 플레이스홀더 이미지를 강제 생성합니다.
    """
    if df is None or df.empty:
        print(f"  ⚠️  [Empty Data] Generating placeholder for {chart_name} (Dataset is empty).")
        fig, ax = plt.subplots(figsize=(10, 5))
        ax.text(0.5, 0.5, f"No Data Available\n({chart_name})", 
                ha='center', va='center', fontsize=14, color='gray', weight='bold')
        ax.axis('off')
        plt.tight_layout()
        plt.savefig(output_path, dpi=100, bbox_inches='tight')
        plt.close()
        return True
    return False

def main():
    parser = argparse.ArgumentParser(description="FinOps Analytics Visualization Engine")
    parser.add_argument("--cluster", type=str, default="ALL", choices=["COMPUTE", "STORAGE", "ALL"])
    args = parser.parse_args()

    print(f"🚀 [Step3] Starting FinOps Time-series English Visualization Engine for Cluster: {args.cluster}...")
    
    # ─── 🗂️ 클러스터별 파티션 파일 서치 및 가변 결합 (분할 정복) ───
    target_clusters = ["COMPUTE", "STORAGE"] if args.cluster.upper() == "ALL" else [args.cluster.upper()]
    
    all_pod_dfs = []
    all_ns_dfs = []
    
    for cl in target_clusters:
        cl_dir = MERGED_DIR / cl
        if cl_dir.exists():
            all_pod_dfs.extend([pd.read_parquet(f) for f in cl_dir.glob("daily_enriched_*.parquet")])
            all_ns_dfs.extend([pd.read_parquet(f) for f in cl_dir.glob("pareto_ns_*.parquet")])
            
    if not all_pod_dfs:
        print(f"❌ Error: No processed daily Parquet files found for cluster '{args.cluster}'. Please run step2 first.")
        return
        
    df_pod = pd.concat(all_pod_dfs, ignore_index=True)
    df_ns  = pd.concat(all_ns_dfs, ignore_index=True) if all_ns_dfs else pd.DataFrame()
    
    # 출력 경로 역시 클러스터별 하위 디렉토리로 격리 파티셔닝
    cluster_type_str = args.cluster.upper() if args.cluster.upper() != "ALL" else "INTEGRATED"
    PLOT_DIR = BASE_PLOT_DIR / cluster_type_str
    PLOT_DIR.mkdir(parents=True, exist_ok=True)
    
    print(f"✅ Data lake aggregated successfully -> Container rows: {len(df_pod):,}")
    
    # 데이터 정렬 오염 차단용 형변환 Guard
    df_pod["date"] = df_pod["date"].astype(str)

    # Injecting efficiency metric layers
    df_pod["cpu_util"] = np.where(df_pod["cpu_request_max"] > 0, (df_pod["cpu_usage_p95"] / df_pod["cpu_request_max"] * 100), 0)
    df_pod["mem_util"] = np.where(df_pod["mem_request_max"] > 0, (df_pod["mem_usage_p95"] / df_pod["mem_request_max"] * 100), 0)
    df_pod["lim_req_ratio"] = np.where(df_pod["cpu_request_max"] > 0, df_pod["cpu_limit_max"] / df_pod["cpu_request_max"], 0)

    # ─── 📊 [Charts 1 & 2] Workload Type Allocated vs Actual Peak ───
    print("⏳ [1/19] Rendering chart1_cpu_req_vs_usage_by_workload...")
    out1 = PLOT_DIR / "chart1_cpu_req_vs_usage_by_workload.png"
    if not check_and_handle_empty(df_pod, out1, "chart1_cpu_req_vs_usage_by_workload"):
        df_wl_cpu = df_pod.groupby("workload_type")[["cpu_request_max", "cpu_usage_p95"]].mean().reset_index()
        plt.figure(figsize=(10, 5))
        df_melt_cpu = df_wl_cpu.melt(id_vars="workload_type", value_vars=["cpu_request_max", "cpu_usage_p95"])
        sns.barplot(data=df_melt_cpu, x="workload_type", y="value", hue="variable", palette="Blues_r")
        plt.xticks(rotation=30, ha='right')
        plt.title("Average CPU Request vs P95 Peak Usage by Workload Type")
        plt.xlabel("Workload Type")
        plt.ylabel("CPU Cores")
        plt.tight_layout()
        plt.savefig(out1, dpi=100)
        plt.close()

    print("⏳ [2/19] Rendering chart2_mem_req_vs_usage_by_workload...")
    out2 = PLOT_DIR / "chart2_mem_req_vs_usage_by_workload.png"
    if not check_and_handle_empty(df_pod, out2, "chart2_mem_req_vs_usage_by_workload"):
        df_wl_mem = df_pod.groupby("workload_type")[["mem_request_max", "mem_usage_p95"]].mean().reset_index()
        plt.figure(figsize=(10, 5))
        df_melt_mem = df_wl_mem.melt(id_vars="workload_type", value_vars=["mem_request_max", "mem_usage_p95"])
        sns.barplot(data=df_melt_mem, x="workload_type", y="value", hue="variable", palette="Purples_r")
        plt.xticks(rotation=30, ha='right')
        plt.title("Average Memory Request vs P95 Peak Usage (GB) by Workload Type")
        plt.xlabel("Workload Type")
        plt.ylabel("Memory Capacity (GB)")
        plt.tight_layout()
        plt.savefig(out2, dpi=100)
        plt.close()

    # ─── 📊 [Chart 3] Daily CPU Waste Stacked Bar ───
    print("⏳ [3/19] Rendering chart3_daily_waste_stack...")
    out3 = PLOT_DIR / "chart3_daily_waste_stack.png"
    df_daily_waste = df_pod.groupby(["date", "workload_type"])["cpu_waste_core_hours"].sum().unstack().fillna(0) if not df_pod.empty else pd.DataFrame()
    if not check_and_handle_empty(df_daily_waste, out3, "chart3_daily_waste_stack"):
        df_daily_waste.plot(kind='bar', stacked=True, figsize=(11, 5), cmap="tab20")
        plt.title("Daily Total CPU Waste Core-Hours Stacked by Workload (KST)")
        plt.xlabel("Date (KST)")
        plt.ylabel("Waste Volume (Core-Hours)")
        plt.xticks(rotation=45)
        plt.tight_layout()
        plt.savefig(out3, dpi=100)
        plt.close()

    # ─── 📊 [Charts 4 & 18] Efficiency Heatmaps ───
    print("⏳ [4/19] Rendering chart4_cpu_efficiency_heatmap...")
    out4 = PLOT_DIR / "chart4_cpu_efficiency_heatmap.png"
    df_heat_cpu = df_pod.groupby(["workload_type", "date"])["cpu_util"].mean().unstack().fillna(0) if not df_pod.empty else pd.DataFrame()
    if not check_and_handle_empty(df_heat_cpu, out4, "chart4_cpu_efficiency_heatmap"):
        plt.figure(figsize=(10, 5))
        sns.heatmap(df_heat_cpu, annot=True, fmt=".1f", cmap="RdYlGn", cbar=True)
        plt.title("Mean CPU Utilization Heatmap (%) (Workload Type x Date)")
        plt.xlabel("Date (KST)")
        plt.ylabel("Workload Type")
        plt.tight_layout()
        plt.savefig(out4, dpi=100)
        plt.close()

    print("⏳ [5/19] Rendering chart18_mem_waste_heatmap...")
    out18 = PLOT_DIR / "chart18_mem_waste_heatmap.png"
    df_heat_mem = df_pod.groupby(["workload_type", "date"])["mem_waste_gb_hours"].sum().unstack().fillna(0) if not df_pod.empty else pd.DataFrame()
    if not check_and_handle_empty(df_heat_mem, out18, "chart18_mem_waste_heatmap"):
        plt.figure(figsize=(10, 5))
        sns.heatmap(df_heat_mem, annot=False, cmap="BuPu", cbar=True)
        plt.title("Total Memory Waste Volume Heatmap (GB-Hours)")
        plt.xlabel("Date (KST)")
        plt.ylabel("Workload Type")
        plt.tight_layout()
        plt.savefig(out18, dpi=100)
        plt.close()

    # ─── 📊 [Charts 5 & 11] Pareto Governance Charts ───
    print("⏳ [6/19] Rendering chart5_pareto_ns_waste...")
    out5 = PLOT_DIR / "chart5_pareto_ns_waste.png"
    if not check_and_handle_empty(df_ns, out5, "chart5_pareto_ns_waste"):
        # 단일 테넌트 중복 방지용 정렬 및 상위 스캔
        df_ns_top = df_ns.groupby("namespace")["total_waste_core_hours"].sum().reset_index().sort_values("total_waste_core_hours", ascending=False).head(15)
        df_ns_top["waste_share_pct"] = (df_ns_top["total_waste_core_hours"] / df_ns_top["total_waste_core_hours"].sum() * 100).round(2)
        df_ns_top["waste_cumsum_pct"] = df_ns_top["waste_share_pct"].cumsum().round(2)
        
        fig, ax1 = plt.subplots(figsize=(11, 5))
        sns.barplot(data=df_ns_top, x="namespace", y="total_waste_core_hours", ax=ax1, color="steelblue")
        ax1.set_xticklabels(ax1.get_xticklabels(), rotation=45, ha="right")
        ax2 = ax1.twinx()
        ax2.plot(df_ns_top["namespace"], df_ns_top["waste_cumsum_pct"], color="crimson", marker="o", linewidth=2)
        ax2.set_ylim(0, 110)
        plt.title("Top 15 Namespace Cost-Waste Pareto Chart (Cumulative Share %)")
        ax1.set_xlabel("Tenant Namespace")
        ax1.set_ylabel("Waste Volume (Core-Hours)")
        plt.tight_layout()
        plt.savefig(out5, dpi=100)
        plt.close()

    print("⏳ [7/19] Rendering chart11_pareto_workload_waste...")
    out11 = PLOT_DIR / "chart11_pareto_workload_waste.png"
    if not check_and_handle_empty(df_pod, out11, "chart11_pareto_workload_waste"):
        df_wl_waste = df_pod.groupby("workload_type")["cpu_waste_core_hours"].sum().reset_index().sort_values("cpu_waste_core_hours", ascending=False)
        plt.figure(figsize=(10, 5))
        sns.barplot(data=df_wl_waste, x="workload_type", y="cpu_waste_core_hours", palette="Oranges_r")
        plt.xticks(rotation=30, ha="right")
        plt.title("Total CPU Waste Volume by Workload Type")
        plt.xlabel("Workload Type")
        plt.ylabel("Total Waste (Core-Hours)")
        plt.tight_layout()
        plt.savefig(out11, dpi=100)
        plt.close()

    # ─── 📊 [Chart 6] Governance Status Donut Chart ───
    print("⏳ [8/19] Rendering chart6_status_donut...")
    out6 = PLOT_DIR / "chart6_status_donut.png"
    if not check_and_handle_empty(df_pod, out6, "chart6_status_donut"):
        status_summary = df_pod["status"].value_counts()
        plt.figure(figsize=(6, 5))
        colors = ["#70AD47", "#1F4E79", "#FFC000", "#C00000"]
        plt.pie(status_summary, labels=status_summary.index, autopct='%1.1f%%', startangle=90, colors=colors[:len(status_summary)], wedgeprops=dict(width=0.4, edgecolor='w'))
        plt.title("Infrastructure Resource Governance Status Ratio")
        plt.tight_layout()
        plt.savefig(out6, dpi=100)
        plt.close()

    # ─── 📊 [Charts 7 & 14] Bubble & Scatter Plots ───
    print("⏳ [9/19] Rendering chart7_waste_footprint_bubble...")
    out7 = PLOT_DIR / "chart7_waste_footprint_bubble.png"
    if not check_and_handle_empty(df_pod, out7, "chart7_waste_footprint_bubble"):
        df_bubble = df_pod.groupby("workload_type").agg(
            x_alloc=("cpu_allocated_core_hours", "sum"),
            y_util=("cpu_util", "mean"),
            z_waste=("cpu_waste_core_hours", "sum")
        ).reset_index()
        plt.figure(figsize=(9, 6))
        sns.scatterplot(data=df_bubble, x="x_alloc", y="y_util", size="z_waste", hue="workload_type", sizes=(100, 2000), alpha=0.7, legend="brief")
        plt.title("Resource Footprint Bubble Chart (Allocated x Utilization x Waste Size)")
        plt.xlabel("Total Allocated Core-Hours")
        plt.ylabel("Average Utilization (%)")
        plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
        plt.tight_layout()
        plt.savefig(out7, dpi=100)
        plt.close()

    print("⏳ [10/19] Rendering chart14_cpu_mem_waste_scatter...")
    out14 = PLOT_DIR / "chart14_cpu_mem_waste_scatter.png"
    if not check_and_handle_empty(df_pod, out14, "chart14_cpu_mem_waste_scatter"):
        plt.figure(figsize=(8, 6))
        sns.scatterplot(data=df_pod.head(5000), x="cpu_waste_core_hours", y="mem_waste_gb_hours", hue="workload_type", alpha=0.5)
        plt.title("Co-relation Scatter Plot: CPU Waste vs Memory Waste (Sampled)")
        plt.xlabel("CPU Waste (Core-Hours)")
        plt.ylabel("Memory Waste (GB-Hours)")
        plt.tight_layout()
        plt.savefig(out14, dpi=100)
        plt.close()

    # ─── 📊 [Chart 8] Resource Shortage Footprint ───
    print("⏳ [11/19] Rendering chart8_shortfall_footprint...")
    out8 = PLOT_DIR / "chart8_shortfall_footprint.png"
    df_short = df_pod.groupby(["workload_type", "date"])["cpu_shortage_cores"].sum().unstack().fillna(0) if not df_pod.empty else pd.DataFrame()
    if not check_and_handle_empty(df_short, out8, "chart8_shortfall_footprint"):
        plt.figure(figsize=(10, 4))
        sns.heatmap(df_short, annot=False, cmap="YlOrRd", cbar=True)
        plt.title("Total CPU Shortfall (Deficit) Cores Footprint")
        plt.xlabel("Date (KST)")
        plt.ylabel("Workload Type")
        plt.tight_layout()
        plt.savefig(out8, dpi=100)
        plt.close()

    # ─── 📊 [Charts 9 & 10] Boxplot Distribution Analysis ───
    print("⏳ [12/19] Rendering chart9_boxplot_cpu_util_by_workload...")
    out9 = PLOT_DIR / "chart9_boxplot_cpu_util_by_workload.png"
    if not check_and_handle_empty(df_pod, out9, "chart9_boxplot_cpu_util_by_workload"):
        plt.figure(figsize=(11, 5))
        sns.boxplot(data=df_pod, x="workload_type", y="cpu_util", palette="Set3")
        plt.xticks(rotation=30, ha="right")
        plt.ylim(-5, 105)
        plt.title("CPU Utilization P95 Boxplot Distribution by Workload Type")
        plt.xlabel("Workload Type")
        plt.ylabel("P95 Actual Utilization (%)")
        plt.tight_layout()
        plt.savefig(out9, dpi=100)
        plt.close()

    print("⏳ [13/19] Rendering chart10_boxplot_mem_util_by_workload...")
    out10 = PLOT_DIR / "chart10_boxplot_mem_util_by_workload.png"
    if not check_and_handle_empty(df_pod, out10, "chart10_boxplot_mem_util_by_workload"):
        plt.figure(figsize=(11, 5))
        sns.boxplot(data=df_pod, x="workload_type", y="mem_util", palette="Pastel1")
        plt.xticks(rotation=30, ha="right")
        plt.ylim(-5, 105)
        plt.title("Memory Utilization P95 Boxplot Distribution by Workload Type")
        plt.xlabel("Workload Type")
        plt.ylabel("P95 Actual Utilization (%)")
        plt.tight_layout()
        plt.savefig(out10, dpi=100)
        plt.close()

    # ─── 📊 [Charts 12 & 15] Trend and Counts ───
    print("⏳ [14/19] Rendering chart12_daily_waste_trend_by_workload...")
    out12 = PLOT_DIR / "chart12_daily_waste_trend_by_workload.png"
    df_trend_wl = df_pod.groupby(["date", "workload_type"])["cpu_waste_core_hours"].sum().unstack().fillna(0) if not df_pod.empty else pd.DataFrame()
    if not check_and_handle_empty(df_trend_wl, out12, "chart12_daily_waste_trend_by_workload"):
        plt.figure(figsize=(11, 5))
        sns.lineplot(data=df_trend_wl, markers=True, dashes=False, linewidth=2)
        plt.title("Daily CPU Waste Timeline Trend Line by Workload Type")
        plt.xlabel("Date (KST)")
        plt.ylabel("Waste Volume (Core-Hours)")
        plt.xticks(rotation=30)
        plt.tight_layout()
        plt.savefig(out12, dpi=100)
        plt.close()

    print("⏳ [15/19] Rendering chart15_oom_status_by_workload...")
    out15 = PLOT_DIR / "chart15_oom_status_by_workload.png"
    if not check_and_handle_empty(df_pod, out15, "chart15_oom_status_by_workload"):
        plt.figure(figsize=(11, 5))
        sns.countplot(data=df_pod, x="workload_type", hue="status", palette="muted")
        plt.xticks(rotation=30, ha="right")
        plt.title("Governance Status Distribution Count per Workload Type")
        plt.xlabel("Workload Type")
        plt.ylabel("Pod Count")
        plt.legend(loc="upper right")
        plt.tight_layout()
        plt.savefig(out15, dpi=100)
        plt.close()

    # ─── 📊 [Charts 13 & 17] Violin and Overcommit Boxplot ───
    print("⏳ [16/19] Rendering chart13_violin_cpu_util...")
    out13 = PLOT_DIR / "chart13_violin_cpu_util.png"
    if not check_and_handle_empty(df_pod, out13, "chart13_violin_cpu_util"):
        plt.figure(figsize=(10, 5))
        sns.violinplot(data=df_pod, x="workload_type", y="cpu_util", inner="quartile", palette="pastel")
        plt.xticks(rotation=30, ha="right")
        plt.title("CPU Utilization Kernel Density Violin Plot")
        plt.xlabel("Workload Type")
        plt.ylabel("CPU Utilization (%)")
        plt.tight_layout()
        plt.savefig(out13, dpi=100)
        plt.close()

    print("⏳ [17/19] Rendering chart17_cpu_limit_request_ratio...")
    out17 = PLOT_DIR / "chart17_cpu_limit_request_ratio.png"
    if not check_and_handle_empty(df_pod, out17, "chart17_cpu_limit_request_ratio"):
        plt.figure(figsize=(10, 5))
        sns.boxplot(data=df_pod, x="workload_type", y="lim_req_ratio", palette="vlag")
        plt.xticks(rotation=30, ha="right")
        plt.title("Kubernetes Pod CPU Limit / Request Overcommit Ratio")
        plt.xlabel("Workload Type")
        plt.ylabel("Limit / Request Ratio")
        plt.tight_layout()
        plt.savefig(out17, dpi=100)
        plt.close()

    # ─── 📊 [Charts 19 & 20] Capacity vs Actual Trends ───
    print("⏳ [18/19] Rendering chart19_daily_cpu_per_workload...")
    out19 = PLOT_DIR / "chart19_daily_cpu_per_workload.png"
    if not check_and_handle_empty(df_pod, out19, "chart19_daily_cpu_per_workload"):
        df_daily_cpu_req = df_pod.groupby("date")["cpu_request_max"].sum()
        df_daily_cpu_use = df_pod.groupby("date")["cpu_usage_p95"].sum()
        plt.figure(figsize=(11, 5))
        plt.fill_between(df_daily_cpu_req.index, df_daily_cpu_req.values, label="Total CPU Request Cores", color="skyblue", alpha=0.4)
        plt.plot(df_daily_cpu_use.index, df_daily_cpu_use.values, label="Total CPU P95 Actual Cores", color="navy", linewidth=2.5, marker="o")
        plt.title("Daily Total CPU Capacity Allocation vs Actual Peak Consumption (KST)")
        plt.xlabel("Date (KST)")
        plt.ylabel("Total CPU Cores")
        plt.xticks(rotation=30)
        plt.legend(loc="upper left")
        plt.tight_layout()
        plt.savefig(out19, dpi=100)
        plt.close()

    print("⏳ [19/19] Rendering chart20_daily_mem_per_workload...")
    out20 = PLOT_DIR / "chart20_daily_mem_per_workload.png"
    if not check_and_handle_empty(df_pod, out20, "chart20_daily_mem_per_workload"):
        df_daily_mem_req = df_pod.groupby("date")["mem_request_max"].sum()
        df_daily_mem_use = df_pod.groupby("date")["mem_usage_p95"].sum()
        plt.figure(figsize=(11, 5))
        plt.fill_between(df_daily_mem_req.index, df_daily_mem_req.values, label="Total Memory Request (GB)", color="plum", alpha=0.4)
        plt.plot(df_daily_mem_use.index, df_daily_mem_use.values, label="Total Memory P95 Actual (GB)", color="purple", linewidth=2.5, marker="o")
        plt.title("Daily Total Memory Capacity Allocation vs Actual Peak Consumption (KST)")
        plt.xlabel("Date (KST)")
        plt.ylabel("Total Memory (GB)")
        plt.xticks(rotation=30)
        plt.legend(loc="upper left")
        plt.tight_layout()
        plt.savefig(out20, dpi=100)
        plt.close()

    print(f"\n🏁 === [Step3 Success] All charts generated cleanly under ./data/output/plots/{cluster_type_str} ===")

if __name__ == "__main__":
    main()

0개의 댓글