Skip to content

Commit 81ab3d6

Browse files
authored
Merge pull request #353 from SinghaAnirban005/karpenter/metrics
karpenter: Visualization of Karpenter Metrics (Prometheus)
2 parents 2c2d661 + fdf8319 commit 81ab3d6

File tree

9 files changed

+686
-1
lines changed

9 files changed

+686
-1
lines changed

prometheus/src/components/Chart/GenericMetricsChart/GenericMetricsChart.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ export function GenericMetricsChart(props: GenericMetricsChartProps) {
248248
);
249249
}
250250

251-
function CustomToggleButton({
251+
export function CustomToggleButton({
252252
label,
253253
icon,
254254
value,
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import { Icon } from '@iconify/react';
2+
import { Loader, SectionBox } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
3+
import { useCluster } from '@kinvolk/headlamp-plugin/lib/k8s';
4+
import Alert from '@mui/material/Alert';
5+
import Box from '@mui/material/Box';
6+
import Button from '@mui/material/Button';
7+
import ListSubheader from '@mui/material/ListSubheader';
8+
import MenuItem from '@mui/material/MenuItem';
9+
import Paper from '@mui/material/Paper';
10+
import Select from '@mui/material/Select';
11+
import { useTheme } from '@mui/material/styles';
12+
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
13+
import { useEffect, useState } from 'react';
14+
import {
15+
getConfigStore,
16+
getPrometheusInterval,
17+
getPrometheusPrefix,
18+
getPrometheusResolution,
19+
getPrometheusSubPath,
20+
} from '../../../util';
21+
import { createTickTimestampFormatter } from '../../../util';
22+
import { PrometheusNotFoundBanner } from '../common';
23+
import { CustomToggleButton } from '../GenericMetricsChart/GenericMetricsChart';
24+
25+
interface ChartConfig {
26+
key: string;
27+
label: string;
28+
icon: string;
29+
queries: Record<string, string>;
30+
component: React.ComponentType<any>;
31+
}
32+
33+
interface KarpenterChartProps {
34+
chartConfigs: ChartConfig[];
35+
defaultChart?: string;
36+
}
37+
38+
export function KarpenterChart(props: KarpenterChartProps) {
39+
enum prometheusState {
40+
UNKNOWN,
41+
LOADING,
42+
ERROR,
43+
INSTALLED,
44+
}
45+
46+
const cluster = useCluster();
47+
const configStore = getConfigStore();
48+
const useClusterConfig = configStore.useConfig();
49+
const clusterConfig = useClusterConfig();
50+
const theme = useTheme();
51+
52+
const [refresh, setRefresh] = useState<boolean>(true);
53+
const [prometheusPrefix, setPrometheusPrefix] = useState<string | null>(null);
54+
const [state, setState] = useState<prometheusState>(prometheusState.LOADING);
55+
const [isVisible, setIsVisible] = useState<boolean>(false);
56+
57+
useEffect(() => {
58+
const isEnabled = clusterConfig?.[cluster]?.isMetricsEnabled || false;
59+
setIsVisible(isEnabled);
60+
61+
if (!isEnabled) {
62+
setState(prometheusState.UNKNOWN);
63+
setPrometheusPrefix(null);
64+
return;
65+
}
66+
67+
setState(prometheusState.LOADING);
68+
(async () => {
69+
try {
70+
const prefix = await getPrometheusPrefix(cluster);
71+
if (prefix) {
72+
setPrometheusPrefix(prefix);
73+
setState(prometheusState.INSTALLED);
74+
} else {
75+
setState(prometheusState.UNKNOWN);
76+
}
77+
} catch (e) {
78+
console.error('Error checking Prometheus installation:', e);
79+
setState(prometheusState.ERROR);
80+
}
81+
})();
82+
}, [clusterConfig, cluster]);
83+
84+
const interval = getPrometheusInterval(cluster);
85+
const graphResolution = getPrometheusResolution(cluster);
86+
const subPath = getPrometheusSubPath(cluster);
87+
88+
const [timespan, setTimespan] = useState(interval ?? '1h');
89+
const [resolution, setResolution] = useState(graphResolution ?? 'medium');
90+
const [selectedChart, setSelectedChart] = useState<string>(
91+
props.defaultChart || props.chartConfigs[0]?.key || ''
92+
);
93+
94+
if (!isVisible || props.chartConfigs.length === 0) {
95+
return null;
96+
}
97+
98+
const handleChartVariantChange = (event: React.MouseEvent<HTMLButtonElement>) => {
99+
setSelectedChart(event.currentTarget.value);
100+
};
101+
102+
const NodePoolTooltip = props => {
103+
const { active, payload, label } = props;
104+
if (active && payload && payload.length) {
105+
const formatter = createTickTimestampFormatter(timespan);
106+
return (
107+
<div
108+
style={{
109+
backgroundColor: theme.palette.background.paper,
110+
border: `1px solid ${theme.palette.divider}`,
111+
padding: theme.spacing(1),
112+
borderRadius: theme.shape.borderRadius,
113+
}}
114+
>
115+
<p>{`Time: ${formatter(label)}`}</p>
116+
{payload.map((p, i) => (
117+
<p key={i} style={{ color: p.color }}>
118+
{`${p.name}: ${p.value}`}
119+
</p>
120+
))}
121+
</div>
122+
);
123+
}
124+
return null;
125+
};
126+
127+
const currentChartConfig = props.chartConfigs.find(config => config.key === selectedChart);
128+
129+
return (
130+
<SectionBox>
131+
<Paper variant="outlined" sx={{ p: 1 }}>
132+
{state === prometheusState.INSTALLED && (
133+
<>
134+
<Box display="flex" gap={1} justifyContent="space-between" mb={2}>
135+
<Box display="flex" gap={1}>
136+
<ToggleButtonGroup
137+
onChange={handleChartVariantChange}
138+
size="small"
139+
aria-label="metric chooser"
140+
value={selectedChart}
141+
exclusive
142+
>
143+
{props.chartConfigs.map(config => (
144+
<CustomToggleButton
145+
key={config.key}
146+
label={config.label}
147+
value={config.key}
148+
icon={config.icon}
149+
/>
150+
))}
151+
</ToggleButtonGroup>
152+
</Box>
153+
<Box display="flex" gap={1}>
154+
<Button
155+
variant="outlined"
156+
size="small"
157+
onClick={() => {
158+
setRefresh(refresh => !refresh);
159+
}}
160+
startIcon={<Icon icon={refresh ? 'mdi:pause' : 'mdi:play'} />}
161+
sx={{ filter: 'grayscale(1.0)' }}
162+
>
163+
{refresh ? 'Pause' : 'Resume'}
164+
</Button>
165+
<Box>
166+
<Select
167+
variant="outlined"
168+
size="small"
169+
name="Time"
170+
value={timespan}
171+
onChange={e => setTimespan(e.target.value)}
172+
>
173+
<MenuItem value={'10m'}>10 minutes</MenuItem>
174+
<MenuItem value={'30m'}>30 minutes</MenuItem>
175+
<MenuItem value={'1h'}>1 hour</MenuItem>
176+
<MenuItem value={'3h'}>3 hours</MenuItem>
177+
<MenuItem value={'6h'}>6 hours</MenuItem>
178+
<MenuItem value={'12h'}>12 hours</MenuItem>
179+
<MenuItem value={'24h'}>24 hours</MenuItem>
180+
<MenuItem value={'48h'}>48 hours</MenuItem>
181+
<MenuItem value={'today'}>Today</MenuItem>
182+
<MenuItem value={'yesterday'}>Yesterday</MenuItem>
183+
<MenuItem value={'week'}>Week</MenuItem>
184+
<MenuItem value={'lastweek'}>Last week</MenuItem>
185+
<MenuItem value={'7d'}>7 days</MenuItem>
186+
<MenuItem value={'14d'}>14 days</MenuItem>
187+
</Select>
188+
</Box>
189+
<Box>
190+
<Select
191+
variant="outlined"
192+
size="small"
193+
name="Time"
194+
value={resolution}
195+
onChange={e => setResolution(e.target.value)}
196+
>
197+
<ListSubheader>Automatic resolution</ListSubheader>
198+
<MenuItem value="low">Low res.</MenuItem>
199+
<MenuItem value="medium">Medium res.</MenuItem>
200+
<MenuItem value="high">High res.</MenuItem>
201+
202+
<ListSubheader>Fixed resolution</ListSubheader>
203+
<MenuItem value="10s">10s</MenuItem>
204+
<MenuItem value="30s">30s</MenuItem>
205+
<MenuItem value="1m">1m</MenuItem>
206+
<MenuItem value="5m">5m</MenuItem>
207+
<MenuItem value="15m">15m</MenuItem>
208+
<MenuItem value="1h">1h</MenuItem>
209+
</Select>
210+
</Box>
211+
</Box>
212+
</Box>
213+
214+
<Box
215+
sx={{
216+
height: '400px',
217+
border: `1px solid ${theme.palette.divider}`,
218+
borderRadius: 1,
219+
p: 2,
220+
}}
221+
>
222+
{currentChartConfig && (
223+
<currentChartConfig.component
224+
refresh={refresh}
225+
prometheusPrefix={prometheusPrefix}
226+
resolution={resolution}
227+
subPath={subPath}
228+
timespan={timespan}
229+
NodePoolTooltip={NodePoolTooltip}
230+
{...currentChartConfig.queries}
231+
/>
232+
)}
233+
</Box>
234+
</>
235+
)}
236+
237+
{state === prometheusState.LOADING ? (
238+
<Box m={2}>
239+
<Loader title="Loading Prometheus Info" />
240+
</Box>
241+
) : state === prometheusState.ERROR ? (
242+
<Box m={2}>
243+
<Alert severity="warning">Error fetching Prometheus Info</Alert>
244+
</Box>
245+
) : state === prometheusState.UNKNOWN ? (
246+
<PrometheusNotFoundBanner />
247+
) : null}
248+
</Paper>
249+
</SectionBox>
250+
);
251+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { green } from '@mui/material/colors';
2+
import { alpha, useTheme } from '@mui/material/styles';
3+
import { fetchMetrics } from '../../../request';
4+
import { createDataProcessor, createTickTimestampFormatter } from '../../../util';
5+
import Chart from '../Chart/Chart';
6+
7+
interface KarpenterDisruptionChartProps {
8+
refresh: boolean;
9+
prometheusPrefix: string;
10+
resolution: string;
11+
subPath: string;
12+
activeNodesQuery: string;
13+
timespan: string;
14+
NodePoolTooltip;
15+
}
16+
17+
export const KarpenterDisruptionChart = (props: KarpenterDisruptionChartProps) => {
18+
const xTickFormatter = createTickTimestampFormatter(props.timespan);
19+
const theme = useTheme();
20+
21+
const plots = [
22+
{
23+
query: props.activeNodesQuery,
24+
name: 'Allowed Disruptions',
25+
strokeColor: alpha(green[600], 0.8),
26+
fillColor: alpha(green[400], 0.1),
27+
dataProcessor: createDataProcessor(0),
28+
},
29+
];
30+
31+
const xAxisProps = {
32+
dataKey: 'timestamp',
33+
tickLine: false,
34+
tick: props => {
35+
const value = xTickFormatter(props.payload.value);
36+
return (
37+
value !== '' && (
38+
<g
39+
transform={`translate(${props.x},${props.y})`}
40+
fill={theme.palette.chartStyles.labelColor}
41+
>
42+
<text x={0} y={10} dy={0} textAnchor="middle">
43+
{value}
44+
</text>
45+
</g>
46+
)
47+
);
48+
},
49+
};
50+
51+
return (
52+
<Chart
53+
plots={plots}
54+
xAxisProps={xAxisProps}
55+
yAxisProps={{ domain: [0, 'auto'], width: 60 }}
56+
CustomTooltip={props.NodePoolTooltip}
57+
fetchMetrics={fetchMetrics}
58+
autoRefresh={props.refresh}
59+
prometheusPrefix={props.prometheusPrefix}
60+
interval={props.timespan}
61+
resolution={props.resolution}
62+
subPath={props.subPath}
63+
/>
64+
);
65+
};

0 commit comments

Comments
 (0)