Start working on /alerts page, factor out rule definition display

Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
Julius Volz 2024-02-23 17:37:56 +01:00
parent 65cc7b058e
commit 128b6461e9
7 changed files with 372 additions and 160 deletions

View file

@ -46,6 +46,14 @@ interface RuleGroup {
lastEvaluation: string;
}
type AlertingRuleGroup = Omit<RuleGroup, "rules"> & {
rules: AlertingRule[];
};
export interface RulesMap {
groups: RuleGroup[];
}
export interface AlertingRulesMap {
groups: AlertingRuleGroup[];
}

View file

@ -0,0 +1,49 @@
.statsBadge {
background-color: light-dark(
var(--mantine-color-gray-1),
var(--mantine-color-gray-9)
);
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-5));
}
.labelBadge {
background-color: light-dark(
var(--mantine-color-gray-1),
var(--mantine-color-gray-9)
);
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-5));
}
.healthOk {
background-color: light-dark(
var(--mantine-color-green-1),
var(--mantine-color-green-9)
);
color: light-dark(var(--mantine-color-green-9), var(--mantine-color-green-1));
}
.healthErr {
background-color: light-dark(
var(--mantine-color-red-1),
darken(var(--mantine-color-red-9), 0.25)
);
color: light-dark(var(--mantine-color-red-9), var(--mantine-color-red-1));
}
.healthWarn {
background-color: light-dark(
var(--mantine-color-yellow-1),
var(--mantine-color-yellow-9)
);
color: light-dark(
var(--mantine-color-yellow-9),
var(--mantine-color-yellow-1)
);
}
.healthUnknown {
background-color: light-dark(
var(--mantine-color-gray-1),
var(--mantine-color-gray-9)
);
}

View file

@ -4,42 +4,3 @@
var(--mantine-color-gray-9)
);
}
.statsBadge {
background-color: light-dark(
var(--mantine-color-gray-1),
var(--mantine-color-gray-9)
);
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-5));
}
.labelBadge {
background-color: light-dark(
var(--mantine-color-gray-1),
var(--mantine-color-gray-9)
);
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-5));
}
.healthOk {
background-color: light-dark(
var(--mantine-color-green-1),
var(--mantine-color-green-9)
);
color: light-dark(var(--mantine-color-green-9), var(--mantine-color-green-1));
}
.healthErr {
background-color: light-dark(
var(--mantine-color-red-1),
var(--mantine-color-red-9)
);
color: light-dark(var(--mantine-color-red-9), var(--mantine-color-red-1));
}
.healthUnknown {
background-color: light-dark(
var(--mantine-color-gray-1),
var(--mantine-color-gray-9)
);
}

View file

@ -109,10 +109,14 @@ export const humanizeDuration = (milliseconds: number): string => {
return "0s";
};
export const formatRelative = (startStr: string, end: number): string => {
export const formatRelative = (
startStr: string,
end: number,
suffix: string = " ago"
): string => {
const start = parseTime(startStr);
if (start < 0) {
return "Never";
return "never";
}
return humanizeDuration(end - start) + " ago";
return humanizeDuration(end - start) + suffix;
};

View file

@ -1,3 +1,187 @@
import {
Card,
Group,
Table,
Text,
Accordion,
Badge,
Tooltip,
Box,
Switch,
} from "@mantine/core";
import { useSuspenseAPIQuery } from "../api/api";
import { AlertingRulesMap } from "../api/response-types/rules";
import badgeClasses from "../badge.module.css";
import RuleDefinition from "../rule-definition";
import { formatRelative, now } from "../lib/time-format";
import { Fragment, useState } from "react";
export default function Alerts() {
return <>Alerts page</>;
const { data } = useSuspenseAPIQuery<AlertingRulesMap>(`/rules?type=alert`);
const [showAnnotations, setShowAnnotations] = useState(false);
const ruleStatsCount = {
inactive: 0,
pending: 0,
firing: 0,
};
data.data.groups.forEach((el) =>
el.rules.forEach((r) => ruleStatsCount[r.state]++)
);
return (
<>
<Switch
checked={showAnnotations}
label="Show annotations"
onChange={(event) => setShowAnnotations(event.currentTarget.checked)}
mb="md"
/>
{data.data.groups.map((g, i) => (
<Card
shadow="xs"
withBorder
radius="md"
p="md"
mb="md"
key={i} // TODO: Find a stable and definitely unique key.
>
<Group mb="md" mt="xs" ml="xs" justify="space-between">
<Group align="baseline">
<Text fz="xl" fw={600} c="var(--mantine-primary-color-filled)">
{g.name}
</Text>
<Text fz="sm" c="gray.6">
{g.file}
</Text>
</Group>
</Group>
<Accordion multiple variant="separated">
{g.rules.map((r, j) => {
const numFiring = r.alerts.filter(
(a) => a.state === "firing"
).length;
const numPending = r.alerts.filter(
(a) => a.state === "pending"
).length;
return (
<Accordion.Item key={j} value={j.toString()}>
<Accordion.Control>
<Group wrap="nowrap" justify="space-between" mr="lg">
<Text>{r.name}</Text>
<Group gap="xs">
{numFiring > 0 && (
<Badge className={badgeClasses.healthErr}>
firing ({numFiring})
</Badge>
)}
{numPending > 0 && (
<Badge className={badgeClasses.healthWarn}>
pending ({numPending})
</Badge>
)}
{/* {numFiring === 0 && numPending === 0 && (
<Badge className={badgeClasses.healthOk}>
inactive
</Badge>
)} */}
</Group>
</Group>
</Accordion.Control>
<Accordion.Panel>
<RuleDefinition rule={r} />
{r.alerts.length > 0 && (
<Table mt="lg">
<Table.Thead>
<Table.Tr>
<Table.Th>Alert labels</Table.Th>
<Table.Th>State</Table.Th>
<Table.Th>Active Since</Table.Th>
<Table.Th>Value</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{r.type === "alerting" &&
r.alerts.map((a, k) => (
<Fragment key={k}>
<Table.Tr>
<Table.Td>
<Group gap="xs">
{Object.entries(a.labels).map(
([k, v]) => {
return (
<Badge
variant="light"
className={
badgeClasses.labelBadge
}
styles={{
label: {
textTransform: "none",
},
}}
key={k}
>
{/* TODO: Proper quote escaping */}
{k}="{v}"
</Badge>
);
}
)}
</Group>
</Table.Td>
<Table.Td>
<Badge
className={
a.state === "firing"
? badgeClasses.healthErr
: badgeClasses.healthWarn
}
>
{a.state}
</Badge>
</Table.Td>
<Table.Td>
<Tooltip label={a.activeAt}>
<Box>
{formatRelative(a.activeAt, now(), "")}
</Box>
</Tooltip>
</Table.Td>
<Table.Td>{a.value}</Table.Td>
</Table.Tr>
{showAnnotations && (
<Table.Tr>
<Table.Td colSpan={4}>
<Table mt="md" mb="xl">
<Table.Tbody>
{Object.entries(a.annotations).map(
([k, v]) => (
<Table.Tr key={k}>
<Table.Th>{k}</Table.Th>
<Table.Td>{v}</Table.Td>
</Table.Tr>
)
)}
</Table.Tbody>
</Table>
</Table.Td>
</Table.Tr>
)}
</Fragment>
))}
</Table.Tbody>
</Table>
)}
</Accordion.Panel>
</Accordion.Item>
);
})}
</Accordion>
</Card>
))}
</>
);
}

View file

@ -1,72 +1,45 @@
import {
Alert,
Badge,
Card,
Group,
Table,
Text,
Tooltip,
useComputedColorScheme,
} from "@mantine/core";
import { Alert, Badge, Card, Group, Table, Text, Tooltip } from "@mantine/core";
// import { useQuery } from "react-query";
import {
formatDuration,
formatRelative,
humanizeDuration,
now,
} from "../lib/time-format";
import { formatRelative, humanizeDuration, now } from "../lib/time-format";
import {
IconAlertTriangle,
IconBell,
IconClockPause,
IconClockPlay,
IconDatabaseImport,
IconHourglass,
IconRefresh,
IconRepeat,
} from "@tabler/icons-react";
import CodeMirror, { EditorView } from "@uiw/react-codemirror";
import { useSuspenseAPIQuery } from "../api/api";
import { RulesMap } from "../api/response-types/rules";
import { syntaxHighlighting } from "@codemirror/language";
import {
baseTheme,
darkPromqlHighlighter,
lightTheme,
promqlHighlighter,
} from "../codemirror/theme";
import { PromQLExtension } from "@prometheus-io/codemirror-promql";
import classes from "../codebox.module.css";
import badgeClasses from "../badge.module.css";
import RuleDefinition from "../rule-definition";
const healthBadgeClass = (state: string) => {
switch (state) {
case "ok":
return classes.healthOk;
return badgeClasses.healthOk;
case "err":
return classes.healthErr;
return badgeClasses.healthErr;
case "unknown":
return classes.healthUnknown;
return badgeClasses.healthUnknown;
default:
return "orange";
}
};
const promqlExtension = new PromQLExtension();
export default function Rules() {
const { data } = useSuspenseAPIQuery<RulesMap>(`/rules`);
const theme = useComputedColorScheme();
return (
<>
{data.data.groups.map((g) => (
{data.data.groups.map((g, i) => (
<Card
shadow="xs"
withBorder
radius="md"
p="md"
mb="md"
key={g.name + ";" + g.file}
key={i} // TODO: Find a stable and definitely unique key.
>
<Group mb="md" mt="xs" ml="xs" justify="space-between">
<Group align="baseline">
@ -81,7 +54,7 @@ export default function Rules() {
<Tooltip label="Last group evaluation" withArrow>
<Badge
variant="light"
className={classes.statsBadge}
className={badgeClasses.statsBadge}
styles={{ label: { textTransform: "none" } }}
leftSection={<IconRefresh size={12} />}
>
@ -91,7 +64,7 @@ export default function Rules() {
<Tooltip label="Duration of last group evaluation" withArrow>
<Badge
variant="light"
className={classes.statsBadge}
className={badgeClasses.statsBadge}
styles={{ label: { textTransform: "none" } }}
leftSection={<IconHourglass size={12} />}
>
@ -101,7 +74,7 @@ export default function Rules() {
<Tooltip label="Group evaluation interval" withArrow>
<Badge
variant="light"
className={classes.statsBadge}
className={badgeClasses.statsBadge}
styles={{ label: { textTransform: "none" } }}
leftSection={<IconRepeat size={12} />}
>
@ -113,8 +86,9 @@ export default function Rules() {
<Table>
<Table.Tbody>
{g.rules.map((r) => (
// TODO: Find a stable and definitely unique key.
<Table.Tr key={r.name}>
<Table.Td p="md" valign="top">
<Table.Td p="md" py="xl" valign="top">
<Group gap="xs" wrap="nowrap">
{r.type === "alerting" ? (
<IconBell size={14} />
@ -134,7 +108,7 @@ export default function Rules() {
<Tooltip label="Last rule evaluation" withArrow>
<Badge
variant="light"
className={classes.statsBadge}
className={badgeClasses.statsBadge}
styles={{ label: { textTransform: "none" } }}
leftSection={<IconRefresh size={12} />}
>
@ -148,7 +122,7 @@ export default function Rules() {
>
<Badge
variant="light"
className={classes.statsBadge}
className={badgeClasses.statsBadge}
styles={{ label: { textTransform: "none" } }}
leftSection={<IconHourglass size={12} />}
>
@ -160,31 +134,8 @@ export default function Rules() {
</Group>
</Group>
</Table.Td>
<Table.Td p="md">
<Card
p="xs"
className={classes.codebox}
radius="sm"
shadow="none"
>
<CodeMirror
basicSetup={false}
value={r.query}
editable={false}
extensions={[
baseTheme,
lightTheme,
syntaxHighlighting(
theme === "light"
? promqlHighlighter
: darkPromqlHighlighter
),
promqlExtension.asExtension(),
EditorView.lineWrapping,
]}
/>
</Card>
<Table.Td p="md" py="xl">
<RuleDefinition rule={r} />
{r.lastError && (
<Alert
color="red"
@ -195,56 +146,6 @@ export default function Rules() {
<strong>Error:</strong> {r.lastError}
</Alert>
)}
{r.type === "alerting" && (
<Group mt="md" gap="xs">
{r.duration && (
<Badge
variant="light"
styles={{ label: { textTransform: "none" } }}
leftSection={<IconClockPause size={12} />}
>
for: {formatDuration(r.duration * 1000)}
</Badge>
)}
{r.keepFiringFor && (
<Badge
variant="light"
styles={{ label: { textTransform: "none" } }}
leftSection={<IconClockPlay size={12} />}
>
keep_firing_for: {formatDuration(r.duration * 1000)}
</Badge>
)}
</Group>
)}
{r.labels && Object.keys(r.labels).length > 0 && (
<Group mt="md" gap="xs">
{Object.entries(r.labels).map(([k, v]) => (
<Badge
variant="light"
className={classes.labelBadge}
styles={{ label: { textTransform: "none" } }}
key={k}
>
{k}: {v}
</Badge>
))}
</Group>
)}
{/* {Object.keys(r.annotations).length > 0 && (
<Group mt="md" gap="xs">
{Object.entries(r.annotations).map(([k, v]) => (
<Badge
variant="light"
color="orange.9"
styles={{ label: { textTransform: "none" } }}
key={k}
>
{k}: {v}
</Badge>
))}
</Group>
)} */}
</Table.Td>
</Table.Tr>
))}

View file

@ -0,0 +1,105 @@
import {
Alert,
Badge,
Card,
Group,
useComputedColorScheme,
} from "@mantine/core";
import {
IconAlertTriangle,
IconClockPause,
IconClockPlay,
} from "@tabler/icons-react";
import { FC } from "react";
import { formatDuration } from "./lib/time-format";
import codeboxClasses from "./codebox.module.css";
import badgeClasses from "./badge.module.css";
import { Rule } from "./api/response-types/rules";
import CodeMirror, { EditorView } from "@uiw/react-codemirror";
import { syntaxHighlighting } from "@codemirror/language";
import {
baseTheme,
darkPromqlHighlighter,
lightTheme,
promqlHighlighter,
} from "./codemirror/theme";
import { PromQLExtension } from "@prometheus-io/codemirror-promql";
const promqlExtension = new PromQLExtension();
const RuleDefinition: FC<{ rule: Rule }> = ({ rule }) => {
const theme = useComputedColorScheme();
return (
<>
<Card p="xs" className={codeboxClasses.codebox} radius="sm" shadow="none">
<CodeMirror
basicSetup={false}
value={rule.query}
editable={false}
extensions={[
baseTheme,
lightTheme,
syntaxHighlighting(
theme === "light" ? promqlHighlighter : darkPromqlHighlighter
),
promqlExtension.asExtension(),
EditorView.lineWrapping,
]}
/>
</Card>
{rule.type === "alerting" && (
<Group mt="md" gap="xs">
{rule.duration && (
<Badge
variant="light"
styles={{ label: { textTransform: "none" } }}
leftSection={<IconClockPause size={12} />}
>
for: {formatDuration(rule.duration * 1000)}
</Badge>
)}
{rule.keepFiringFor && (
<Badge
variant="light"
styles={{ label: { textTransform: "none" } }}
leftSection={<IconClockPlay size={12} />}
>
keep_firing_for: {formatDuration(rule.duration * 1000)}
</Badge>
)}
</Group>
)}
{rule.labels && Object.keys(rule.labels).length > 0 && (
<Group mt="md" gap="xs">
{Object.entries(rule.labels).map(([k, v]) => (
<Badge
variant="light"
className={badgeClasses.labelBadge}
styles={{ label: { textTransform: "none" } }}
key={k}
>
{k}: {v}
</Badge>
))}
</Group>
)}
{/* {Object.keys(r.annotations).length > 0 && (
<Group mt="md" gap="xs">
{Object.entries(r.annotations).map(([k, v]) => (
<Badge
variant="light"
color="orange.9"
styles={{ label: { textTransform: "none" } }}
key={k}
>
{k}: {v}
</Badge>
))}
</Group>
)} */}
</>
);
};
export default RuleDefinition;