Skip to content

Commit a2d2296

Browse files
Merge pull request #12 from GetStream/chore-readme
added metrics
2 parents 9c2c3d8 + 6361082 commit a2d2296

35 files changed

+3205
-438
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,12 @@ The server exposes a set of endpoints for cluster management and raw data operat
138138
| POST | /api/raw/delete | Delete a key-value pair. | `{"key": "mykey"}` |
139139
| POST | /api/raw/scan | Scan a range of keys. | `{"start_key": "a", "end_key": "z", "limit": 100}` |
140140

141+
### Metrics
142+
143+
| Method | Endpoint | Description |
144+
| ------ | -------- | -------------------------------------- |
145+
| GET | /metrics | PD and TiKV metrics from the instances |
146+
141147
## 🤝 Contributing
142148

143149
We welcome contributions from the community! If you're interested in making the TiKV Admin Web UI even better:

app/app/globals.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22
@import "tw-animate-css";
33

44
@theme {
5+
--breakpoint-sm: 640px;
6+
--breakpoint-md: 768px;
7+
--breakpoint-lg: 1024px;
8+
--breakpoint-xl: 1280px;
9+
--breakpoint-2xl: 1536px;
10+
--breakpoint-3xl: 2160px;
11+
512
--color-background: hsl(0 0% 100%);
613
--color-foreground: hsl(240 10% 3.9%);
714
--color-card: hsl(0 0% 100%);
@@ -23,6 +30,11 @@
2330
--color-ring: hsl(346.8 77.2% 49.8%);
2431
--radius: 0.65rem;
2532
--color-menu: #eee;
33+
--chart-1: hsl(346.8 77.2% 49.8%);
34+
--chart-2: oklch(0.6 0.118 184.704);
35+
--chart-3: oklch(0.398 0.07 227.392);
36+
--chart-4: oklch(0.828 0.189 84.429);
37+
--chart-5: oklch(0.769 0.188 70.08);
2638
}
2739

2840
@layer base {
@@ -47,6 +59,11 @@
4759
--color-border: hsl(240 3.7% 15.9%);
4860
--color-input: hsl(240 3.7% 15.9%);
4961
--color-ring: hsl(346.8 77.2% 49.8%);
62+
--chart-1: hsl(346.8 77.2% 49.8%);
63+
--chart-2: oklch(0.696 0.17 162.48);
64+
--chart-3: oklch(0.769 0.188 70.08);
65+
--chart-4: oklch(0.627 0.265 303.9);
66+
--chart-5: oklch(0.645 0.246 16.439);
5067
}
5168

5269
* {

app/app/layout.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { Geist, Geist_Mono } from "next/font/google";
44
import "./globals.css";
55
import { Toaster } from "@/components/ui/sonner";
66
import TiKV from "@/assets/img/tikv.webp";
7+
import Sidebar from "@/components/sidebar";
8+
import Provider from "@/components/provider";
9+
import Cluster from "@/components/cluster";
710

811
const geistSans = Geist({
912
variable: "--font-geist-sans",
@@ -33,8 +36,14 @@ export default function RootLayout({
3336
<body
3437
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
3538
>
36-
{children}
37-
<Toaster position="bottom-right" />
39+
<Provider>
40+
<div className="flex flex-1 flex-row h-screen">
41+
<Sidebar />
42+
{children}
43+
</div>
44+
<Cluster />
45+
<Toaster position="bottom-right" />
46+
</Provider>
3847
</body>
3948
</html>
4049
);

app/app/metrics/page.tsx

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
"use client";
2+
3+
import { useMetrics } from "@/hooks/use-metrics";
4+
import {
5+
CircleXIcon,
6+
CircleCheckIcon,
7+
ServerIcon,
8+
ClockIcon,
9+
HeartIcon,
10+
HardDriveIcon,
11+
InfoIcon,
12+
ChartSplineIcon,
13+
ServerCrashIcon,
14+
} from "lucide-react";
15+
import { Area, AreaChart, Line, LineChart, XAxis, YAxis } from "recharts";
16+
import {
17+
type ChartConfig,
18+
ChartContainer,
19+
ChartTooltip,
20+
ChartTooltipContent,
21+
} from "@/components/ui/chart";
22+
import { Badge } from "@/components/ui/badge";
23+
import { ScrollArea } from "@/components/ui/scroll-area";
24+
import { MetricCard } from "@/components/ui/metric-card";
25+
import { Spinner } from "@/components/ui/spinner";
26+
import dayjs from "dayjs";
27+
import relativeTime from "dayjs/plugin/relativeTime";
28+
import {
29+
parseBytes,
30+
formatUptime,
31+
getFormatter,
32+
processGaugeData,
33+
} from "@/lib/utils";
34+
35+
dayjs.extend(relativeTime);
36+
37+
export default function MetricsPage() {
38+
const { isLoading, data } = useMetrics();
39+
return (
40+
<div className="p-5 w-full">
41+
<div className="flex items-center gap-2 text-xl font-semibold mb-8 pb-4 border-b">
42+
<ServerIcon size={18} /> Metrics
43+
</div>
44+
{isLoading ? (
45+
<div className="flex flex-col gap-2 text-gray-500 items-center justify-center h-full mt-[-5%]">
46+
<div>
47+
<Spinner className="size-5" />
48+
</div>
49+
<div className="text-xs uppercase">loading metrics...</div>
50+
</div>
51+
) : (
52+
<ScrollArea className="h-full">
53+
<div className="text-md font-semibold text-gray-500 mb-4">
54+
PD Stores ({data?.pd?.count})
55+
</div>
56+
<div className="grid gap-4 grid-cols-1 md:grid-cols-3 lg:grid-cols-4 3xl:grid-cols-5 mb-20">
57+
{data?.pd?.stores.map((store) => (
58+
<MetricCard key={store.store.id}>
59+
<div className="border-b pb-5 mb-2 relative">
60+
<div className="flex items-center gap-2">
61+
<div>
62+
<div className="text-sm font-semibold">
63+
Node: #{store.store.id}
64+
</div>
65+
<div className="text-xs text-gray-500 font-mono flex items-center gap-1">
66+
<ServerCrashIcon size={12} />
67+
{store.store.address}
68+
</div>
69+
<div className="text-xs text-gray-500 font-mono flex items-center gap-1">
70+
<ChartSplineIcon size={12} />
71+
{store.store.status_address}
72+
</div>
73+
</div>
74+
</div>
75+
76+
{store.store.state_name.toLocaleLowerCase() === "up" ? (
77+
<Badge
78+
variant="outline"
79+
className="absolute top-0 right-0 text-green-300/50 border-green-300/50 font-semibold flex items-center gap-1"
80+
>
81+
<CircleCheckIcon size={13} />
82+
Online
83+
</Badge>
84+
) : (
85+
<Badge
86+
variant="outline"
87+
className="absolute top-0 right-0 text-red-400/60 border-red-400/60 font-semibold flex items-center gap-1"
88+
>
89+
<CircleXIcon size={13} />
90+
Offline
91+
</Badge>
92+
)}
93+
</div>
94+
<div className="flex items-center justify-between gap-2 border-b p-5 mb-2">
95+
<div className="flex items-center justify-center gap-2">
96+
<ClockIcon />
97+
<div>
98+
<div className="text-[0.6rem] text-gray-500 uppercase">
99+
uptime
100+
</div>
101+
<div
102+
className="text-gray-200/90"
103+
title={store.status.uptime}
104+
>
105+
{formatUptime(store.status.uptime)}
106+
</div>
107+
</div>
108+
</div>
109+
<div className="flex items-center justify-center gap-2">
110+
<HeartIcon />
111+
<div>
112+
<div className="text-[0.6rem] text-gray-500 text-center uppercase">
113+
heartbeat
114+
</div>
115+
<div className="text-gray-200/90">
116+
{dayjs(store.store.last_heartbeat / 1000000).format(
117+
"HH:mm:ss"
118+
)}
119+
</div>
120+
</div>
121+
</div>
122+
</div>
123+
<div className="mt-5">
124+
<div className="flex items-center justify-between gap-2">
125+
<div className="flex items-center justify-center gap-1 text-gray-500 text-sm">
126+
<HardDriveIcon size={16} /> Storage
127+
</div>
128+
<div className="text-sm text-gray-500">
129+
{store.status.used_size} / {store.status.capacity}
130+
</div>
131+
</div>
132+
<div className="rounded-lg bg-gray-600 relative my-2 h-2 overflow-hidden">
133+
<div
134+
className="absolute top-0 left-0 w-full h-full bg-green-500/50"
135+
style={{
136+
width: `${(
137+
(parseBytes(store.status.used_size) /
138+
parseBytes(store.status.capacity)) *
139+
100
140+
).toFixed(2)}%`,
141+
}}
142+
></div>
143+
</div>
144+
<div className="flex items-center justify-between gap-2">
145+
<div className="text-sm text-gray-500 ">
146+
{store.status.available} available
147+
</div>
148+
<div className="text-sm text-green-800 font-semibold ">
149+
{(
150+
(parseBytes(store.status.used_size) /
151+
parseBytes(store.status.capacity)) *
152+
100
153+
).toFixed(2)}
154+
% used
155+
</div>
156+
</div>
157+
</div>
158+
</MetricCard>
159+
))}
160+
</div>
161+
<div className="text-md font-semibold text-gray-500 mb-4">
162+
TiKV Overview
163+
</div>
164+
<div className="grid gap-4 pb-30 grid-cols-1 md:grid-cols-3 3xl:grid-cols-4 ">
165+
{data?.tikv?.gauges.map((gauge: any, i: number) => {
166+
const { data: chartData, series } = processGaugeData(
167+
gauge.points
168+
);
169+
170+
const formatter = getFormatter(gauge.unit);
171+
172+
const dynamicConfig: ChartConfig = {
173+
...series.reduce((acc, key, index) => {
174+
const colorVar = `--chart-${(index % 5) + 1}`;
175+
acc[key] = {
176+
label: key,
177+
color: `var(${colorVar})`,
178+
};
179+
return acc;
180+
}, {} as Record<string, { label: string; color: string }>),
181+
};
182+
183+
return (
184+
<MetricCard key={gauge.name + i}>
185+
<div className="font-semibold text-gray-500 mb-10 flex items-center justify-between">
186+
<span>{gauge.name}</span>
187+
</div>
188+
<ChartContainer config={dynamicConfig}>
189+
{series.length > 1 ? (
190+
<LineChart data={chartData.slice(0, 8)}>
191+
<ChartTooltip
192+
content={
193+
<ChartTooltipContent
194+
formatter={(value, name, item) => (
195+
<div className="flex flex-1 justify-between items-center gap-4">
196+
<div className="flex items-center gap-2">
197+
<div
198+
className="h-2.5 w-2.5 shrink-0 rounded-[2px]"
199+
style={{ backgroundColor: item.color }}
200+
/>
201+
<span className="text-muted-foreground truncate max-w-[150px]">
202+
{name}
203+
</span>
204+
</div>
205+
<span className="font-mono font-medium">
206+
{formatter(value as number)}
207+
</span>
208+
</div>
209+
)}
210+
/>
211+
}
212+
/>
213+
<XAxis
214+
dataKey="ts"
215+
tickFormatter={(value) =>
216+
dayjs(value).format("HH:mm")
217+
}
218+
/>
219+
<YAxis tickFormatter={formatter} width={70} />
220+
{series.slice(0, 10).map((key) => (
221+
<Line
222+
key={key}
223+
dataKey={key}
224+
type="monotone"
225+
stroke={dynamicConfig[key]?.color}
226+
strokeWidth={2}
227+
dot={false}
228+
/>
229+
))}
230+
</LineChart>
231+
) : (
232+
<AreaChart data={chartData}>
233+
<ChartTooltip
234+
content={
235+
<ChartTooltipContent
236+
formatter={(value, name, item) => (
237+
<div className="flex flex-1 justify-between items-center gap-4">
238+
<div className="flex items-center gap-2">
239+
<div
240+
className="h-2.5 w-2.5 shrink-0 rounded-[2px]"
241+
style={{ backgroundColor: item.color }}
242+
/>
243+
<span className="text-muted-foreground truncate max-w-[150px]">
244+
{name}
245+
</span>
246+
</div>
247+
<span className="font-mono font-medium">
248+
{formatter(value as number)}
249+
</span>
250+
</div>
251+
)}
252+
/>
253+
}
254+
/>
255+
<XAxis
256+
dataKey="ts"
257+
tickFormatter={(value) =>
258+
dayjs(value).format("HH:mm")
259+
}
260+
/>
261+
<YAxis tickFormatter={formatter} width={70} />
262+
<defs>
263+
{series.map((key, index) => (
264+
<linearGradient
265+
key={key}
266+
id={`gradient-${i}-${index}`}
267+
x1="0"
268+
y1="0"
269+
x2="0"
270+
y2="1"
271+
>
272+
<stop
273+
offset="5%"
274+
stopColor={dynamicConfig[key]?.color}
275+
stopOpacity={0.5}
276+
/>
277+
<stop
278+
offset="95%"
279+
stopColor={dynamicConfig[key]?.color}
280+
stopOpacity={0.1}
281+
/>
282+
</linearGradient>
283+
))}
284+
</defs>
285+
{series.map((key, index) => (
286+
<Area
287+
key={key}
288+
dataKey={key}
289+
type="basis"
290+
fill={`url(#gradient-${i}-${index})`}
291+
fillOpacity={0.2}
292+
stroke={dynamicConfig[key]?.color}
293+
/>
294+
))}
295+
</AreaChart>
296+
)}
297+
</ChartContainer>
298+
<div className="text-xs text-gray-600 flex items-center gap-1">
299+
<InfoIcon size={12} /> {gauge.description}
300+
</div>
301+
</MetricCard>
302+
);
303+
})}
304+
</div>
305+
</ScrollArea>
306+
)}
307+
</div>
308+
);
309+
}

0 commit comments

Comments
 (0)