feat: date range picker, split network into Ingress/Istio, fix NodeTable undefined instance
CI/CD / build-and-push (push) Successful in 1m29s
Details
CI/CD / build-and-push (push) Successful in 1m29s
Details
This commit is contained in:
parent
7a0b1841f9
commit
82635e0335
|
|
@ -35,8 +35,10 @@ config:
|
||||||
QUERY_MEMORY: '(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100'
|
QUERY_MEMORY: '(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100'
|
||||||
QUERY_DISK: 'sum by (mountpoint,fstype)((1 - node_filesystem_avail_bytes{fstype!~"tmpfs|overlay|nfs",mountpoint!~"/boot.*"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay|nfs",mountpoint!~"/boot.*"}) * 100)'
|
QUERY_DISK: 'sum by (mountpoint,fstype)((1 - node_filesystem_avail_bytes{fstype!~"tmpfs|overlay|nfs",mountpoint!~"/boot.*"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay|nfs",mountpoint!~"/boot.*"}) * 100)'
|
||||||
QUERY_NAS: '((node_filesystem_size_bytes{fstype=~"nfs", node=~".*(bastion).*"} - node_filesystem_free_bytes{fstype=~"nfs", node=~".*(bastion).*"}) / node_filesystem_size_bytes{fstype=~"nfs", node=~".*(bastion).*"}) * 100'
|
QUERY_NAS: '((node_filesystem_size_bytes{fstype=~"nfs", node=~".*(bastion).*"} - node_filesystem_free_bytes{fstype=~"nfs", node=~".*(bastion).*"}) / node_filesystem_size_bytes{fstype=~"nfs", node=~".*(bastion).*"}) * 100'
|
||||||
QUERY_NET_RX: 'sum(rate(container_network_receive_bytes_total{namespace="ingress-nginx", pod=~"ingress-nginx-controller-.*"}[5m]))'
|
QUERY_INGRESS_RX: 'sum(rate(container_network_receive_bytes_total{namespace="ingress-nginx", pod=~"ingress-nginx-controller-.*"}[5m]))'
|
||||||
QUERY_NET_TX: 'sum(rate(container_network_transmit_bytes_total{namespace="ingress-nginx", pod=~"ingress-nginx-controller-.*"}[5m]))'
|
QUERY_INGRESS_TX: 'sum(rate(container_network_transmit_bytes_total{namespace="ingress-nginx", pod=~"ingress-nginx-controller-.*"}[5m]))'
|
||||||
|
QUERY_ISTIO_RX: 'sum(rate(istio_request_bytes_sum{reporter="destination"}[5m]))'
|
||||||
|
QUERY_ISTIO_TX: 'sum(rate(istio_response_bytes_sum{reporter="destination"}[5m]))'
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
|
|
|
||||||
2
plan.md
2
plan.md
|
|
@ -1776,7 +1776,7 @@ export default function Home() {
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## STEP 11. 빌드 및 배포
|
## STEP 11. 빌드 및 배포 ✅
|
||||||
|
|
||||||
### 11-1. 로컬 개발
|
### 11-1. 로컬 개발
|
||||||
|
|
||||||
|
|
|
||||||
24
record.md
24
record.md
|
|
@ -1,3 +1,27 @@
|
||||||
# 구현 기록 (plan.md 대비 변경사항)
|
# 구현 기록 (plan.md 대비 변경사항)
|
||||||
|
|
||||||
## 변경 이력
|
## 변경 이력
|
||||||
|
|
||||||
|
### 2026-03-16
|
||||||
|
|
||||||
|
1. **Dockerfile 수정**: `COPY --from=builder /app/public ./public`에서 public 디렉토리가 존재하지 않아 빌드 실패. builder 단계에 `RUN mkdir -p public` 추가로 해결.
|
||||||
|
|
||||||
|
2. **next.config.js 수정**: `serverExternalPackages`는 Next.js 15+ 옵션. Next.js 14에서는 `experimental.serverComponentsExternalPackages`로 변경.
|
||||||
|
|
||||||
|
3. **ArgoCD 연동 방식 변경**: argocd CLI가 bastion에 미설치되어 있어, kubectl로 직접 Secret(repo 인증) + Application 리소스를 생성하는 방식으로 대체.
|
||||||
|
|
||||||
|
4. **API Route params 수정**: Next.js 14 App Router에서 동적 route의 `params`를 `Promise<{ type: string }>`으로 처리 (async params pattern).
|
||||||
|
|
||||||
|
5. **CI Harbor 로그인 실패 수정**: 비밀번호 `Ijinc123!@#$`의 특수문자(`!`, `$`)가 큰따옴표 안에서 shell에 의해 해석되는 문제. `echo "${{ env.REGISTRY_PASSWORD }}"` → `echo '${{ secrets.REGISTRY_PASSWORD }}'` (작은따옴표 + secrets 직접 참조)로 변경.
|
||||||
|
|
||||||
|
6. **CI env 블록 구조 변경**: workflow 레벨 `env`에서 secrets를 매핑하던 방식을 제거하고, 각 step에서 `secrets.XXX`를 직접 참조하도록 변경.
|
||||||
|
|
||||||
|
7. **SSR 클라이언트 에러 수정**: `echarts-for-react`, `html2canvas`, `jspdf`가 SSR 비호환 라이브러리라 클라이언트 에러 발생. `dynamic(() => import(...), { ssr: false })`로 변경하고, `html2canvas`/`jspdf`는 함수 내부에서 동적 import하도록 수정.
|
||||||
|
|
||||||
|
8. **Helm chart 버전 전략 변경**: plan.md에서는 고정 버전 `0.1.0`이었으나, ArgoCD가 동일 버전에서 변경을 감지하지 못하는 문제 발생. semver pre-release(`0.1.0-sha`)도 정식 버전보다 낮게 취급되어 `>0.0.0` range에서 감지 안 됨. 최종적으로 타임스탬프 기반 patch 버전(`0.1.YYYYMMDDHHMMSS`)으로 변경하여 매 빌드마다 고유한 정식 semver 생성.
|
||||||
|
|
||||||
|
9. **ArgoCD targetRevision 변경**: `0.1.0` 고정 → `>0.0.0` semver range로 변경하여 새 chart 버전 자동 감지. ArgoCD 기본 polling interval(~3분) 내에 자동 sync됨.
|
||||||
|
|
||||||
|
10. **Deployment 이미지 태그 변경**: `image.tag` values 고정 대신 `{{ .Values.image.tag | default .Chart.AppVersion }}`으로 변경. ArgoCD helm values에서 `image.tag` override 제거하여 chart의 `appVersion`(CI에서 `sha-xxxx`로 설정)을 자동 사용.
|
||||||
|
|
||||||
|
11. **.gitignore 추가**: plan.md에 없었으나 `.env`, `node_modules/`, `.next/` 등을 git에서 제외하기 위해 추가.
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,16 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { queryRange, QUERIES } from '@/lib/prometheus';
|
import { queryRange, QUERIES } from '@/lib/prometheus';
|
||||||
import { calculateStats, detectPeaks, DataPoint } from '@/lib/analytics';
|
import { calculateStats, detectPeaks, DataPoint } from '@/lib/analytics';
|
||||||
|
|
||||||
const QUERY_MAP: Record<string, string | string[]> = {
|
const QUERY_MAP: Record<string, string> = {
|
||||||
cpu: QUERIES.cpuUsage,
|
cpu: QUERIES.cpuUsage,
|
||||||
memory: QUERIES.memoryUsage,
|
memory: QUERIES.memoryUsage,
|
||||||
disk: QUERIES.diskUsage,
|
disk: QUERIES.diskUsage,
|
||||||
nas: QUERIES.nasUsage,
|
nas: QUERIES.nasUsage,
|
||||||
network: [QUERIES.networkReceive, QUERIES.networkTransmit],
|
};
|
||||||
|
|
||||||
|
const NETWORK_MAP: Record<string, { rx: string; tx: string }> = {
|
||||||
|
ingress: { rx: QUERIES.ingressReceive, tx: QUERIES.ingressTransmit },
|
||||||
|
istio: { rx: QUERIES.istioReceive, tx: QUERIES.istioTransmit },
|
||||||
};
|
};
|
||||||
|
|
||||||
function parsePrometheusValues(values: [number, string][]): DataPoint[] {
|
function parsePrometheusValues(values: [number, string][]): DataPoint[] {
|
||||||
|
|
@ -26,46 +30,37 @@ export async function GET(
|
||||||
const end = new Date();
|
const end = new Date();
|
||||||
const start = new Date(end.getTime() - days * 24 * 60 * 60 * 1000);
|
const start = new Date(end.getTime() - days * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
const queries = QUERY_MAP[type];
|
|
||||||
if (!queries) {
|
|
||||||
return NextResponse.json({ error: `Unknown metric type: ${type}` }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (type === 'network') {
|
const netQuery = NETWORK_MAP[type];
|
||||||
|
if (netQuery) {
|
||||||
const [rxResults, txResults] = await Promise.all([
|
const [rxResults, txResults] = await Promise.all([
|
||||||
queryRange(QUERIES.networkReceive, start, end, step),
|
queryRange(netQuery.rx, start, end, step),
|
||||||
queryRange(QUERIES.networkTransmit, start, end, step),
|
queryRange(netQuery.tx, start, end, step),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const receive = rxResults.map((r) => {
|
const process = (results: typeof rxResults) => results.map((r) => {
|
||||||
const dataPoints = parsePrometheusValues(r.values);
|
const dataPoints = parsePrometheusValues(r.values);
|
||||||
return {
|
return {
|
||||||
instance: r.metric.instance,
|
instance: r.metric.instance || '',
|
||||||
dataPoints,
|
dataPoints,
|
||||||
stats: calculateStats(dataPoints),
|
stats: calculateStats(dataPoints),
|
||||||
peaks: detectPeaks(dataPoints),
|
peaks: detectPeaks(dataPoints),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const transmit = txResults.map((r) => {
|
return NextResponse.json({ type, period: { start, end, step }, receive: process(rxResults), transmit: process(txResults) });
|
||||||
const dataPoints = parsePrometheusValues(r.values);
|
|
||||||
return {
|
|
||||||
instance: r.metric.instance,
|
|
||||||
dataPoints,
|
|
||||||
stats: calculateStats(dataPoints),
|
|
||||||
peaks: detectPeaks(dataPoints),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json({ type, period: { start, end, step }, receive, transmit });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await queryRange(queries as string, start, end, step);
|
const query = QUERY_MAP[type];
|
||||||
|
if (!query) {
|
||||||
|
return NextResponse.json({ error: `Unknown metric type: ${type}` }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await queryRange(query, start, end, step);
|
||||||
const instances = results.map((r) => {
|
const instances = results.map((r) => {
|
||||||
const dataPoints = parsePrometheusValues(r.values);
|
const dataPoints = parsePrometheusValues(r.values);
|
||||||
return {
|
return {
|
||||||
instance: r.metric.instance,
|
instance: r.metric.instance || '',
|
||||||
metric: r.metric,
|
metric: r.metric,
|
||||||
dataPoints,
|
dataPoints,
|
||||||
stats: calculateStats(dataPoints),
|
stats: calculateStats(dataPoints),
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ function processResults(results: { metric: Record<string, string>; values: [numb
|
||||||
return results.map((r) => {
|
return results.map((r) => {
|
||||||
const dataPoints = parseValues(r.values);
|
const dataPoints = parseValues(r.values);
|
||||||
return {
|
return {
|
||||||
instance: r.metric.instance,
|
instance: r.metric.instance || '',
|
||||||
metric: r.metric,
|
metric: r.metric,
|
||||||
dataPoints,
|
dataPoints,
|
||||||
stats: calculateStats(dataPoints),
|
stats: calculateStats(dataPoints),
|
||||||
|
|
@ -22,34 +22,55 @@ function processResults(results: { metric: Record<string, string>; values: [numb
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
const searchParams = request.nextUrl.searchParams;
|
const searchParams = request.nextUrl.searchParams;
|
||||||
const days = parseInt(searchParams.get('days') || process.env.REPORT_RANGE_DAYS || '7');
|
const startParam = searchParams.get('start');
|
||||||
|
const endParam = searchParams.get('end');
|
||||||
const step = searchParams.get('step') || process.env.REPORT_STEP || '5m';
|
const step = searchParams.get('step') || process.env.REPORT_STEP || '5m';
|
||||||
|
|
||||||
const end = new Date();
|
let end: Date;
|
||||||
const start = new Date(end.getTime() - days * 24 * 60 * 60 * 1000);
|
let start: Date;
|
||||||
|
|
||||||
|
if (startParam && endParam) {
|
||||||
|
start = new Date(startParam);
|
||||||
|
end = new Date(endParam);
|
||||||
|
} else {
|
||||||
|
const days = parseInt(searchParams.get('days') || process.env.REPORT_RANGE_DAYS || '7');
|
||||||
|
end = new Date();
|
||||||
|
start = new Date(end.getTime() - days * 24 * 60 * 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [cpuData, memData, diskData, nasData, rxData, txData, nodes] = await Promise.all([
|
const [
|
||||||
|
cpuData, memData, diskData, nasData,
|
||||||
|
ingressRxData, ingressTxData,
|
||||||
|
istioRxData, istioTxData,
|
||||||
|
nodes,
|
||||||
|
] = await Promise.all([
|
||||||
queryRange(QUERIES.cpuUsage, start, end, step),
|
queryRange(QUERIES.cpuUsage, start, end, step),
|
||||||
queryRange(QUERIES.memoryUsage, start, end, step),
|
queryRange(QUERIES.memoryUsage, start, end, step),
|
||||||
queryRange(QUERIES.diskUsage, start, end, step),
|
queryRange(QUERIES.diskUsage, start, end, step),
|
||||||
queryRange(QUERIES.nasUsage, start, end, step).catch(() => []),
|
queryRange(QUERIES.nasUsage, start, end, step).catch(() => []),
|
||||||
queryRange(QUERIES.networkReceive, start, end, step),
|
queryRange(QUERIES.ingressReceive, start, end, step).catch(() => []),
|
||||||
queryRange(QUERIES.networkTransmit, start, end, step),
|
queryRange(QUERIES.ingressTransmit, start, end, step).catch(() => []),
|
||||||
|
queryRange(QUERIES.istioReceive, start, end, step).catch(() => []),
|
||||||
|
queryRange(QUERIES.istioTransmit, start, end, step).catch(() => []),
|
||||||
getNodes(),
|
getNodes(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
period: { start, end, days, step },
|
period: { start, end, step },
|
||||||
nodes,
|
nodes,
|
||||||
metrics: {
|
metrics: {
|
||||||
cpu: processResults(cpuData),
|
cpu: processResults(cpuData),
|
||||||
memory: processResults(memData),
|
memory: processResults(memData),
|
||||||
disk: processResults(diskData),
|
disk: processResults(diskData),
|
||||||
nas: processResults(nasData),
|
nas: processResults(nasData),
|
||||||
network: {
|
ingress: {
|
||||||
receive: processResults(rxData),
|
receive: processResults(ingressRxData),
|
||||||
transmit: processResults(txData),
|
transmit: processResults(ingressTxData),
|
||||||
|
},
|
||||||
|
istio: {
|
||||||
|
receive: processResults(istioRxData),
|
||||||
|
transmit: processResults(istioTxData),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,20 @@ import NodeTable from '@/components/report/NodeTable';
|
||||||
import AlertSection from '@/components/report/AlertSection';
|
import AlertSection from '@/components/report/AlertSection';
|
||||||
import ErrorBoundary from '@/components/ErrorBoundary';
|
import ErrorBoundary from '@/components/ErrorBoundary';
|
||||||
|
|
||||||
|
function getDefaultRange() {
|
||||||
|
const end = new Date();
|
||||||
|
const start = new Date(end.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||||
|
return {
|
||||||
|
start: start.toISOString().slice(0, 10),
|
||||||
|
end: end.toISOString().slice(0, 10),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [days, setDays] = useState(7);
|
const defaultRange = getDefaultRange();
|
||||||
const { data, isLoading, error } = useReport(days);
|
const [startDate, setStartDate] = useState(defaultRange.start);
|
||||||
|
const [endDate, setEndDate] = useState(defaultRange.end);
|
||||||
|
const { data, isLoading, error } = useReport(startDate, endDate);
|
||||||
const reportRef = useRef<HTMLDivElement>(null);
|
const reportRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const handleExportPdf = async () => {
|
const handleExportPdf = async () => {
|
||||||
|
|
@ -38,7 +49,14 @@ export default function Home() {
|
||||||
heightLeft -= pdf.internal.pageSize.getHeight();
|
heightLeft -= pdf.internal.pageSize.getHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
pdf.save(`infra-report-${days}d.pdf`);
|
pdf.save(`infra-report-${startDate}_${endDate}.pdf`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setPresetRange = (days: number) => {
|
||||||
|
const end = new Date();
|
||||||
|
const start = new Date(end.getTime() - days * 24 * 60 * 60 * 1000);
|
||||||
|
setStartDate(start.toISOString().slice(0, 10));
|
||||||
|
setEndDate(end.toISOString().slice(0, 10));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
|
|
@ -68,18 +86,33 @@ export default function Home() {
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex items-center gap-4 flex-wrap">
|
||||||
{[1, 3, 7, 14, 30].map((d) => (
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<input
|
||||||
key={d}
|
type="date"
|
||||||
onClick={() => setDays(d)}
|
value={startDate}
|
||||||
className={`px-3 py-1 text-sm rounded ${
|
onChange={(e) => setStartDate(e.target.value)}
|
||||||
days === d ? 'bg-blue-600 text-white' : 'bg-white text-gray-700 hover:bg-gray-50'
|
className="px-3 py-1.5 text-sm border rounded bg-white"
|
||||||
}`}
|
/>
|
||||||
>
|
<span className="text-gray-500">~</span>
|
||||||
{d}일
|
<input
|
||||||
</button>
|
type="date"
|
||||||
))}
|
value={endDate}
|
||||||
|
onChange={(e) => setEndDate(e.target.value)}
|
||||||
|
className="px-3 py-1.5 text-sm border rounded bg-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{[1, 3, 7, 14, 30].map((d) => (
|
||||||
|
<button
|
||||||
|
key={d}
|
||||||
|
onClick={() => setPresetRange(d)}
|
||||||
|
className="px-3 py-1.5 text-sm rounded bg-white text-gray-700 hover:bg-gray-50 border"
|
||||||
|
>
|
||||||
|
{d}일
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ErrorBoundary fallback="SummaryCards 오류">
|
<ErrorBoundary fallback="SummaryCards 오류">
|
||||||
|
|
@ -87,7 +120,7 @@ export default function Home() {
|
||||||
cpu={data.metrics.cpu}
|
cpu={data.metrics.cpu}
|
||||||
memory={data.metrics.memory}
|
memory={data.metrics.memory}
|
||||||
disk={data.metrics.disk}
|
disk={data.metrics.disk}
|
||||||
network={data.metrics.network}
|
ingress={data.metrics.ingress}
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|
||||||
|
|
@ -107,8 +140,12 @@ export default function Home() {
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ErrorBoundary fallback="Network 차트 오류">
|
<ErrorBoundary fallback="Ingress Network 차트 오류">
|
||||||
<NetworkChart data={data.metrics.network} />
|
<NetworkChart title="Ingress Network Traffic" data={data.metrics.ingress} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
|
||||||
|
<ErrorBoundary fallback="Istio Network 차트 오류">
|
||||||
|
<NetworkChart title="Istio Network Traffic" data={data.metrics.istio} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|
||||||
<ErrorBoundary fallback="NodeTable 오류">
|
<ErrorBoundary fallback="NodeTable 오류">
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,22 @@ import { formatTimestamp, formatBytes } from '@/lib/formatters';
|
||||||
const ReactECharts = dynamic(() => import('echarts-for-react').then(mod => mod), { ssr: false, loading: () => <div style={{height: 300}} /> });
|
const ReactECharts = dynamic(() => import('echarts-for-react').then(mod => mod), { ssr: false, loading: () => <div style={{height: 300}} /> });
|
||||||
|
|
||||||
interface NetworkChartProps {
|
interface NetworkChartProps {
|
||||||
|
title?: string;
|
||||||
data: NetworkMetrics;
|
data: NetworkMetrics;
|
||||||
height?: number;
|
height?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NetworkChart({ data, height = 300 }: NetworkChartProps) {
|
export default function NetworkChart({ title = 'Network Traffic', data, height = 300 }: NetworkChartProps) {
|
||||||
|
if (!data.receive.length && !data.transmit.length) {
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-50 border rounded-lg p-4 text-center text-gray-400 text-sm" style={{ height }}>
|
||||||
|
{title} - 데이터 없음
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const rxSeries = data.receive.map((inst) => ({
|
const rxSeries = data.receive.map((inst) => ({
|
||||||
name: `RX ${inst.instance}`,
|
name: `RX ${inst.instance || 'total'}`,
|
||||||
type: 'line' as const,
|
type: 'line' as const,
|
||||||
smooth: true,
|
smooth: true,
|
||||||
symbol: 'none',
|
symbol: 'none',
|
||||||
|
|
@ -22,7 +31,7 @@ export default function NetworkChart({ data, height = 300 }: NetworkChartProps)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const txSeries = data.transmit.map((inst) => ({
|
const txSeries = data.transmit.map((inst) => ({
|
||||||
name: `TX ${inst.instance}`,
|
name: `TX ${inst.instance || 'total'}`,
|
||||||
type: 'line' as const,
|
type: 'line' as const,
|
||||||
smooth: true,
|
smooth: true,
|
||||||
symbol: 'none',
|
symbol: 'none',
|
||||||
|
|
@ -31,7 +40,7 @@ export default function NetworkChart({ data, height = 300 }: NetworkChartProps)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const option = {
|
const option = {
|
||||||
title: { text: 'Network Traffic', left: 'center', textStyle: { fontSize: 14 } },
|
title: { text: title, left: 'center', textStyle: { fontSize: 14 } },
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
formatter: (params: any[]) => {
|
formatter: (params: any[]) => {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ interface NodeTableProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function findInstanceStats(instances: MetricInstance[], nodeName: string) {
|
function findInstanceStats(instances: MetricInstance[], nodeName: string) {
|
||||||
const inst = instances.find((i) => i.instance.includes(nodeName));
|
const inst = instances.find((i) => i.instance && i.instance.includes(nodeName));
|
||||||
return inst?.stats || null;
|
return inst?.stats || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ interface SummaryCardsProps {
|
||||||
cpu: MetricInstance[];
|
cpu: MetricInstance[];
|
||||||
memory: MetricInstance[];
|
memory: MetricInstance[];
|
||||||
disk: MetricInstance[];
|
disk: MetricInstance[];
|
||||||
network: NetworkMetrics;
|
ingress: NetworkMetrics;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CardData {
|
interface CardData {
|
||||||
|
|
@ -26,13 +26,13 @@ function getClusterStats(instances: MetricInstance[]): { avg: number; max: numbe
|
||||||
return { avg, max, min };
|
return { avg, max, min };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SummaryCards({ cpu, memory, disk, network }: SummaryCardsProps) {
|
export default function SummaryCards({ cpu, memory, disk, ingress }: SummaryCardsProps) {
|
||||||
const cpuStats = getClusterStats(cpu);
|
const cpuStats = getClusterStats(cpu);
|
||||||
const memStats = getClusterStats(memory);
|
const memStats = getClusterStats(memory);
|
||||||
const diskStats = getClusterStats(disk);
|
const diskStats = getClusterStats(disk);
|
||||||
|
|
||||||
const netRxAvg = network.receive.reduce((sum, i) => sum + i.stats.avg, 0);
|
const netRxAvg = ingress.receive.reduce((sum, i) => sum + i.stats.avg, 0);
|
||||||
const netTxAvg = network.transmit.reduce((sum, i) => sum + i.stats.avg, 0);
|
const netTxAvg = ingress.transmit.reduce((sum, i) => sum + i.stats.avg, 0);
|
||||||
|
|
||||||
const cards: CardData[] = [
|
const cards: CardData[] = [
|
||||||
{
|
{
|
||||||
|
|
@ -57,7 +57,7 @@ export default function SummaryCards({ cpu, memory, disk, network }: SummaryCard
|
||||||
color: diskStats.max > 90 ? 'border-red-500' : diskStats.max > 70 ? 'border-yellow-500' : 'border-green-500',
|
color: diskStats.max > 90 ? 'border-red-500' : diskStats.max > 70 ? 'border-yellow-500' : 'border-green-500',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Network',
|
label: 'Ingress',
|
||||||
avg: `RX ${formatBytesPerSec(netRxAvg)}`,
|
avg: `RX ${formatBytesPerSec(netRxAvg)}`,
|
||||||
max: `TX ${formatBytesPerSec(netTxAvg)}`,
|
max: `TX ${formatBytesPerSec(netTxAvg)}`,
|
||||||
min: '',
|
min: '',
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ import { useQuery } from '@tanstack/react-query';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import type { ReportData } from '@/types/metrics';
|
import type { ReportData } from '@/types/metrics';
|
||||||
|
|
||||||
export function useReport(days: number = 7) {
|
export function useReport(start: string, end: string) {
|
||||||
return useQuery<ReportData>({
|
return useQuery<ReportData>({
|
||||||
queryKey: ['report', days],
|
queryKey: ['report', start, end],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await axios.get(`/api/report?days=${days}`);
|
const res = await axios.get(`/api/report?start=${start}&end=${end}`);
|
||||||
return res.data;
|
return res.data;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,12 @@ export const QUERIES = {
|
||||||
|| 'sum by (mountpoint,fstype)((1 - node_filesystem_avail_bytes{fstype!~"tmpfs|overlay|nfs",mountpoint!~"/boot.*"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay|nfs",mountpoint!~"/boot.*"}) * 100)',
|
|| 'sum by (mountpoint,fstype)((1 - node_filesystem_avail_bytes{fstype!~"tmpfs|overlay|nfs",mountpoint!~"/boot.*"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay|nfs",mountpoint!~"/boot.*"}) * 100)',
|
||||||
nasUsage: process.env.QUERY_NAS
|
nasUsage: process.env.QUERY_NAS
|
||||||
|| '((node_filesystem_size_bytes{fstype=~"nfs", node=~".*(bastion).*"} - node_filesystem_free_bytes{fstype=~"nfs", node=~".*(bastion).*"}) / node_filesystem_size_bytes{fstype=~"nfs", node=~".*(bastion).*"}) * 100',
|
|| '((node_filesystem_size_bytes{fstype=~"nfs", node=~".*(bastion).*"} - node_filesystem_free_bytes{fstype=~"nfs", node=~".*(bastion).*"}) / node_filesystem_size_bytes{fstype=~"nfs", node=~".*(bastion).*"}) * 100',
|
||||||
networkReceive: process.env.QUERY_NET_RX
|
ingressReceive: process.env.QUERY_INGRESS_RX
|
||||||
|| 'sum(rate(container_network_receive_bytes_total{namespace="ingress-nginx", pod=~"ingress-nginx-controller-.*"}[5m]))',
|
|| 'sum(rate(container_network_receive_bytes_total{namespace="ingress-nginx", pod=~"ingress-nginx-controller-.*"}[5m]))',
|
||||||
networkTransmit: process.env.QUERY_NET_TX
|
ingressTransmit: process.env.QUERY_INGRESS_TX
|
||||||
|| 'sum(rate(container_network_transmit_bytes_total{namespace="ingress-nginx", pod=~"ingress-nginx-controller-.*"}[5m]))',
|
|| 'sum(rate(container_network_transmit_bytes_total{namespace="ingress-nginx", pod=~"ingress-nginx-controller-.*"}[5m]))',
|
||||||
|
istioReceive: process.env.QUERY_ISTIO_RX
|
||||||
|
|| 'sum(rate(istio_request_bytes_sum{reporter="destination"}[5m]))',
|
||||||
|
istioTransmit: process.env.QUERY_ISTIO_TX
|
||||||
|
|| 'sum(rate(istio_response_bytes_sum{reporter="destination"}[5m]))',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ export interface ReportData {
|
||||||
memory: MetricInstance[];
|
memory: MetricInstance[];
|
||||||
disk: MetricInstance[];
|
disk: MetricInstance[];
|
||||||
nas: MetricInstance[];
|
nas: MetricInstance[];
|
||||||
network: NetworkMetrics;
|
ingress: NetworkMetrics;
|
||||||
|
istio: NetworkMetrics;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue