REFACTOR(app): remove monitoring page
- Remove monitoring dashboard - Clean up unused features
This commit is contained in:
@@ -13,7 +13,7 @@ commonLabels:
|
||||
# 이미지 태그 설정
|
||||
images:
|
||||
- name: ghcr.io/mayne0213/portfolio
|
||||
newTag: main-sha-a9ded587c52ffcd723797ef76cfbfde8c3641e0c
|
||||
newTag: main-sha-a58ae10db564ef563f9fda8f033526d282c049bd
|
||||
|
||||
patchesStrategicMerge:
|
||||
- deployment-patch.yaml
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import https from 'https';
|
||||
import { URL } from 'url';
|
||||
|
||||
// 클러스터 내부에서 실행되는 경우 클러스터 내부 서비스 URL 사용
|
||||
// 외부에서 실행되는 경우 환경 변수로 설정 가능
|
||||
const ARGOCD_SERVER_URL = process.env.ARGOCD_SERVER_URL || 'https://argocd-server.argocd.svc.cluster.local';
|
||||
const ARGOCD_TOKEN = process.env.ARGOCD_TOKEN || '';
|
||||
const ARGOCD_CA_CERT = process.env.ARGOCD_CA_CERT || '';
|
||||
|
||||
// Node.js 환경에서 커스텀 fetch 함수 생성 (인증서 지원)
|
||||
async function fetchWithCert(url: string, options: RequestInit = {}): Promise<Response> {
|
||||
// 브라우저 환경에서는 기본 fetch 사용
|
||||
if (typeof window !== 'undefined') {
|
||||
return fetch(url, options);
|
||||
}
|
||||
|
||||
// Node.js 환경에서 인증서가 있으면 https 모듈 직접 사용
|
||||
if (ARGOCD_CA_CERT) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const urlObj = new URL(url);
|
||||
const requestOptions: https.RequestOptions = {
|
||||
hostname: urlObj.hostname,
|
||||
port: urlObj.port || 443,
|
||||
path: urlObj.pathname + urlObj.search,
|
||||
method: options.method || 'GET',
|
||||
headers: {
|
||||
...(options.headers as Record<string, string>),
|
||||
},
|
||||
ca: ARGOCD_CA_CERT,
|
||||
rejectUnauthorized: true,
|
||||
};
|
||||
|
||||
const req = https.request(requestOptions, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
res.on('end', () => {
|
||||
const response = new Response(data, {
|
||||
status: res.statusCode || 200,
|
||||
statusText: res.statusMessage || 'OK',
|
||||
headers: res.headers as HeadersInit,
|
||||
});
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
if (options.body) {
|
||||
req.write(typeof options.body === 'string' ? options.body : JSON.stringify(options.body));
|
||||
}
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
return fetch(url, options);
|
||||
}
|
||||
|
||||
interface ArgoCDApplication {
|
||||
metadata: {
|
||||
name: string;
|
||||
namespace: string;
|
||||
};
|
||||
spec: {
|
||||
source: {
|
||||
repoURL: string;
|
||||
path?: string;
|
||||
targetRevision: string;
|
||||
};
|
||||
destination: {
|
||||
server: string;
|
||||
namespace: string;
|
||||
};
|
||||
};
|
||||
status: {
|
||||
health: {
|
||||
status: string;
|
||||
};
|
||||
sync: {
|
||||
status: string;
|
||||
};
|
||||
resources?: Array<{
|
||||
kind: string;
|
||||
name: string;
|
||||
namespace: string;
|
||||
status: string;
|
||||
health?: {
|
||||
status: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
// GET /api/argocd - Get all ArgoCD applications
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
if (!ARGOCD_TOKEN) {
|
||||
return NextResponse.json(
|
||||
{ error: 'ArgoCD token not configured' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// ArgoCD API v1 - Get applications
|
||||
const response = await fetchWithCert(`${ARGOCD_SERVER_URL}/api/v1/applications`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${ARGOCD_TOKEN}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('ArgoCD API error:', response.status, errorText);
|
||||
return NextResponse.json(
|
||||
{ error: `ArgoCD API error: ${response.status}` },
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Read-only로 필터링 (필요한 정보만 반환)
|
||||
const applications = (data.items || []).map((app: ArgoCDApplication) => ({
|
||||
name: app.metadata.name,
|
||||
namespace: app.metadata.namespace,
|
||||
repoURL: app.spec.source.repoURL,
|
||||
path: app.spec.source.path || '',
|
||||
targetRevision: app.spec.source.targetRevision,
|
||||
destination: app.spec.destination,
|
||||
health: app.status.health?.status || 'Unknown',
|
||||
sync: app.status.sync?.status || 'Unknown',
|
||||
resources: app.status.resources?.map((resource) => ({
|
||||
kind: resource.kind,
|
||||
name: resource.name,
|
||||
namespace: resource.namespace,
|
||||
status: resource.status,
|
||||
health: resource.health?.status || 'Unknown',
|
||||
})) || [],
|
||||
}));
|
||||
|
||||
return NextResponse.json({
|
||||
applications,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching ArgoCD applications:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch ArgoCD applications' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,275 +0,0 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import ArgoCDView from '@/components/argocd/ArgoCDView';
|
||||
|
||||
export const metadata = {
|
||||
title: 'ArgoCD - Infrastructure Status',
|
||||
description: 'View ArgoCD applications status (Read-only)',
|
||||
};
|
||||
|
||||
export default function ArgoCDPage() {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8 pt-24 max-w-7xl">
|
||||
<ArgoCDView />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,695 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user