Skip to content

Commit ab1f738

Browse files
committed
feat(hub): add metrics charts (#2695)
<!-- Please make sure there is an issue that this PR is correlated to. --> ## Changes <!-- If there are frontend changes, please include screenshots. -->
1 parent 12330f4 commit ab1f738

File tree

12 files changed

+566
-818
lines changed

12 files changed

+566
-818
lines changed

frontend/apps/hub/src/domains/project/components/actors/actors-provider.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ export function ActorsProvider({
261261

262262
const metrics = atom({
263263
metrics: { cpu: null, memory: null } as Metrics,
264+
updatedAt: Date.now(),
264265
status: "pending",
265266
});
266267
metrics.onMount = (set) => {
@@ -293,6 +294,7 @@ export function ActorsProvider({
293294
...prev,
294295
...data,
295296
status: query.status,
297+
updatedAt: Date.now(),
296298
}));
297299
}
298300

frontend/apps/hub/src/queries/global.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import { ls } from "@/lib/ls";
22
import { isRivetError } from "@/lib/utils";
33
import { RivetClient } from "@rivet-gg/api-full";
44
import { RivetClient as RivetEeClient } from "@rivet-gg/api-ee";
5-
import { type APIResponse, type Fetcher, fetcher } from "@rivet-gg/api/core";
5+
import {
6+
type APIResponse,
7+
type Fetcher,
8+
fetcher,
9+
} from "@rivet-gg/api-full/core";
610
import { getConfig, timing, toast } from "@rivet-gg/components";
711
import { broadcastQueryClient } from "@tanstack/query-broadcast-client-experimental";
812
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
@@ -133,7 +137,7 @@ const clientOptions: RivetClient.Options = {
133137
...args,
134138
withCredentials: true,
135139
maxRetries: 0,
136-
timeoutMs: 30_000 // 30 seconds
140+
timeoutMs: 30_000, // 30 seconds
137141
});
138142

139143
return response;

frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId._v2/actors.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ function Actor() {
3838
ActorFeature.Config,
3939
ActorFeature.Logs,
4040
ActorFeature.State,
41+
ActorFeature.Metrics,
4142
ActorFeature.Connections,
4243
]}
4344
/>

frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId._v2/containers.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ function Actor() {
3939
if (!actor) {
4040
return (
4141
<ActorNotFound
42-
features={[ActorFeature.Config, ActorFeature.Logs]}
42+
features={[
43+
ActorFeature.Config,
44+
ActorFeature.Logs,
45+
ActorFeature.Metrics,
46+
]}
4347
/>
4448
);
4549
}

frontend/apps/hub/vite.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ export default defineConfig({
5252
// Listen on a different port since we don't proxy WebSockets on /ui
5353
hmr: {
5454
port: 5080,
55-
host: "127.0.0.1"
56-
}
55+
host: "127.0.0.1",
56+
},
5757
},
5858
preview: {
5959
port: 5080,

frontend/packages/components/src/actors/actor-context.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export type LogsAtom = Atom<{
6161
}>;
6262
export type MetricsAtom = Atom<{
6363
metrics: Metrics;
64+
updatedAt: number;
6465
// query status
6566
status: string;
6667
}>;
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { format } from "date-fns";
2+
import { useId } from "react";
3+
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
4+
import {
5+
type ChartConfig,
6+
ChartContainer,
7+
ChartTooltip,
8+
ChartTooltipContent,
9+
} from "../ui/chart";
10+
import { timing } from "../lib/timing";
11+
12+
interface ActorCpuStatsProps {
13+
interval?: number;
14+
cpu: number[];
15+
metricsAt: number;
16+
syncId?: string;
17+
}
18+
19+
const chartConfig = {
20+
value: {
21+
color: "hsl(var(--chart-1))",
22+
label: "CPU Usage",
23+
},
24+
} satisfies ChartConfig;
25+
26+
export function ActorCpuStats({
27+
interval = 15,
28+
cpu,
29+
metricsAt,
30+
syncId,
31+
}: ActorCpuStatsProps) {
32+
const data = cpu.map((value, i) => ({
33+
x: `${(cpu.length - i) * -interval}`,
34+
value: value / 100,
35+
config: {
36+
label: new Date(
37+
metricsAt - (cpu.length - i) * timing.seconds(interval),
38+
),
39+
},
40+
}));
41+
42+
const id = useId();
43+
44+
const fillId = `fill-${id}`;
45+
return (
46+
<ChartContainer config={chartConfig} className="-ml-6">
47+
<AreaChart accessibilityLayer data={data} syncId={syncId}>
48+
<CartesianGrid vertical={true} />
49+
<XAxis
50+
interval="preserveStartEnd"
51+
dataKey="x"
52+
hide
53+
axisLine={false}
54+
domain={[0, 60]}
55+
tickCount={60}
56+
/>
57+
<YAxis
58+
dataKey="value"
59+
axisLine={false}
60+
domain={[0, 1]}
61+
tickFormatter={(value) => `${value * 100}%`}
62+
/>
63+
<ChartTooltip
64+
content={
65+
<ChartTooltipContent
66+
hideIndicator
67+
labelKey="label"
68+
labelFormatter={(label) => {
69+
return format(label, "HH:mm:ss");
70+
}}
71+
valueFormatter={(value) => {
72+
if (typeof value !== "number") {
73+
return "n/a";
74+
}
75+
return `${(value * 100).toFixed(2)}%`;
76+
}}
77+
/>
78+
}
79+
/>
80+
<defs>
81+
<linearGradient id={fillId} x1="0" y1="0" x2="0" y2="1">
82+
<stop
83+
offset="5%"
84+
stopColor="var(--color-value)"
85+
stopOpacity={0.8}
86+
/>
87+
<stop
88+
offset="95%"
89+
stopColor="var(--color-value)"
90+
stopOpacity={0.1}
91+
/>
92+
</linearGradient>
93+
</defs>
94+
<Area
95+
isAnimationActive={false}
96+
dataKey="value"
97+
type="linear"
98+
fill={`url(#${fillId})`}
99+
fillOpacity={0.4}
100+
stroke="var(--color-value)"
101+
stackId="a"
102+
/>
103+
</AreaChart>
104+
</ChartContainer>
105+
);
106+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { format } from "date-fns";
2+
import { filesize } from "filesize";
3+
import { useId } from "react";
4+
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
5+
import {
6+
type ChartConfig,
7+
ChartContainer,
8+
ChartTooltip,
9+
ChartTooltipContent,
10+
} from "../ui/chart";
11+
import { timing } from "../lib/timing";
12+
13+
interface ActorMemoryStatsProps {
14+
metricsAt: number;
15+
memory: number[];
16+
allocatedMemory?: number;
17+
syncId?: string;
18+
interval?: number;
19+
}
20+
21+
const chartConfig = {
22+
value: {
23+
color: "hsl(var(--chart-1))",
24+
label: "Memory Usage",
25+
},
26+
} satisfies ChartConfig;
27+
28+
export function ActorMemoryStats({
29+
interval = 15,
30+
memory,
31+
allocatedMemory,
32+
metricsAt,
33+
syncId,
34+
}: ActorMemoryStatsProps) {
35+
const data = memory.map((value, i) => ({
36+
x: `${(memory.length - i) * -interval}`,
37+
value,
38+
config: {
39+
label: new Date(
40+
metricsAt - (memory.length - i) * timing.seconds(interval),
41+
),
42+
},
43+
}));
44+
45+
const max = allocatedMemory || Math.max(...memory);
46+
47+
const id = useId();
48+
49+
const fillId = `fill-${id}`;
50+
return (
51+
<ChartContainer config={chartConfig} className="-ml-6">
52+
<AreaChart accessibilityLayer data={data} syncId={syncId}>
53+
<CartesianGrid vertical={true} />
54+
<XAxis
55+
interval="preserveStartEnd"
56+
dataKey="x"
57+
hide
58+
axisLine={false}
59+
domain={[0, 60]}
60+
tickCount={60}
61+
includeHidden
62+
/>
63+
<YAxis
64+
dataKey="value"
65+
axisLine={false}
66+
domain={[0, max]}
67+
tickFormatter={(value) =>
68+
`${Math.ceil((value / max) * 100)}%`
69+
}
70+
/>
71+
<ChartTooltip
72+
content={
73+
<ChartTooltipContent
74+
hideIndicator
75+
labelKey="label"
76+
labelFormatter={(label) => {
77+
return format(label, "HH:mm:ss");
78+
}}
79+
valueFormatter={(value) => {
80+
if (typeof value !== "number") {
81+
return "n/a";
82+
}
83+
return `${filesize(value)} (${Math.round((value / max) * 100).toFixed(2)}%)`;
84+
}}
85+
/>
86+
}
87+
/>
88+
<defs>
89+
<linearGradient id={fillId} x1="0" y1="0" x2="0" y2="1">
90+
<stop
91+
offset="5%"
92+
stopColor="var(--color-value)"
93+
stopOpacity={0.8}
94+
/>
95+
<stop
96+
offset="95%"
97+
stopColor="var(--color-value)"
98+
stopOpacity={0.1}
99+
/>
100+
</linearGradient>
101+
</defs>
102+
<Area
103+
isAnimationActive={false}
104+
dataKey="value"
105+
type="linear"
106+
fill={`url(#${fillId})`}
107+
fillOpacity={0.4}
108+
stroke="var(--color-value)"
109+
stackId="a"
110+
/>
111+
</AreaChart>
112+
</ChartContainer>
113+
);
114+
}

0 commit comments

Comments
 (0)