Files
cameleer-server/ui/src/pages/Routes/RoutesMetrics.tsx

204 lines
7.6 KiB
TypeScript
Raw Normal View History

import { useMemo } from 'react';
import { useParams } from 'react-router';
import {
StatCard, Sparkline, MonoText, Badge,
DataTable, AreaChart, LineChart, BarChart,
} from '@cameleer/design-system';
import type { Column } from '@cameleer/design-system';
import { useRouteMetrics } from '../../api/queries/catalog';
import { useExecutionStats, useStatsTimeseries } from '../../api/queries/executions';
import { useGlobalFilters } from '@cameleer/design-system';
import styles from './RoutesMetrics.module.css';
interface RouteRow {
id: string;
routeId: string;
appId: string;
exchangeCount: number;
successRate: number;
avgDurationMs: number;
p99DurationMs: number;
errorRate: number;
throughputPerSec: number;
sparkline: number[];
}
export default function RoutesMetrics() {
const { appId, routeId } = useParams();
const { timeRange } = useGlobalFilters();
const timeFrom = timeRange.start.toISOString();
const timeTo = timeRange.end.toISOString();
const { data: metrics } = useRouteMetrics(timeFrom, timeTo, appId);
const { data: stats } = useExecutionStats(timeFrom, timeTo, routeId, appId);
const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo, routeId, appId);
const rows: RouteRow[] = useMemo(() =>
(metrics || []).map((m: any) => ({
id: `${m.appId}/${m.routeId}`,
...m,
})),
[metrics],
);
const sparklineData = useMemo(() =>
(timeseries?.buckets || []).map((b: any) => b.totalCount as number),
[timeseries],
);
const chartData = useMemo(() =>
(timeseries?.buckets || []).map((b: any) => ({
time: new Date(b.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
throughput: b.totalCount,
latency: b.avgDurationMs,
errors: b.failedCount,
successRate: b.totalCount > 0 ? ((b.totalCount - b.failedCount) / b.totalCount) * 100 : 100,
})),
[timeseries],
);
const columns: Column<RouteRow>[] = [
{ key: 'routeId', header: 'Route', render: (v) => <MonoText size="sm">{String(v)}</MonoText> },
{ key: 'appId', header: 'App', render: (v) => <Badge label={String(v)} color="auto" /> },
{ key: 'exchangeCount', header: 'Exchanges', sortable: true },
{
key: 'successRate', header: 'Success', sortable: true,
render: (v) => `${((v as number) * 100).toFixed(1)}%`,
},
{ key: 'avgDurationMs', header: 'Avg Duration', sortable: true, render: (v) => `${(v as number).toFixed(0)}ms` },
{ key: 'p99DurationMs', header: 'P99', sortable: true, render: (v) => `${(v as number).toFixed(0)}ms` },
{
key: 'errorRate', header: 'Error Rate', sortable: true,
render: (v) => {
const rate = v as number;
const cls = rate > 0.05 ? styles.rateBad : rate > 0.01 ? styles.rateWarn : styles.rateGood;
return <span className={cls}>{(rate * 100).toFixed(1)}%</span>;
},
},
{
key: 'sparkline', header: 'Trend', width: '80px',
render: (v) => <Sparkline data={v as number[]} />,
},
];
const errorRate = stats?.totalCount
? (((stats.failedCount ?? 0) / stats.totalCount) * 100)
: 0;
const prevErrorRate = stats?.prevTotalCount
? (((stats.prevFailedCount ?? 0) / stats.prevTotalCount) * 100)
: 0;
const errorTrend: 'up' | 'down' | 'neutral' = errorRate > prevErrorRate ? 'up' : errorRate < prevErrorRate ? 'down' : 'neutral';
const errorTrendValue = stats?.prevTotalCount
? `${Math.abs(errorRate - prevErrorRate).toFixed(2)}%`
: undefined;
const p99Ms = stats?.p99LatencyMs ?? 0;
const prevP99Ms = stats?.prevP99LatencyMs ?? 0;
const latencyTrend: 'up' | 'down' | 'neutral' = p99Ms > prevP99Ms ? 'up' : p99Ms < prevP99Ms ? 'down' : 'neutral';
const latencyTrendValue = prevP99Ms ? `${Math.abs(p99Ms - prevP99Ms)}ms` : undefined;
const totalCount = stats?.totalCount ?? 0;
const prevTotalCount = stats?.prevTotalCount ?? 0;
const throughputTrend: 'up' | 'down' | 'neutral' = totalCount > prevTotalCount ? 'up' : totalCount < prevTotalCount ? 'down' : 'neutral';
const throughputTrendValue = prevTotalCount
? `${Math.abs(((totalCount - prevTotalCount) / prevTotalCount) * 100).toFixed(0)}%`
: undefined;
const successRate = stats?.totalCount
? (((stats.totalCount - (stats.failedCount ?? 0)) / stats.totalCount) * 100)
: 100;
const activeCount = stats?.activeCount ?? 0;
const errorSparkline = (timeseries?.buckets || []).map((b: any) => b.failedCount as number);
const latencySparkline = (timeseries?.buckets || []).map((b: any) => b.p99DurationMs as number);
return (
<div>
<div className={styles.statStrip}>
<StatCard
label="Total Throughput"
value={totalCount.toLocaleString()}
detail="exchanges"
trend={throughputTrend}
trendValue={throughputTrendValue}
accent="amber"
sparkline={sparklineData}
/>
<StatCard
label="System Error Rate"
value={`${errorRate.toFixed(2)}%`}
detail={`${stats?.failedCount ?? 0} errors / ${totalCount.toLocaleString()} total`}
trend={errorTrend}
trendValue={errorTrendValue}
accent={errorRate < 1 ? 'success' : 'error'}
sparkline={errorSparkline}
/>
<StatCard
label="P99 Latency"
value={`${p99Ms}ms`}
detail={`Avg: ${stats?.avgDurationMs ?? 0}ms`}
trend={latencyTrend}
trendValue={latencyTrendValue}
accent={p99Ms > 300 ? 'error' : p99Ms > 200 ? 'warning' : 'success'}
sparkline={latencySparkline}
/>
<StatCard
label="Success Rate"
value={`${successRate.toFixed(1)}%`}
detail={`${activeCount} active routes`}
accent="success"
sparkline={sparklineData.map((v, i) => {
const failed = errorSparkline[i] ?? 0;
return v > 0 ? ((v - failed) / v) * 100 : 100;
})}
/>
<StatCard
label="In-Flight"
value={activeCount}
detail="active exchanges"
accent="amber"
/>
</div>
<div className={styles.tableSection}>
<div className={styles.tableHeader}>
<span className={styles.tableTitle}>Route Metrics</span>
<span className={styles.tableMeta}>{rows.length} routes</span>
</div>
<DataTable
columns={columns}
data={rows}
sortable
pageSize={20}
/>
</div>
{chartData.length > 0 && (
<div className={styles.chartGrid}>
<div className={styles.chartCard}>
<div className={styles.chartTitle}>Throughput</div>
<AreaChart series={[{ label: 'Throughput', data: chartData.map((d: any, i: number) => ({ x: i, y: d.throughput })) }]} height={200} />
</div>
<div className={styles.chartCard}>
<div className={styles.chartTitle}>Latency</div>
<LineChart
series={[{ label: 'Latency', data: chartData.map((d: any, i: number) => ({ x: i, y: d.latency })) }]}
height={200}
threshold={{ value: 300, label: 'SLA 300ms' }}
/>
</div>
<div className={styles.chartCard}>
<div className={styles.chartTitle}>Errors</div>
<BarChart series={[{ label: 'Errors', data: chartData.map((d: any) => ({ x: d.time as string, y: d.errors })) }]} height={200} />
</div>
<div className={styles.chartCard}>
<div className={styles.chartTitle}>Success Rate</div>
<AreaChart series={[{ label: 'Success Rate', data: chartData.map((d: any, i: number) => ({ x: i, y: d.successRate })) }]} height={200} />
</div>
</div>
)}
</div>
);
}