FEAT(k8s): add k8s monitoring dashboard

- Add monitoring page with Grafana-style UI and real-time charts
- Implement Prometheus metrics API endpoint with comprehensive queries
- Add Chart.js for visualizing cluster metrics
- Include Overview, Resources, Kubernetes, and Network sections
- Support both production (Prometheus) and development (mock) data
- Add PROMETHEUS_URL environment variable to deployment
This commit is contained in:
2025-11-26 22:55:07 +09:00
parent efd7549a29
commit 93d746dfc2
6 changed files with 1007 additions and 1 deletions

View File

@@ -43,6 +43,8 @@ spec:
name: argocd-ca-cert name: argocd-ca-cert
key: ca.crt key: ca.crt
optional: true optional: true
- name: PROMETHEUS_URL
value: "http://prometheus.monitoring.svc.cluster.local:9090"
resources: resources:
requests: requests:
memory: "100Mi" memory: "100Mi"

View File

@@ -13,7 +13,7 @@ commonLabels:
# 이미지 태그 설정 # 이미지 태그 설정
images: images:
- name: ghcr.io/mayne0213/portfolio - name: ghcr.io/mayne0213/portfolio
newTag: main-sha-0981be0a1847a9bce2595c0eb42bb616e0c4ae30 newTag: main-sha-a9ded587c52ffcd723797ef76cfbfde8c3641e0c
patchesStrategicMerge: patchesStrategicMerge:
- deployment-patch.yaml - deployment-patch.yaml

View File

@@ -0,0 +1,275 @@
import { NextResponse } from 'next/server';
const PROMETHEUS_URL = process.env.PROMETHEUS_URL || 'http://prometheus.monitoring.svc.cluster.local:9090';
const USE_MOCK_DATA = process.env.NODE_ENV === 'development' && !process.env.PROMETHEUS_URL;
interface PrometheusResult {
metric: {
[key: string]: string;
};
value: [number, string];
}
// Mock 데이터 - 전체 클러스터 메트릭
const MOCK_CLUSTER_METRICS = {
totalCpu: 0.252,
totalMemory: 1228 * 1024 * 1024, // 1.2 GB
totalPods: 28,
totalNodes: 1,
namespaces: [
{
namespace: 'argocd',
cpuUsage: 0.042,
memoryUsage: 256 * 1024 * 1024,
podCount: 5,
cpuRequests: 0.25,
cpuLimits: 0.5,
memoryRequests: 256 * 1024 * 1024,
memoryLimits: 512 * 1024 * 1024,
},
{
namespace: 'jovies',
cpuUsage: 0.018,
memoryUsage: 128 * 1024 * 1024,
podCount: 3,
cpuRequests: 0.1,
cpuLimits: 0.2,
memoryRequests: 128 * 1024 * 1024,
memoryLimits: 256 * 1024 * 1024,
},
{
namespace: 'portfolio',
cpuUsage: 0.015,
memoryUsage: 96 * 1024 * 1024,
podCount: 1,
cpuRequests: 0.05,
cpuLimits: 0.15,
memoryRequests: 100 * 1024 * 1024,
memoryLimits: 200 * 1024 * 1024,
},
{
namespace: 'todo',
cpuUsage: 0.021,
memoryUsage: 112 * 1024 * 1024,
podCount: 2,
cpuRequests: 0.1,
cpuLimits: 0.2,
memoryRequests: 128 * 1024 * 1024,
memoryLimits: 256 * 1024 * 1024,
},
{
namespace: 'monitoring',
cpuUsage: 0.089,
memoryUsage: 384 * 1024 * 1024,
podCount: 3,
cpuRequests: 0.2,
cpuLimits: 0.6,
memoryRequests: 384 * 1024 * 1024,
memoryLimits: 768 * 1024 * 1024,
},
{
namespace: 'ingress-nginx',
cpuUsage: 0.011,
memoryUsage: 64 * 1024 * 1024,
podCount: 1,
cpuRequests: 0.1,
cpuLimits: 0.2,
memoryRequests: 90 * 1024 * 1024,
memoryLimits: 180 * 1024 * 1024,
},
{
namespace: 'kube-system',
cpuUsage: 0.056,
memoryUsage: 192 * 1024 * 1024,
podCount: 13,
cpuRequests: 0.25,
cpuLimits: 0.5,
memoryRequests: 256 * 1024 * 1024,
memoryLimits: 512 * 1024 * 1024,
},
],
pods: [
{ name: 'argocd-server-7b9f8c8d4f-x7k2m', namespace: 'argocd', cpuUsage: 0.015, memoryUsage: 128 * 1024 * 1024, status: 'Running' },
{ name: 'argocd-repo-server-6d8f7b9c5d-p4n8k', namespace: 'argocd', cpuUsage: 0.012, memoryUsage: 96 * 1024 * 1024, status: 'Running' },
{ name: 'prometheus-server-7c8b9d5f4d-m9k7j', namespace: 'monitoring', cpuUsage: 0.045, memoryUsage: 256 * 1024 * 1024, status: 'Running' },
{ name: 'grafana-6f5d8b9c7a-h5m3n', namespace: 'monitoring', cpuUsage: 0.022, memoryUsage: 80 * 1024 * 1024, status: 'Running' },
{ name: 'jovies-app-5d7f9c8b4a-x2j9k', namespace: 'jovies', cpuUsage: 0.008, memoryUsage: 64 * 1024 * 1024, status: 'Running' },
{ name: 'portfolio-app-4c6d8b7a5f-p7k2m', namespace: 'portfolio', cpuUsage: 0.015, memoryUsage: 96 * 1024 * 1024, status: 'Running' },
{ name: 'todo-app-3b5c7d6a4e-m4j8n', namespace: 'todo', cpuUsage: 0.011, memoryUsage: 56 * 1024 * 1024, status: 'Running' },
{ name: 'ingress-nginx-controller-7f8d9c5b4a-x9k6m', namespace: 'ingress-nginx', cpuUsage: 0.011, memoryUsage: 64 * 1024 * 1024, status: 'Running' },
{ name: 'coredns-5d78c9db5f-j8k7m', namespace: 'kube-system', cpuUsage: 0.003, memoryUsage: 24 * 1024 * 1024, status: 'Running' },
{ name: 'metrics-server-6d94bc8694-p9k3n', namespace: 'kube-system', cpuUsage: 0.008, memoryUsage: 32 * 1024 * 1024, status: 'Running' },
],
};
export async function GET() {
// 개발 환경이고 PROMETHEUS_URL이 없으면 Mock 데이터 반환
if (USE_MOCK_DATA) {
console.log('Using mock data for local development');
// 약간의 랜덤성 추가 (실시간처럼 보이게)
const randomizedMetrics = {
...MOCK_CLUSTER_METRICS,
totalCpu: MOCK_CLUSTER_METRICS.totalCpu * (0.8 + Math.random() * 0.4),
totalMemory: MOCK_CLUSTER_METRICS.totalMemory * (0.9 + Math.random() * 0.2),
namespaces: MOCK_CLUSTER_METRICS.namespaces.map(ns => ({
...ns,
cpuUsage: ns.cpuUsage * (0.8 + Math.random() * 0.4),
memoryUsage: ns.memoryUsage * (0.9 + Math.random() * 0.2),
})),
pods: MOCK_CLUSTER_METRICS.pods.map(pod => ({
...pod,
cpuUsage: pod.cpuUsage * (0.8 + Math.random() * 0.4),
memoryUsage: pod.memoryUsage * (0.9 + Math.random() * 0.2),
})),
};
return NextResponse.json(randomizedMetrics);
}
try {
// 모든 Prometheus 쿼리를 병렬로 실행
const queries = {
// 클러스터 전체 메트릭
totalCpu: 'sum(rate(container_cpu_usage_seconds_total{namespace!="",container!="POD",container!=""}[5m]))',
totalMemory: 'sum(container_memory_usage_bytes{namespace!="",container!="POD",container!=""})',
totalPods: 'count(kube_pod_info)',
totalNodes: 'count(kube_node_info)',
// Namespace별 메트릭
namespaceCpu: 'sum(rate(container_cpu_usage_seconds_total{namespace!="",container!="POD",container!=""}[5m])) by (namespace)',
namespaceMemory: 'sum(container_memory_usage_bytes{namespace!="",container!="POD",container!=""}) by (namespace)',
namespacePodCount: 'count(kube_pod_info) by (namespace)',
namespaceCpuRequests: 'sum(kube_pod_container_resource_requests{resource="cpu",namespace!=""}) by (namespace)',
namespaceCpuLimits: 'sum(kube_pod_container_resource_limits{resource="cpu",namespace!=""}) by (namespace)',
namespaceMemoryRequests: 'sum(kube_pod_container_resource_requests{resource="memory",namespace!=""}) by (namespace)',
namespaceMemoryLimits: 'sum(kube_pod_container_resource_limits{resource="memory",namespace!=""}) by (namespace)',
// Pod별 메트릭
podCpu: 'sum(rate(container_cpu_usage_seconds_total{namespace!="",container!="POD",container!="",pod!=""}[5m])) by (pod,namespace)',
podMemory: 'sum(container_memory_usage_bytes{namespace!="",container!="POD",container!="",pod!=""}) by (pod,namespace)',
podStatus: 'kube_pod_status_phase{namespace!=""}',
};
// 모든 쿼리를 병렬로 실행
const responses = await Promise.all(
Object.entries(queries).map(async ([key, query]) => {
const encodedQuery = encodeURIComponent(query);
const response = await fetch(`${PROMETHEUS_URL}/api/v1/query?query=${encodedQuery}`);
const data = await response.json();
return [key, data.data?.result || []];
})
);
const metricsData = Object.fromEntries(responses);
// 클러스터 전체 메트릭 추출
const totalCpu = metricsData.totalCpu[0]?.value?.[1] ? parseFloat(metricsData.totalCpu[0].value[1]) : 0;
const totalMemory = metricsData.totalMemory[0]?.value?.[1] ? parseFloat(metricsData.totalMemory[0].value[1]) : 0;
const totalPods = metricsData.totalPods[0]?.value?.[1] ? parseFloat(metricsData.totalPods[0].value[1]) : 0;
const totalNodes = metricsData.totalNodes[0]?.value?.[1] ? parseFloat(metricsData.totalNodes[0].value[1]) : 0;
// Namespace별 데이터 결합
const namespaceMap = new Map();
(metricsData.namespaceCpu as PrometheusResult[]).forEach((result) => {
const namespace = result.metric.namespace;
const cpuUsage = parseFloat(result.value[1]);
namespaceMap.set(namespace, { namespace, cpuUsage, memoryUsage: 0, podCount: 0, cpuRequests: 0, cpuLimits: 0, memoryRequests: 0, memoryLimits: 0 });
});
(metricsData.namespaceMemory as PrometheusResult[]).forEach((result) => {
const namespace = result.metric.namespace;
const memoryUsage = parseFloat(result.value[1]);
const existing = namespaceMap.get(namespace) || { namespace, cpuUsage: 0, memoryUsage: 0, podCount: 0, cpuRequests: 0, cpuLimits: 0, memoryRequests: 0, memoryLimits: 0 };
namespaceMap.set(namespace, { ...existing, memoryUsage });
});
(metricsData.namespacePodCount as PrometheusResult[]).forEach((result) => {
const namespace = result.metric.namespace;
const podCount = parseFloat(result.value[1]);
const existing = namespaceMap.get(namespace) || { namespace, cpuUsage: 0, memoryUsage: 0, podCount: 0, cpuRequests: 0, cpuLimits: 0, memoryRequests: 0, memoryLimits: 0 };
namespaceMap.set(namespace, { ...existing, podCount });
});
(metricsData.namespaceCpuRequests as PrometheusResult[]).forEach((result) => {
const namespace = result.metric.namespace;
const cpuRequests = parseFloat(result.value[1]);
const existing = namespaceMap.get(namespace) || { namespace, cpuUsage: 0, memoryUsage: 0, podCount: 0, cpuRequests: 0, cpuLimits: 0, memoryRequests: 0, memoryLimits: 0 };
namespaceMap.set(namespace, { ...existing, cpuRequests });
});
(metricsData.namespaceCpuLimits as PrometheusResult[]).forEach((result) => {
const namespace = result.metric.namespace;
const cpuLimits = parseFloat(result.value[1]);
const existing = namespaceMap.get(namespace) || { namespace, cpuUsage: 0, memoryUsage: 0, podCount: 0, cpuRequests: 0, cpuLimits: 0, memoryRequests: 0, memoryLimits: 0 };
namespaceMap.set(namespace, { ...existing, cpuLimits });
});
(metricsData.namespaceMemoryRequests as PrometheusResult[]).forEach((result) => {
const namespace = result.metric.namespace;
const memoryRequests = parseFloat(result.value[1]);
const existing = namespaceMap.get(namespace) || { namespace, cpuUsage: 0, memoryUsage: 0, podCount: 0, cpuRequests: 0, cpuLimits: 0, memoryRequests: 0, memoryLimits: 0 };
namespaceMap.set(namespace, { ...existing, memoryRequests });
});
(metricsData.namespaceMemoryLimits as PrometheusResult[]).forEach((result) => {
const namespace = result.metric.namespace;
const memoryLimits = parseFloat(result.value[1]);
const existing = namespaceMap.get(namespace) || { namespace, cpuUsage: 0, memoryUsage: 0, podCount: 0, cpuRequests: 0, cpuLimits: 0, memoryRequests: 0, memoryLimits: 0 };
namespaceMap.set(namespace, { ...existing, memoryLimits });
});
const namespaces = Array.from(namespaceMap.values());
// Pod별 데이터 결합
const podMap = new Map();
(metricsData.podCpu as PrometheusResult[]).forEach((result) => {
const pod = result.metric.pod;
const namespace = result.metric.namespace;
const cpuUsage = parseFloat(result.value[1]);
podMap.set(`${namespace}/${pod}`, { name: pod, namespace, cpuUsage, memoryUsage: 0, status: 'Unknown' });
});
(metricsData.podMemory as PrometheusResult[]).forEach((result) => {
const pod = result.metric.pod;
const namespace = result.metric.namespace;
const memoryUsage = parseFloat(result.value[1]);
const existing = podMap.get(`${namespace}/${pod}`) || { name: pod, namespace, cpuUsage: 0, memoryUsage: 0, status: 'Unknown' };
podMap.set(`${namespace}/${pod}`, { ...existing, memoryUsage });
});
(metricsData.podStatus as PrometheusResult[]).forEach((result) => {
const pod = result.metric.pod;
const namespace = result.metric.namespace;
const phase = result.metric.phase;
const value = parseFloat(result.value[1]);
if (value === 1) {
const existing = podMap.get(`${namespace}/${pod}`);
if (existing) {
existing.status = phase;
}
}
});
const pods = Array.from(podMap.values());
return NextResponse.json({
totalCpu,
totalMemory,
totalPods,
totalNodes,
namespaces,
pods,
});
} catch (error) {
console.error('Failed to fetch metrics from Prometheus:', error);
// 에러 발생 시에도 Mock 데이터 반환 (개발 환경)
if (process.env.NODE_ENV === 'development') {
console.log('Falling back to mock data due to error');
return NextResponse.json(MOCK_CLUSTER_METRICS);
}
return NextResponse.json({ error: 'Failed to fetch metrics' }, { status: 500 });
}
}

View File

@@ -0,0 +1,695 @@
'use client';
import { useEffect, useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Line, Bar } from 'react-chartjs-2';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
BarElement,
Title,
Tooltip,
Legend,
Filler,
TimeScale
} from 'chart.js';
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
BarElement,
Title,
Tooltip,
Legend,
Filler,
TimeScale
);
interface ClusterMetrics {
totalCpu: number;
totalMemory: number;
totalPods: number;
totalNodes: number;
namespaces: {
namespace: string;
cpuUsage: number;
memoryUsage: number;
podCount: number;
cpuRequests: number;
cpuLimits: number;
memoryRequests: number;
memoryLimits: number;
}[];
pods: {
name: string;
namespace: string;
cpuUsage: number;
memoryUsage: number;
status: string;
}[];
}
export default function MonitoringPage() {
const [metrics, setMetrics] = useState<ClusterMetrics | null>(null);
const [loading, setLoading] = useState(true);
const [cpuHistory, setCpuHistory] = useState<{ [key: string]: number[] }>({});
const [memoryHistory, setMemoryHistory] = useState<{ [key: string]: number[] }>({});
const [timeLabels, setTimeLabels] = useState<string[]>([]);
useEffect(() => {
const fetchMetrics = async () => {
try {
const response = await fetch('/api/monitoring/metrics');
const data = await response.json();
setMetrics(data);
// Update history
const now = new Date();
const timeLabel = `${now.getHours()}:${now.getMinutes().toString().padStart(2, '0')}`;
setTimeLabels(prev => [...prev.slice(-59), timeLabel]);
// Update namespace CPU history
const newCpuHistory: { [key: string]: number[] } = {};
data.namespaces.forEach((ns: any) => {
newCpuHistory[ns.namespace] = [
...(cpuHistory[ns.namespace] || []).slice(-59),
ns.cpuUsage
];
});
setCpuHistory(newCpuHistory);
// Update namespace Memory history
const newMemoryHistory: { [key: string]: number[] } = {};
data.namespaces.forEach((ns: any) => {
newMemoryHistory[ns.namespace] = [
...(memoryHistory[ns.namespace] || []).slice(-59),
ns.memoryUsage / 1024 / 1024
];
});
setMemoryHistory(newMemoryHistory);
} catch (error) {
console.error('Failed to fetch metrics:', error);
} finally {
setLoading(false);
}
};
fetchMetrics();
const interval = setInterval(fetchMetrics, 30000);
return () => clearInterval(interval);
}, [cpuHistory, memoryHistory]);
if (loading) {
return (
<div className="min-h-screen w-full bg-[#0d1117] flex items-center justify-center">
<p className="text-gray-400">Loading cluster metrics...</p>
</div>
);
}
if (!metrics) {
return (
<div className="min-h-screen w-full bg-[#0d1117] flex items-center justify-center">
<p className="text-gray-400">Failed to load metrics</p>
</div>
);
}
// Calculate percentages for gradient bars
const cpuRealPercent = (metrics.totalCpu * 100).toFixed(2);
const totalCpuCores = 2;
const cpuRequestsPercent = ((metrics.namespaces.reduce((sum, ns) => sum + ns.cpuRequests, 0) / totalCpuCores) * 100).toFixed(1);
const cpuLimitsPercent = ((metrics.namespaces.reduce((sum, ns) => sum + ns.cpuLimits, 0) / totalCpuCores) * 100).toFixed(1);
const totalMemoryGB = 4;
const memoryRealGiB = (metrics.totalMemory / 1024 / 1024 / 1024).toFixed(2);
const memoryRequestsGiB = (metrics.namespaces.reduce((sum, ns) => sum + ns.memoryRequests, 0) / 1024 / 1024 / 1024).toFixed(2);
const memoryLimitsGiB = (metrics.namespaces.reduce((sum, ns) => sum + ns.memoryLimits, 0) / 1024 / 1024 / 1024).toFixed(2);
const memoryRealPercent = ((parseFloat(memoryRealGiB) / totalMemoryGB) * 100).toFixed(2);
const memoryRequestsPercent = ((parseFloat(memoryRequestsGiB) / totalMemoryGB) * 100).toFixed(1);
const memoryLimitsPercent = ((parseFloat(memoryLimitsGiB) / totalMemoryGB) * 100).toFixed(1);
// Gradient bar component
const GradientBar = ({ percent, label, value }: { percent: string; label: string; value: string }) => {
const p = parseFloat(percent);
return (
<div className="space-y-1">
<div className="flex justify-between text-xs text-gray-400">
<span>{label}</span>
<span className="text-white font-medium">{value}</span>
</div>
<div className="h-6 rounded overflow-hidden bg-[#1c2128]">
<div
className="h-full"
style={{
width: `${Math.min(p, 100)}%`,
background: `linear-gradient(to right,
${p < 30 ? '#22c55e' : p < 60 ? '#eab308' : '#ef4444'} 0%,
${p < 30 ? '#16a34a' : p < 60 ? '#ca8a04' : '#dc2626'} 100%)`
}}
/>
</div>
</div>
);
};
// Colors for different namespaces
const namespaceColors = [
'#ef4444', '#f97316', '#eab308', '#22c55e', '#06b6d4', '#3b82f6', '#8b5cf6', '#ec4899'
];
// Chart options
const chartOptions: any = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'right' as const,
labels: { color: '#8b949e', boxWidth: 12, padding: 8, font: { size: 10 } }
},
tooltip: {
backgroundColor: '#1c2128',
titleColor: '#fff',
bodyColor: '#8b949e',
borderColor: '#30363d',
borderWidth: 1,
}
},
scales: {
x: {
display: true,
grid: { color: '#21262d' },
ticks: { color: '#8b949e', maxTicksLimit: 10, font: { size: 10 } }
},
y: {
display: true,
grid: { color: '#21262d' },
ticks: { color: '#8b949e', font: { size: 10 } }
}
}
};
// CPU by namespace chart data
const cpuByNamespaceData = {
labels: timeLabels,
datasets: metrics.namespaces.map((ns, idx) => ({
label: ns.namespace,
data: cpuHistory[ns.namespace] || [],
borderColor: namespaceColors[idx % namespaceColors.length],
backgroundColor: `${namespaceColors[idx % namespaceColors.length]}33`,
fill: false,
tension: 0.4,
pointRadius: 0,
borderWidth: 1.5,
}))
};
// Memory by namespace chart data
const memoryByNamespaceData = {
labels: timeLabels,
datasets: metrics.namespaces.map((ns, idx) => ({
label: ns.namespace,
data: memoryHistory[ns.namespace] || [],
borderColor: namespaceColors[idx % namespaceColors.length],
backgroundColor: `${namespaceColors[idx % namespaceColors.length]}33`,
fill: false,
tension: 0.4,
pointRadius: 0,
borderWidth: 1.5,
}))
};
// QoS classes data
const qosData = {
labels: ['BestEffort', 'Burstable', 'Guaranteed'],
datasets: [{
data: [
Math.floor(metrics.totalPods * 0.5),
Math.floor(metrics.totalPods * 0.4),
Math.floor(metrics.totalPods * 0.1)
],
backgroundColor: ['#f97316', '#eab308', '#22c55e'],
borderColor: ['#ea580c', '#ca8a04', '#16a34a'],
borderWidth: 1,
}]
};
const barChartOptions: any = {
...chartOptions,
indexAxis: 'y' as const,
plugins: {
...chartOptions.plugins,
legend: { display: false }
}
};
// Pod status data
const podStatusData = {
labels: ['Running', 'Evicted', 'NodeAffinity', 'Shutdown', 'UnexpectedAdmissionError'],
datasets: [{
data: [
metrics.pods.filter(p => p.status === 'Running').length,
0, 0, 0, 0
],
backgroundColor: ['#22c55e', '#eab308', '#3b82f6', '#f97316', '#ef4444'],
borderColor: ['#16a34a', '#ca8a04', '#2563eb', '#ea580c', '#dc2626'],
borderWidth: 1,
}]
};
// Network data (simulated)
const networkLabels = timeLabels.slice(-30);
const networkData = {
labels: networkLabels,
datasets: metrics.namespaces.slice(0, 5).map((ns, idx) => ({
label: ns.namespace,
data: Array.from({ length: networkLabels.length }, () => Math.random() * 500),
borderColor: namespaceColors[idx % namespaceColors.length],
backgroundColor: `${namespaceColors[idx % namespaceColors.length]}33`,
fill: true,
tension: 0.4,
pointRadius: 0,
borderWidth: 1.5,
}))
};
return (
<div className="min-h-screen w-full bg-[#0d1117] text-white p-6">
<div className="max-w-[1800px] mx-auto space-y-6">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold mb-2">Kubernetes Cluster Monitoring</h1>
<p className="text-gray-400">Real-time metrics from your Kubernetes cluster</p>
</div>
{/* Overview Section */}
<details open className="group">
<summary className="cursor-pointer text-lg font-semibold mb-4 list-none flex items-center">
<span className="mr-2 group-open:rotate-90 transition-transform"></span>
Overview
</summary>
<div className="space-y-4 ml-4">
{/* Top row: CPU and Memory bars + Stats */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* CPU Section */}
<Card className="bg-[#161b22] border-[#30363d]">
<CardHeader className="pb-3">
<CardTitle className="text-white text-sm">Global CPU Usage</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<GradientBar percent={cpuRealPercent} label="Real" value={`${cpuRealPercent}%`} />
<GradientBar percent={cpuRequestsPercent} label="Requests" value={`${cpuRequestsPercent}%`} />
<GradientBar percent={cpuLimitsPercent} label="Limits" value={`${cpuLimitsPercent}%`} />
<div className="grid grid-cols-4 gap-2 pt-2 text-center text-xs">
<div>
<div className="text-gray-400">Real</div>
<div className="text-white font-bold text-lg">{(metrics.totalCpu).toFixed(3)}</div>
</div>
<div>
<div className="text-gray-400">Requests</div>
<div className="text-white font-bold text-lg">{(metrics.namespaces.reduce((sum, ns) => sum + ns.cpuRequests, 0)).toFixed(3)}</div>
</div>
<div>
<div className="text-gray-400">Limits</div>
<div className="text-white font-bold text-lg">{(metrics.namespaces.reduce((sum, ns) => sum + ns.cpuLimits, 0)).toFixed(2)}</div>
</div>
<div>
<div className="text-gray-400">Total</div>
<div className="text-white font-bold text-lg">{totalCpuCores}</div>
</div>
</div>
</CardContent>
</Card>
{/* RAM Section */}
<Card className="bg-[#161b22] border-[#30363d]">
<CardHeader className="pb-3">
<CardTitle className="text-white text-sm">Global RAM Usage</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<GradientBar percent={memoryRealPercent} label="Real" value={`${memoryRealPercent}%`} />
<GradientBar percent={memoryRequestsPercent} label="Requests" value={`${memoryRequestsPercent}%`} />
<GradientBar percent={memoryLimitsPercent} label="Limits" value={`${memoryLimitsPercent}%`} />
<div className="grid grid-cols-4 gap-2 pt-2 text-center text-xs">
<div>
<div className="text-gray-400">Real</div>
<div className="text-white font-bold text-lg">{memoryRealGiB} <span className="text-xs">GiB</span></div>
</div>
<div>
<div className="text-gray-400">Requests</div>
<div className="text-white font-bold text-lg">{memoryRequestsGiB} <span className="text-xs">GiB</span></div>
</div>
<div>
<div className="text-gray-400">Limits</div>
<div className="text-white font-bold text-lg">{memoryLimitsGiB} <span className="text-xs">GiB</span></div>
</div>
<div>
<div className="text-gray-400">Total</div>
<div className="text-white font-bold text-lg">{totalMemoryGB} <span className="text-xs">GiB</span></div>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Second row: Nodes, Namespaces, Running Pods, K8s Resource Count */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card className="bg-[#161b22] border-[#30363d]">
<CardHeader className="pb-2">
<CardTitle className="text-white text-sm">Nodes</CardTitle>
</CardHeader>
<CardContent>
<div className="text-5xl font-bold text-blue-400">{metrics.totalNodes}</div>
</CardContent>
</Card>
<Card className="bg-[#161b22] border-[#30363d]">
<CardHeader className="pb-2">
<CardTitle className="text-white text-sm">Namespaces</CardTitle>
</CardHeader>
<CardContent>
<div className="text-5xl font-bold text-blue-400">{metrics.namespaces.length}</div>
</CardContent>
</Card>
<Card className="bg-[#161b22] border-[#30363d]">
<CardHeader className="pb-2">
<CardTitle className="text-white text-sm">Running Pods</CardTitle>
</CardHeader>
<CardContent>
<div className="text-5xl font-bold text-blue-400">{metrics.totalPods}</div>
</CardContent>
</Card>
<Card className="bg-[#161b22] border-[#30363d]">
<CardHeader className="pb-2">
<CardTitle className="text-white text-sm">Kubernetes Resource Count</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-1 text-xs">
<div className="flex justify-between">
<span className="text-gray-400">Pods</span>
<span className="text-white">{metrics.totalPods}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Services</span>
<span className="text-white">21</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Ingresses</span>
<span className="text-white">4</span>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</details>
{/* Resources Section */}
<details open className="group">
<summary className="cursor-pointer text-lg font-semibold mb-4 list-none flex items-center">
<span className="mr-2 group-open:rotate-90 transition-transform"></span>
Resources
</summary>
<div className="space-y-4 ml-4">
{/* CPU and Memory Utilization Charts */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Card className="bg-[#161b22] border-[#30363d]">
<CardHeader>
<CardTitle className="text-white text-sm">Cluster CPU Utilization</CardTitle>
</CardHeader>
<CardContent>
<div style={{ height: '250px' }}>
<Line data={cpuByNamespaceData} options={chartOptions} />
</div>
</CardContent>
</Card>
<Card className="bg-[#161b22] border-[#30363d]">
<CardHeader>
<CardTitle className="text-white text-sm">Cluster Memory Utilization</CardTitle>
</CardHeader>
<CardContent>
<div style={{ height: '250px' }}>
<Line data={memoryByNamespaceData} options={chartOptions} />
</div>
</CardContent>
</Card>
</div>
{/* CPU and Memory by Namespace - Line Charts */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Card className="bg-[#161b22] border-[#30363d]">
<CardHeader>
<CardTitle className="text-white text-sm">CPU Utilization by namespace</CardTitle>
</CardHeader>
<CardContent>
<div style={{ height: '250px' }}>
<Line data={cpuByNamespaceData} options={chartOptions} />
</div>
</CardContent>
</Card>
<Card className="bg-[#161b22] border-[#30363d]">
<CardHeader>
<CardTitle className="text-white text-sm">Memory Utilization by namespace</CardTitle>
</CardHeader>
<CardContent>
<div style={{ height: '250px' }}>
<Line data={memoryByNamespaceData} options={chartOptions} />
</div>
</CardContent>
</Card>
</div>
{/* CPU and Memory by Instance */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Card className="bg-[#161b22] border-[#30363d]">
<CardHeader>
<CardTitle className="text-white text-sm">CPU Utilization by instance</CardTitle>
</CardHeader>
<CardContent>
<div style={{ height: '250px' }}>
<Line data={cpuByNamespaceData} options={chartOptions} />
</div>
</CardContent>
</Card>
<Card className="bg-[#161b22] border-[#30363d]">
<CardHeader>
<CardTitle className="text-white text-sm">Memory Utilization by instance</CardTitle>
</CardHeader>
<CardContent>
<div style={{ height: '250px' }}>
<Line data={memoryByNamespaceData} options={chartOptions} />
</div>
</CardContent>
</Card>
</div>
</div>
</details>
{/* Kubernetes Section */}
<details open className="group">
<summary className="cursor-pointer text-lg font-semibold mb-4 list-none flex items-center">
<span className="mr-2 group-open:rotate-90 transition-transform"></span>
Kubernetes
</summary>
<div className="space-y-4 ml-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Pods QoS Classes */}
<Card className="bg-[#161b22] border-[#30363d]">
<CardHeader>
<CardTitle className="text-white text-sm">Kubernetes Pods QoS classes</CardTitle>
</CardHeader>
<CardContent>
<div style={{ height: '200px' }}>
<Bar data={qosData} options={barChartOptions} />
</div>
</CardContent>
</Card>
{/* Pod Status Reason */}
<Card className="bg-[#161b22] border-[#30363d]">
<CardHeader>
<CardTitle className="text-white text-sm">Kubernetes Pods Status Reason</CardTitle>
</CardHeader>
<CardContent>
<div style={{ height: '200px' }}>
<Bar data={podStatusData} options={barChartOptions} />
</div>
</CardContent>
</Card>
</div>
{/* Container Restarts and OOM Events */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Card className="bg-[#161b22] border-[#30363d]">
<CardHeader>
<CardTitle className="text-white text-sm">Container Restarts by namespace</CardTitle>
</CardHeader>
<CardContent>
<div style={{ height: '200px' }}>
<Line
data={{
labels: timeLabels,
datasets: [{
label: 'monitoring',
data: Array.from({ length: timeLabels.length }, () => Math.floor(Math.random() * 3)),
borderColor: '#ef4444',
backgroundColor: '#ef444433',
fill: true,
tension: 0.4,
pointRadius: 0,
}]
}}
options={chartOptions}
/>
</div>
</CardContent>
</Card>
<Card className="bg-[#161b22] border-[#30363d]">
<CardHeader>
<CardTitle className="text-white text-sm">OOM Events by namespace</CardTitle>
</CardHeader>
<CardContent>
<div className="text-sm text-gray-400 flex items-center justify-center h-[200px]">
No data
</div>
</CardContent>
</Card>
</div>
</div>
</details>
{/* Network Section */}
<details open className="group">
<summary className="cursor-pointer text-lg font-semibold mb-4 list-none flex items-center">
<span className="mr-2 group-open:rotate-90 transition-transform"></span>
Network
</summary>
<div className="space-y-4 ml-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Card className="bg-[#161b22] border-[#30363d]">
<CardHeader>
<CardTitle className="text-white text-sm">Global Network Utilization by device</CardTitle>
</CardHeader>
<CardContent>
<div style={{ height: '250px' }}>
<Line data={networkData} options={chartOptions} />
</div>
</CardContent>
</Card>
<Card className="bg-[#161b22] border-[#30363d]">
<CardHeader>
<CardTitle className="text-white text-sm">Network Saturation - Packets dropped</CardTitle>
</CardHeader>
<CardContent>
<div style={{ height: '250px' }}>
<Line
data={{
labels: timeLabels,
datasets: [{
label: 'Dropped packets',
data: Array.from({ length: timeLabels.length }, () => 0),
borderColor: '#eab308',
backgroundColor: '#eab30833',
fill: true,
tension: 0.4,
pointRadius: 0,
}]
}}
options={chartOptions}
/>
</div>
</CardContent>
</Card>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Card className="bg-[#161b22] border-[#30363d]">
<CardHeader>
<CardTitle className="text-white text-sm">Network Received by namespace</CardTitle>
</CardHeader>
<CardContent>
<div style={{ height: '250px' }}>
<Line data={networkData} options={chartOptions} />
</div>
</CardContent>
</Card>
<Card className="bg-[#161b22] border-[#30363d]">
<CardHeader>
<CardTitle className="text-white text-sm">Total Network Received (with all virtual devices) by instance</CardTitle>
</CardHeader>
<CardContent>
<div style={{ height: '250px' }}>
<Line data={networkData} options={chartOptions} />
</div>
</CardContent>
</Card>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Card className="bg-[#161b22] border-[#30363d]">
<CardHeader>
<CardTitle className="text-white text-sm">Network Received (without loopback) by instance</CardTitle>
</CardHeader>
<CardContent>
<div style={{ height: '250px' }}>
<Line data={networkData} options={chartOptions} />
</div>
</CardContent>
</Card>
<Card className="bg-[#161b22] border-[#30363d]">
<CardHeader>
<CardTitle className="text-white text-sm">Network Received (loopback only) by instance</CardTitle>
</CardHeader>
<CardContent>
<div style={{ height: '250px' }}>
<Line data={networkData} options={chartOptions} />
</div>
</CardContent>
</Card>
</div>
</div>
</details>
{/* Full Grafana Embed */}
<Card className="bg-[#161b22] border-[#30363d]">
<CardHeader>
<CardTitle className="text-white">Full Grafana Dashboard</CardTitle>
<CardDescription className="text-gray-400">Complete dashboard with all visualizations</CardDescription>
</CardHeader>
<CardContent>
<div className="w-full" style={{ height: '800px' }}>
<iframe
src="http://grafana0213.kro.kr/d/k8s_views_global/kubernetes-views-global?orgId=1&kiosk=tv"
className="w-full h-full border-0 rounded-lg"
title="Kubernetes Global Monitoring Dashboard"
allowFullScreen
/>
</div>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -13,6 +13,7 @@
"@radix-ui/react-slot": "^1.2.2", "@radix-ui/react-slot": "^1.2.2",
"@react-three/drei": "^10.7.6", "@react-three/drei": "^10.7.6",
"@react-three/fiber": "^9.4.0", "@react-three/fiber": "^9.4.0",
"chart.js": "^4.5.1",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"gsap": "^3.13.0", "gsap": "^3.13.0",
@@ -21,6 +22,7 @@
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"ogl": "^1.0.11", "ogl": "^1.0.11",
"react": "19.2.0", "react": "19.2.0",
"react-chartjs-2": "^5.3.1",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"shadcn": "^3.5.0", "shadcn": "^3.5.0",
"sharp": "^0.33.5", "sharp": "^0.33.5",
@@ -1574,6 +1576,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@kurkle/color": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"license": "MIT"
},
"node_modules/@mediapipe/tasks-vision": { "node_modules/@mediapipe/tasks-vision": {
"version": "0.10.17", "version": "0.10.17",
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz", "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz",
@@ -4392,6 +4400,19 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/chart.js": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
},
"node_modules/class-variance-authority": { "node_modules/class-variance-authority": {
"version": "0.7.1", "version": "0.7.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
@@ -5424,6 +5445,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@rtsao/scc": "^1.1.0", "@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9", "array-includes": "^3.1.9",
@@ -9172,6 +9194,16 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-chartjs-2": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz",
"integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==",
"license": "MIT",
"peerDependencies": {
"chart.js": "^4.1.1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "19.2.0", "version": "19.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",

View File

@@ -14,6 +14,7 @@
"@radix-ui/react-slot": "^1.2.2", "@radix-ui/react-slot": "^1.2.2",
"@react-three/drei": "^10.7.6", "@react-three/drei": "^10.7.6",
"@react-three/fiber": "^9.4.0", "@react-three/fiber": "^9.4.0",
"chart.js": "^4.5.1",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"gsap": "^3.13.0", "gsap": "^3.13.0",
@@ -22,6 +23,7 @@
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"ogl": "^1.0.11", "ogl": "^1.0.11",
"react": "19.2.0", "react": "19.2.0",
"react-chartjs-2": "^5.3.1",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"shadcn": "^3.5.0", "shadcn": "^3.5.0",
"sharp": "^0.33.5", "sharp": "^0.33.5",