feat: add Traefik metrics to dashboard API and sparkline charts
- Add promRange() for time-series data from Prometheus - Query Traefik metrics: RPS, latency, error rate - Dashboard page shows 4 sparkline charts (RPS, latency, errors, traces) - Charts render inline SVG with auto-refresh every 15s Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
72f4759f8b
commit
d14ed70240
@@ -104,6 +104,15 @@ async function fetchTraces(): Promise<Trace[]> {
|
||||
return all.sort((a, b) => b.duration - a.duration).slice(0, 20);
|
||||
}
|
||||
|
||||
async function promRange(query: string, steps = 20): Promise<number[]> {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const u = `${PROMETHEUS}/api/v1/query_range?query=${encodeURIComponent(query)}&start=${now - 300}&end=${now}&step=${(300 / steps).toFixed(0)}`;
|
||||
const d = await fetchJSON(u);
|
||||
const result = (d as { data?: { result?: { values?: unknown[][] }[] } })?.data?.result;
|
||||
if (!result?.length) return [];
|
||||
return result[0].values.map((v) => { const f = parseFloat(v[1] as string); return isNaN(f) ? 0 : f; });
|
||||
}
|
||||
|
||||
async function promQuery(query: string): Promise<number | null> {
|
||||
const d = await fetchJSON(
|
||||
`${PROMETHEUS}/api/v1/query?query=${encodeURIComponent(query)}`,
|
||||
@@ -127,6 +136,10 @@ interface DashboardData {
|
||||
netIn: number | null;
|
||||
netOut: number | null;
|
||||
};
|
||||
rps: number[];
|
||||
latency: number[];
|
||||
errors: number[];
|
||||
traceVolume: number[];
|
||||
links: { url: string; label: string }[];
|
||||
}
|
||||
|
||||
@@ -138,7 +151,7 @@ export async function GET() {
|
||||
const containers = Array.isArray(containersRaw) ? containersRaw : [];
|
||||
const services = parseServices(containers);
|
||||
|
||||
const [traces, cpu, ram, disk, load1, load5, load15, netIn, netOut] =
|
||||
const [traces, cpu, ram, disk, load1, load5, load15, netIn, netOut, rps, latency, errors] =
|
||||
await Promise.all([
|
||||
fetchTraces(),
|
||||
promQuery(`100 - (avg by(instance)(rate(node_cpu_seconds_total{mode="idle"}[1m])) * 100)`),
|
||||
@@ -149,6 +162,9 @@ export async function GET() {
|
||||
promQuery('node_load15'),
|
||||
promQuery(`rate(node_network_receive_bytes_total{device!="lo"}[1m])`),
|
||||
promQuery(`rate(node_network_transmit_bytes_total{device!="lo"}[1m])`),
|
||||
promRange('sum(rate(traefik_service_requests_total[1m]))'),
|
||||
promRange('avg(traefik_service_request_duration_ms_sum / traefik_service_request_duration_ms_count)'),
|
||||
promRange('sum(rate(traefik_service_requests_total{code=~"5.."}[1m]))'),
|
||||
]);
|
||||
|
||||
const links: { url: string; label: string }[] = [
|
||||
@@ -166,10 +182,13 @@ export async function GET() {
|
||||
}
|
||||
}
|
||||
|
||||
const traceVolume = [traces.length];
|
||||
|
||||
const data: DashboardData = {
|
||||
services,
|
||||
traces,
|
||||
node: { cpu, ram, disk, load1, load5, load15, netIn, netOut },
|
||||
rps, latency, errors, traceVolume,
|
||||
links,
|
||||
};
|
||||
|
||||
|
||||
@@ -29,6 +29,10 @@ interface DashboardData {
|
||||
netIn: number | null;
|
||||
netOut: number | null;
|
||||
};
|
||||
rps: number[];
|
||||
latency: number[];
|
||||
errors: number[];
|
||||
traceVolume: number[];
|
||||
links: { url: string; label: string }[];
|
||||
}
|
||||
|
||||
@@ -177,6 +181,7 @@ export default function Dashboard() {
|
||||
const running = data?.services.filter(s => s.state === 'running').length ?? 0;
|
||||
const degraded = (data?.services.length ?? 0) - running;
|
||||
const hasNode = data?.node.cpu !== null || data?.node.ram !== null;
|
||||
const hasTraffik = (data?.rps?.length ?? 0) > 0 || (data?.latency?.length ?? 0) > 0;
|
||||
const hasOTel = false;
|
||||
const node = data?.node;
|
||||
const gaugeColor = (v: number | null) => {
|
||||
@@ -301,6 +306,36 @@ export default function Dashboard() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Request Rate */}
|
||||
{hasTraffik && (
|
||||
<div className="card">
|
||||
<div className="card-h"><h2>Request Rate</h2><span className="badge">{data?.rps?.length ? `${data.rps[data.rps.length - 1].toFixed(1)}/s` : '-'}</span></div>
|
||||
<div className="card-c"><Sparkline data={data?.rps ?? []} color="#58a6ff" /></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Latency */}
|
||||
{hasTraffik && (
|
||||
<div className="card">
|
||||
<div className="card-h"><h2>Latency</h2><span className="badge">{data?.latency?.length ? `${data.latency[data.latency.length - 1].toFixed(0)}ms` : '-'}</span></div>
|
||||
<div className="card-c"><Sparkline data={data?.latency ?? []} color="#bc8cff" /></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Rate */}
|
||||
{hasTraffik && (
|
||||
<div className="card">
|
||||
<div className="card-h"><h2>Error Rate</h2><span className="badge">{data?.errors?.length ? `${data.errors[data.errors.length - 1].toFixed(1)}/s` : '-'}</span></div>
|
||||
<div className="card-c"><Sparkline data={data?.errors ?? []} color="#f85149" /></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Trace Volume */}
|
||||
<div className="card">
|
||||
<div className="card-h"><h2>Trace Volume</h2><span className="badge">{data?.traces.length ?? 0} traces</span></div>
|
||||
<div className="card-c"><Sparkline data={data?.traceVolume ?? []} color="#3fb950" /></div>
|
||||
</div>
|
||||
|
||||
{/* Traces */}
|
||||
<div className="card">
|
||||
<div className="card-h"><h2>Recent Traces</h2><span className="badge">{data?.traces.length ?? 0}</span></div>
|
||||
|
||||
Reference in New Issue
Block a user