app/vmui: add alert rule links to top queries table

When vmalert is enabled, fetch all alert rules on load and match their
queries against entries in the Top Queries table. If a query matches an
alert rule, display an "alert?" link in the time range column that
navigates directly to the first matching alert rule.

Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9493
Supersedes https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10787
This commit is contained in:
Max Kotliar
2026-05-13 16:39:22 +03:00
parent 5f5a2109e8
commit db1018cda3
4 changed files with 81 additions and 6 deletions

View File

@@ -10,7 +10,7 @@ import useDeviceDetect from "../../../hooks/useDeviceDetect";
export interface TopQueryColumn {
title?: string;
tooltip?: string;
tooltip?: ReactNode;
key: keyof TopQuery;
sortBy?: keyof TopQuery;
format?: (row: TopQuery) => ReactNode;

View File

@@ -0,0 +1,46 @@
import { useEffect, useMemo, useState } from "preact/compat";
import { useAppState } from "../../../state/common/StateContext";
import { Group } from "../../../types";
const getAllRulesUrl = (server: string): string =>
`${server}/vmalert/api/v1/rules?datasource_type=prometheus&group_limit=1000`;
type AlertRuleRef = { group_id: string; rule_id: string };
export const useFetchAlertQueries = (): Map<string, AlertRuleRef> => {
const { serverUrl, appConfig } = useAppState();
const [alertQueries, setAlertQueries] = useState<Map<string, AlertRuleRef>>(new Map());
const isEnabled = appConfig?.vmalert?.enabled ?? false;
const fetchUrl = useMemo(
() => (isEnabled ? getAllRulesUrl(serverUrl) : null),
[serverUrl, isEnabled],
);
useEffect(() => {
if (!fetchUrl) return;
const fetchData = async () => {
try {
const response = await fetch(fetchUrl);
if (!response.ok) return;
const resp = await response.json();
const groups = (resp?.data?.groups || []) as Group[];
const queries = new Map<string, AlertRuleRef>();
for (const group of groups) {
for (const rule of group.rules) {
if (rule.query && !queries.has(rule.query)) {
queries.set(rule.query, { group_id: group.id, rule_id: rule.id });
}
}
}
setAlertQueries(queries);
} catch (e) {
// silently ignore fetch errors
}
};
fetchData();
}, [fetchUrl]);
return alertQueries;
};

View File

@@ -1,13 +1,18 @@
import { useMemo } from "react";
import { useMemo, Fragment } from "react";
import { Link } from "react-router-dom";
import { TopQueryColumn } from "../TopQueryPanel/TopQueryPanel";
import { humanizeSeconds } from "../../../utils/time";
import { formatBytes } from "../../../utils/bytes";
import router from "../../../router";
type AlertRuleRef = { group_id: string; rule_id: string };
type UseTopQueriesColumns = {
maxLifetime: string;
alertQueries?: Map<string, AlertRuleRef>;
};
export const useTopQueriesColumns = ({ maxLifetime }: UseTopQueriesColumns) => {
export const useTopQueriesColumns = ({ maxLifetime, alertQueries }: UseTopQueriesColumns) => {
return useMemo(() => {
const queryCol: TopQueryColumn = {
key: "query"
@@ -17,7 +22,29 @@ export const useTopQueriesColumns = ({ maxLifetime }: UseTopQueriesColumns) => {
key: "timeRange",
sortBy: "timeRangeSeconds",
title: "range",
tooltip: "The time range between start and end of the query request. 'instant' means the query was executed at a single point in time without a time range"
tooltip: (
<Fragment>
The time range between start and end of the query request.<br/>
<br/>
&apos;instant&apos; means the query was executed at a single point in time without a time range.<br/>
<br/>
&apos;alert?&apos; means the query matches an alert rule the link goes to the first matching alert.
</Fragment>
),
format: (row) => {
const ref = alertQueries?.get(row.query);
if (ref) {
return (
<Link
to={`${router.rules}?group_id=${ref.group_id}&rule_id=${ref.rule_id}`}
className="vm-link vm-link_colored"
>
alert?
</Link>
);
}
return row.timeRange;
},
};
const countCol: TopQueryColumn = {
@@ -73,5 +100,5 @@ export const useTopQueriesColumns = ({ maxLifetime }: UseTopQueriesColumns) => {
topByCount,
topByAvgMemoryUsage,
};
}, [maxLifetime]);
}, [maxLifetime, alertQueries]);
};

View File

@@ -16,6 +16,7 @@ import useDeviceDetect from "../../hooks/useDeviceDetect";
import classNames from "classnames";
import useStateSearchParams from "../../hooks/useStateSearchParams";
import { useTopQueriesColumns } from "./hooks/useTopQueriesColumns";
import { useFetchAlertQueries } from "./hooks/useFetchAlertQueries";
const exampleDuration = "30ms, 15s, 3d4h, 1y2w";
@@ -24,7 +25,8 @@ const TopQueries: FC = () => {
const [topN, setTopN] = useStateSearchParams(10, "topN");
const [maxLifetime, setMaxLifetime] = useStateSearchParams("10m", "maxLifetime");
const columns = useTopQueriesColumns({ maxLifetime });
const alertQueries = useFetchAlertQueries();
const columns = useTopQueriesColumns({ maxLifetime, alertQueries });
const { data, error, loading, fetch } = useFetchTopQueries({ topN, maxLifetime });