proxy,ui: add performance monitoring with Prometheus metrics (#743)

Add a comprehensive performance monitoring system that collects CPU, memory, swap, load average, network IO, and GPU stats. Provides both a REST API for the UI and a Prometheus /metrics endpoint.

Backend changes:
- New internal/perf package with configurable interval-based stats collection
- GPU monitoring via LACT (Unix socket) and nvidia-smi fallback on Linux
- Ring buffer (internal/ring) for time-series stat storage
- Prometheus /metrics endpoint with all system and GPU metrics
- Moved LogMonitor to internal/logmon package
- New PerformanceConfig for hot-reloadable monitoring settings
- REST /api/performance endpoint replacing SSE streaming

UI changes:
- New Performance page with real-time charts for CPU, memory, GPU, and network
- Reusable PerformanceChart component
- LLAMA_SWAP_URL environment variable support
- Improved capture dialog display

Other:
- Example Grafana dashboard for Prometheus metrics
- monitor-test standalone binary
- Config schema and example updates

fixes #596
This commit is contained in:
Benson Wong
2026-05-09 13:29:22 -07:00
committed by GitHub
parent e261745c66
commit 7e3e94a08a
49 changed files with 4322 additions and 273 deletions
@@ -0,0 +1,148 @@
<script lang="ts">
import { onMount } from "svelte";
import { Chart, registerables } from "chart.js";
import { isDarkMode } from "../stores/theme";
Chart.register(...registerables);
interface Dataset {
label: string;
data: number[];
borderColor: string;
}
interface Props {
title: string;
labels: string[];
datasets: Dataset[];
yMin?: number;
yMax?: number;
yLabel?: string;
showLegend?: boolean;
}
let { title, labels, datasets, yMin, yMax, yLabel, showLegend = true }: Props = $props();
let canvas: HTMLCanvasElement;
let chart: Chart;
function getChartColors(dark: boolean) {
return {
grid: dark ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.08)",
tick: dark ? "#9ca3af" : "#6b7280",
legend: dark ? "#d1d5db" : "#374151",
tooltipBg: dark ? "#1f2937" : "#ffffff",
tooltipText: dark ? "#f3f4f6" : "#111827",
tooltipBorder: dark ? "#374151" : "#e5e7eb",
};
}
function buildOptions(dark: boolean) {
const colors = getChartColors(dark);
return {
responsive: true,
maintainAspectRatio: false,
animation: false as const,
interaction: {
mode: "index" as const,
intersect: false,
},
plugins: {
legend: {
display: showLegend,
position: "top" as const,
labels: {
color: colors.legend,
usePointStyle: true,
pointStyle: "circle" as const,
padding: 12,
font: { size: 11 },
},
},
title: {
display: true,
text: title,
color: colors.legend,
font: { size: 14, weight: "bold" as const },
},
tooltip: {
backgroundColor: colors.tooltipBg,
titleColor: colors.tooltipText,
bodyColor: colors.tooltipText,
borderColor: colors.tooltipBorder,
borderWidth: 1,
},
},
scales: {
x: {
bounds: "data" as const,
offset: false,
ticks: { color: colors.tick, maxRotation: 0, font: { size: 10 }, maxTicksLimit: 10 },
grid: { color: colors.grid },
},
y: {
min: yMin,
max: yMax,
ticks: { color: colors.tick, font: { size: 10 } },
grid: { color: colors.grid },
title: yLabel
? { display: true, text: yLabel, color: colors.tick }
: undefined,
},
},
};
}
onMount(() => {
chart = new Chart(canvas, {
type: "line",
data: {
labels: [...labels],
datasets: datasets.map((ds) => ({
label: ds.label,
data: [...ds.data],
borderColor: ds.borderColor,
backgroundColor: ds.borderColor + "20",
borderWidth: 1.5,
pointRadius: 0,
tension: 0.4,
fill: false,
})),
},
options: buildOptions($isDarkMode),
});
return () => {
chart.destroy();
};
});
$effect(() => {
if (!chart) return;
const _dark = $isDarkMode;
chart.options = buildOptions(_dark);
chart.update("none");
});
$effect(() => {
if (!chart) return;
const _l = labels;
const _d = datasets;
chart.data.labels = [..._l];
chart.data.datasets = _d.map((ds) => ({
label: ds.label,
data: [...ds.data],
borderColor: ds.borderColor,
backgroundColor: ds.borderColor + "20",
borderWidth: 1.5,
pointRadius: 0,
tension: 0.4,
fill: false,
}));
chart.update("none");
});
</script>
<div class="card p-4 h-[300px]">
<canvas bind:this={canvas}></canvas>
</div>