mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-23 12:31:07 +01:00
vmui: add lists of top queries (#3065)
* feat: add lists of top queries * fix: change the field label * refactor: add handlers for readability * app/vmselect: `make vmui-update` * docs: document `top queries` tab Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
parent
defced2599
commit
9541ef2e9e
12
README.md
12
README.md
@ -260,7 +260,10 @@ Prometheus doesn't drop data during VictoriaMetrics restart. See [this article](
|
||||
|
||||
VictoriaMetrics provides UI for query troubleshooting and exploration. The UI is available at `http://victoriametrics:8428/vmui`.
|
||||
The UI allows exploring query results via graphs and tables.
|
||||
It also provides the ability to [explore cardinality](#cardinality-explorer) and to [investigate query traces](#query-tracing).
|
||||
It also provides the following features:
|
||||
- [cardinality explorer](#cardinality-explorer)
|
||||
- [query tracer](#query-tracing)
|
||||
- [top queries explorer](#top-queries)
|
||||
|
||||
Graphs in vmui support scrolling and zooming:
|
||||
|
||||
@ -280,6 +283,13 @@ VMUI allows investigating correlations between two queries on the same graph. Ju
|
||||
|
||||
See the [example VMUI at VictoriaMetrics playground](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/prometheus/graph/?g0.expr=100%20*%20sum(rate(process_cpu_seconds_total))%20by%20(job)&g0.range_input=1d).
|
||||
|
||||
## Top queries
|
||||
|
||||
[VMUI](#vmui) provides `top queries` tab, which can help determining the following query types:
|
||||
|
||||
* the most frequently executed queries;
|
||||
* queries with the biggest average execution duration;
|
||||
* queries that took the most summary time for execution.
|
||||
|
||||
## Cardinality explorer
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "./static/css/main.9b22c3e0.css",
|
||||
"main.js": "./static/js/main.b8df40e9.js",
|
||||
"main.js": "./static/js/main.79f7bbc2.js",
|
||||
"static/js/27.939f971b.chunk.js": "./static/js/27.939f971b.chunk.js",
|
||||
"index.html": "./index.html"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.9b22c3e0.css",
|
||||
"static/js/main.b8df40e9.js"
|
||||
"static/js/main.79f7bbc2.js"
|
||||
]
|
||||
}
|
@ -1 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="VM-UI is a metric explorer for Victoria Metrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/><script src="./dashboards/index.js" type="module"></script><script defer="defer" src="./static/js/main.b8df40e9.js"></script><link href="./static/css/main.9b22c3e0.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="VM-UI is a metric explorer for Victoria Metrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/><script src="./dashboards/index.js" type="module"></script><script defer="defer" src="./static/js/main.79f7bbc2.js"></script><link href="./static/css/main.9b22c3e0.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
2
app/vmselect/vmui/static/js/main.79f7bbc2.js
Normal file
2
app/vmselect/vmui/static/js/main.79f7bbc2.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -5,6 +5,7 @@ import {StateProvider} from "./state/common/StateContext";
|
||||
import {AuthStateProvider} from "./state/auth/AuthStateContext";
|
||||
import {GraphStateProvider} from "./state/graph/GraphStateContext";
|
||||
import {CardinalityStateProvider} from "./state/cardinality/CardinalityStateContext";
|
||||
import {TopQueriesStateProvider} from "./state/topQueries/TopQueriesStateContext";
|
||||
import THEME from "./theme/theme";
|
||||
import { ThemeProvider, StyledEngineProvider } from "@mui/material/styles";
|
||||
import CssBaseline from "@mui/material/CssBaseline";
|
||||
@ -16,6 +17,7 @@ import CustomPanel from "./components/CustomPanel/CustomPanel";
|
||||
import HomeLayout from "./components/Home/HomeLayout";
|
||||
import DashboardsLayout from "./components/PredefinedPanels/DashboardsLayout";
|
||||
import CardinalityPanel from "./components/CardinalityPanel/CardinalityPanel";
|
||||
import TopQueries from "./components/TopQueries/TopQueries";
|
||||
|
||||
|
||||
const App: FC = () => {
|
||||
@ -30,15 +32,18 @@ const App: FC = () => {
|
||||
<AuthStateProvider> {/* Auth related info - optionally persisted to Local Storage */}
|
||||
<GraphStateProvider> {/* Graph settings */}
|
||||
<CardinalityStateProvider> {/* Cardinality settings */}
|
||||
<SnackbarProvider> {/* Display various snackbars */}
|
||||
<Routes>
|
||||
<Route path={"/"} element={<HomeLayout/>}>
|
||||
<Route path={router.home} element={<CustomPanel/>}/>
|
||||
<Route path={router.dashboards} element={<DashboardsLayout/>}/>
|
||||
<Route path={router.cardinality} element={<CardinalityPanel/>} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</SnackbarProvider>
|
||||
<TopQueriesStateProvider> {/* Top Queries settings */}
|
||||
<SnackbarProvider> {/* Display various snackbars */}
|
||||
<Routes>
|
||||
<Route path={"/"} element={<HomeLayout/>}>
|
||||
<Route path={router.home} element={<CustomPanel/>}/>
|
||||
<Route path={router.dashboards} element={<DashboardsLayout/>}/>
|
||||
<Route path={router.cardinality} element={<CardinalityPanel/>} />
|
||||
<Route path={router.topQueries} element={<TopQueries/>} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</SnackbarProvider>
|
||||
</TopQueriesStateProvider>
|
||||
</CardinalityStateProvider>
|
||||
</GraphStateProvider>
|
||||
</AuthStateProvider>
|
||||
|
3
app/vmui/packages/vmui/src/api/top-queries.ts
Normal file
3
app/vmui/packages/vmui/src/api/top-queries.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const getTopQueries = (server: string, topN: number | null, maxLifetime?: string) => (
|
||||
`${server}/api/v1/status/top_queries?topN=${topN || ""}&maxLifetime=${maxLifetime || ""}`
|
||||
);
|
@ -3,9 +3,10 @@ import {InstantMetricResult} from "../../../api/types";
|
||||
import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import {useSnack} from "../../../contexts/Snackbar";
|
||||
import {TopQuery} from "../../../types";
|
||||
|
||||
export interface JsonViewProps {
|
||||
data: InstantMetricResult[];
|
||||
data: InstantMetricResult[] | TopQuery[];
|
||||
}
|
||||
|
||||
const JsonView: FC<JsonViewProps> = ({data}) => {
|
||||
|
@ -61,8 +61,26 @@ const Header: FC = () => {
|
||||
const {date} = useCardinalityState();
|
||||
const cardinalityDispatch = useCardinalityDispatch();
|
||||
|
||||
const {search, pathname} = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const {search, pathname} = useLocation();
|
||||
const routes = [
|
||||
{
|
||||
label: "Custom panel",
|
||||
value: router.home,
|
||||
},
|
||||
{
|
||||
label: "Dashboards",
|
||||
value: router.dashboards,
|
||||
},
|
||||
{
|
||||
label: "Cardinality",
|
||||
value: router.cardinality,
|
||||
},
|
||||
{
|
||||
label: "Top queries",
|
||||
value: router.topQueries,
|
||||
}
|
||||
];
|
||||
|
||||
const [activeMenu, setActiveMenu] = useState(pathname);
|
||||
|
||||
@ -102,13 +120,15 @@ const Header: FC = () => {
|
||||
<Box sx={{ml: 8}}>
|
||||
<Tabs value={activeMenu} textColor="inherit" TabIndicatorProps={{style: {background: "white"}}}
|
||||
onChange={(e, val) => setActiveMenu(val)}>
|
||||
<Tab label="Custom panel" value={router.home} component={RouterLink} to={`${router.home}${search}`}/>
|
||||
<Tab label="Dashboards" value={router.dashboards} component={RouterLink} to={`${router.dashboards}${search}`}/>
|
||||
<Tab
|
||||
label="Cardinality"
|
||||
value={router.cardinality}
|
||||
component={RouterLink}
|
||||
to={`${router.cardinality}${search}`}/>
|
||||
{routes.map(r => (
|
||||
<Tab
|
||||
key={`${r.label}_${r.value}`}
|
||||
label={r.label}
|
||||
value={r.value}
|
||||
component={RouterLink}
|
||||
to={`${r.value}${search}`}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
</Box>
|
||||
<Box display="flex" gap={1} alignItems="center" ml="auto" mr={0}>
|
||||
|
@ -78,7 +78,7 @@ const PredefinedDashboard: FC<PredefinedDashboardProps> = ({index, title, panels
|
||||
|
||||
return <Accordion defaultExpanded={!index} sx={{boxShadow: "none"}}>
|
||||
<AccordionSummary
|
||||
sx={{px: 3, bgcolor: "rgba(227, 242, 253, 0.6)"}}
|
||||
sx={{px: 3, bgcolor: "primary.light"}}
|
||||
aria-controls={`panel${index}-content`}
|
||||
id={`panel${index}-header`}
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
|
148
app/vmui/packages/vmui/src/components/TopQueries/TopQueries.tsx
Normal file
148
app/vmui/packages/vmui/src/components/TopQueries/TopQueries.tsx
Normal file
@ -0,0 +1,148 @@
|
||||
import React, {ChangeEvent, FC, useEffect, useMemo, KeyboardEvent} from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import {useFetchTopQueries} from "../../hooks/useFetchTopQueries";
|
||||
import Spinner from "../common/Spinner";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import TopQueryPanel from "./TopQueryPanel/TopQueryPanel";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import {useTopQueriesDispatch, useTopQueriesState} from "../../state/topQueries/TopQueriesStateContext";
|
||||
import {formatPrettyNumber} from "../../utils/uplot/helpers";
|
||||
import {isSupportedDuration} from "../../utils/time";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import PlayCircleOutlineIcon from "@mui/icons-material/PlayCircleOutline";
|
||||
import dayjs from "dayjs";
|
||||
import {TopQueryStats} from "../../types";
|
||||
|
||||
const exampleDuration = "30ms, 15s, 3d4h, 1y2w";
|
||||
|
||||
const TopQueries: FC = () => {
|
||||
const {data, error, loading} = useFetchTopQueries();
|
||||
const {topN, maxLifetime} = useTopQueriesState();
|
||||
const topQueriesDispatch = useTopQueriesDispatch();
|
||||
|
||||
const invalidTopN = useMemo(() => !!topN && topN < 1, [topN]);
|
||||
|
||||
const maxLifetimeValid = useMemo(() => {
|
||||
const durItems = maxLifetime.trim().split(" ");
|
||||
const durObject = durItems.reduce((prev, curr) => {
|
||||
const dur = isSupportedDuration(curr);
|
||||
return dur ? {...prev, ...dur} : {...prev};
|
||||
}, {});
|
||||
const delta = dayjs.duration(durObject).asMilliseconds();
|
||||
return !!delta;
|
||||
}, [maxLifetime]);
|
||||
|
||||
const getQueryStatsTitle = (key: keyof TopQueryStats) => {
|
||||
if (!data) return key;
|
||||
const value = data[key];
|
||||
if (typeof value === "number") return formatPrettyNumber(value);
|
||||
return value || key;
|
||||
};
|
||||
|
||||
const onTopNChange = (e: ChangeEvent<HTMLTextAreaElement|HTMLInputElement>) => {
|
||||
topQueriesDispatch({type: "SET_TOP_N", payload: +e.target.value});
|
||||
};
|
||||
|
||||
const onMaxLifetimeChange = (e: ChangeEvent<HTMLTextAreaElement|HTMLInputElement>) => {
|
||||
topQueriesDispatch({type: "SET_MAX_LIFE_TIME", payload: e.target.value});
|
||||
};
|
||||
|
||||
const onApplyQuery = () => {
|
||||
topQueriesDispatch({type: "SET_RUN_QUERY"});
|
||||
};
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Enter") onApplyQuery();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
if (!topN) topQueriesDispatch({type: "SET_TOP_N", payload: +data.topN});
|
||||
if (!maxLifetime) topQueriesDispatch({type: "SET_MAX_LIFE_TIME", payload: data.maxLifetime});
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<Box p={4} style={{minHeight: "calc(100vh - 64px)"}}>
|
||||
{loading && <Spinner isLoading={true} height={"100%"}/>}
|
||||
|
||||
<Box boxShadow="rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;" p={4} pb={2} m={-4} mb={4}>
|
||||
<Box display={"flex"} alignItems={"flex"} mb={2}>
|
||||
<Box mr={2} flexGrow={1}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Max lifetime"
|
||||
size="medium"
|
||||
variant="outlined"
|
||||
value={maxLifetime}
|
||||
error={!maxLifetimeValid}
|
||||
helperText={!maxLifetimeValid ? "Invalid duration value" : `For example ${exampleDuration}`}
|
||||
onChange={onMaxLifetimeChange}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</Box>
|
||||
<Box mr={2}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Number of returned queries"
|
||||
type="number"
|
||||
size="medium"
|
||||
variant="outlined"
|
||||
value={topN || ""}
|
||||
error={invalidTopN}
|
||||
helperText={invalidTopN ? "Number must be bigger than zero" : " "}
|
||||
onChange={onTopNChange}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Tooltip title="Apply">
|
||||
<IconButton onClick={onApplyQuery} sx={{height: "49px", width: "49px"}}>
|
||||
<PlayCircleOutlineIcon/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography variant="body1" pt={2}>
|
||||
VictoriaMetrics tracks the last
|
||||
<Tooltip arrow title={<Typography>search.queryStats.lastQueriesCount</Typography>}>
|
||||
<b style={{cursor: "default"}}>
|
||||
{getQueryStatsTitle("search.queryStats.lastQueriesCount")}
|
||||
</b>
|
||||
</Tooltip>
|
||||
queries with durations at least
|
||||
<Tooltip arrow title={<Typography>search.queryStats.minQueryDuration</Typography>}>
|
||||
<b style={{cursor: "default"}}>
|
||||
{getQueryStatsTitle("search.queryStats.minQueryDuration")}
|
||||
</b>
|
||||
</Tooltip>
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{error && <Alert color="error" severity="error" sx={{whiteSpace: "pre-wrap", my: 2}}>{error}</Alert>}
|
||||
|
||||
{data && (<>
|
||||
<Box>
|
||||
<TopQueryPanel
|
||||
rows={data.topByCount}
|
||||
title={"Top by count"}
|
||||
description={"The most frequently executed queries"}
|
||||
/>
|
||||
<TopQueryPanel
|
||||
rows={data.topByAvgDuration}
|
||||
title={"Top by avg duration"}
|
||||
description={"Queries that took the most average execution time"}
|
||||
/>
|
||||
<TopQueryPanel
|
||||
rows={data.topBySumDuration}
|
||||
title={"Top by sum duration"}
|
||||
description={"Queries that took the highest summary execution time"}
|
||||
/>
|
||||
</Box>
|
||||
</>)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopQueries;
|
@ -0,0 +1,93 @@
|
||||
import React, {FC, useState} from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import {TopQuery} from "../../../types";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import InfoIcon from "@mui/icons-material/Info";
|
||||
import Accordion from "@mui/material/Accordion";
|
||||
import AccordionSummary from "@mui/material/AccordionSummary";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import AccordionDetails from "@mui/material/AccordionDetails";
|
||||
import Tabs from "@mui/material/Tabs";
|
||||
import Tab from "@mui/material/Tab";
|
||||
import TableChartIcon from "@mui/icons-material/TableChart";
|
||||
import CodeIcon from "@mui/icons-material/Code";
|
||||
import TopQueryTable from "../TopQueryTable/TopQueryTable";
|
||||
import JsonView from "../../CustomPanel/Views/JsonView";
|
||||
|
||||
interface TopQueryPanelProps {
|
||||
rows: TopQuery[],
|
||||
title: string,
|
||||
description: string
|
||||
}
|
||||
const tabs = ["table", "JSON"];
|
||||
|
||||
const TopQueryPanel: FC<TopQueryPanelProps> = ({rows, title, description}) => {
|
||||
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
|
||||
const onChangeTab = (e: React.SyntheticEvent, val: number) => {
|
||||
setActiveTab(val);
|
||||
};
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
defaultExpanded={true}
|
||||
sx={{
|
||||
mt: 2,
|
||||
border: "1px solid",
|
||||
borderColor: "primary.light",
|
||||
boxShadow: "none",
|
||||
"&:before": {
|
||||
opacity: 0
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AccordionSummary
|
||||
sx={{
|
||||
p: 2,
|
||||
bgcolor: "primary.light",
|
||||
minHeight: "64px",
|
||||
".MuiAccordionSummary-content": { display: "flex", alignItems: "center" },
|
||||
}}
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
>
|
||||
<Tooltip arrow title={description}>
|
||||
<InfoIcon color="info" sx={{mr: 1}}/>
|
||||
</Tooltip>
|
||||
<Typography variant="h6" component="h6">
|
||||
{title}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{p: 0}}>
|
||||
<Box width={"100%"}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={onChangeTab}
|
||||
sx={{minHeight: "0", marginBottom: "-1px"}}
|
||||
>
|
||||
{tabs.map((title: string, i: number) =>
|
||||
<Tab
|
||||
key={title}
|
||||
label={title}
|
||||
aria-controls={`tabpanel-${i}`}
|
||||
id={`${title}_${i}`}
|
||||
iconPosition={"start"}
|
||||
sx={{minHeight: "41px"}}
|
||||
icon={ i === 0 ? <TableChartIcon /> : <CodeIcon /> } />
|
||||
)}
|
||||
</Tabs>
|
||||
</Box>
|
||||
{activeTab === 0 && <TopQueryTable rows={rows}/>}
|
||||
{activeTab === 1 && <Box m={2}><JsonView data={rows} /></Box>}
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
<Box >
|
||||
|
||||
</Box>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopQueryPanel;
|
@ -0,0 +1,77 @@
|
||||
import React, {FC, useState, useMemo} from "react";
|
||||
import TableContainer from "@mui/material/TableContainer";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableHead from "@mui/material/TableHead";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableSortLabel from "@mui/material/TableSortLabel";
|
||||
import {TopQuery} from "../../../types";
|
||||
import {getComparator, stableSort} from "../../Table/helpers";
|
||||
|
||||
interface TopQueryTableProps {
|
||||
rows: TopQuery[],
|
||||
}
|
||||
type ColumnKeys = keyof TopQuery;
|
||||
const columns: ColumnKeys[] = ["query", "timeRangeSeconds", "avgDurationSeconds", "count", "accountID", "projectID"];
|
||||
|
||||
const TopQueryTable:FC<TopQueryTableProps> = ({rows}) => {
|
||||
|
||||
const [orderBy, setOrderBy] = useState("count");
|
||||
const [orderDir, setOrderDir] = useState<"asc" | "desc">("desc");
|
||||
|
||||
const sortedList = useMemo(() => stableSort(rows as [], getComparator(orderDir, orderBy)),
|
||||
[rows, orderBy, orderDir]);
|
||||
|
||||
const onSortHandler = (key: string) => {
|
||||
setOrderDir((prev) => prev === "asc" && orderBy === key ? "desc" : "asc");
|
||||
setOrderBy(key);
|
||||
};
|
||||
|
||||
const createSortHandler = (col: string) => () => {
|
||||
onSortHandler(col);
|
||||
};
|
||||
|
||||
return <TableContainer>
|
||||
<Table
|
||||
sx={{minWidth: 750}}
|
||||
aria-labelledby="tableTitle"
|
||||
>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columns.map((col) => (
|
||||
<TableCell key={col} sx={{ borderBottomColor: "primary.light" }}>
|
||||
<TableSortLabel
|
||||
active={orderBy === col}
|
||||
direction={orderDir}
|
||||
id={col}
|
||||
onClick={createSortHandler(col)}
|
||||
>
|
||||
{col}
|
||||
</TableSortLabel>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{sortedList.map((row, rowIndex) => (
|
||||
<TableRow key={rowIndex}>
|
||||
{columns.map((col) => (
|
||||
<TableCell
|
||||
key={col}
|
||||
sx={{
|
||||
borderBottom: rowIndex === rows.length - 1 ? "none" : "",
|
||||
borderBottomColor: "primary.light"
|
||||
}}
|
||||
>
|
||||
{row[col] || "-"}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>;
|
||||
};
|
||||
|
||||
export default TopQueryTable;
|
48
app/vmui/packages/vmui/src/hooks/useFetchTopQueries.ts
Normal file
48
app/vmui/packages/vmui/src/hooks/useFetchTopQueries.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import {ErrorTypes} from "../types";
|
||||
import {getAppModeEnable, getAppModeParams} from "../utils/app-mode";
|
||||
import {useAppState} from "../state/common/StateContext";
|
||||
import {useMemo} from "preact/compat";
|
||||
import {getTopQueries} from "../api/top-queries";
|
||||
import {TopQueriesData} from "../types";
|
||||
import {useTopQueriesState} from "../state/topQueries/TopQueriesStateContext";
|
||||
|
||||
export const useFetchTopQueries = () => {
|
||||
const appModeEnable = getAppModeEnable();
|
||||
const {serverURL: appServerUrl} = getAppModeParams();
|
||||
const {serverUrl} = useAppState();
|
||||
const {topN, maxLifetime, runQuery} = useTopQueriesState();
|
||||
|
||||
const [data, setData] = useState<TopQueriesData | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<ErrorTypes | string>();
|
||||
|
||||
const server = useMemo(() => appModeEnable ? appServerUrl : serverUrl,
|
||||
[appModeEnable, serverUrl, appServerUrl]);
|
||||
const fetchUrl = useMemo(() => getTopQueries(server, topN, maxLifetime), [server, topN, maxLifetime]);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(fetchUrl);
|
||||
const resp = await response.json();
|
||||
setData(response.ok ? resp : null);
|
||||
setError(String(resp.error || ""));
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.name !== "AbortError") {
|
||||
setError(`${e.name}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [runQuery]);
|
||||
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
loading
|
||||
};
|
||||
};
|
@ -2,6 +2,7 @@ const router = {
|
||||
home: "/",
|
||||
dashboards: "/dashboards",
|
||||
cardinality: "/cardinality",
|
||||
topQueries: "/top-queries",
|
||||
};
|
||||
|
||||
export interface RouterOptions {
|
||||
|
@ -19,14 +19,14 @@ export const initialPrepopulatedState = Object.entries(initialState)
|
||||
}), {}) as AppState;
|
||||
|
||||
export const StateProvider: FC = ({children}) => {
|
||||
const location = useLocation();
|
||||
const {pathname} = useLocation();
|
||||
|
||||
const [state, dispatch] = useReducer(reducer, initialPrepopulatedState);
|
||||
|
||||
useEffect(() => {
|
||||
if (location.pathname === router.cardinality) return;
|
||||
if (pathname !== router.dashboards || pathname !== router.home) return;
|
||||
setQueryStringValue(state as unknown as Record<string, unknown>);
|
||||
}, [state, location]);
|
||||
}, [state, pathname]);
|
||||
|
||||
const contextValue = useMemo(() => {
|
||||
return { state, dispatch };
|
||||
|
@ -0,0 +1,35 @@
|
||||
import React, {createContext, FC, useContext, useEffect, useMemo, useReducer} from "preact/compat";
|
||||
import {Action, TopQueriesState, initialState, reducer} from "./reducer";
|
||||
import {Dispatch} from "react";
|
||||
import {useLocation} from "react-router-dom";
|
||||
import {setQueryStringValue} from "../../utils/query-string";
|
||||
import router from "../../router";
|
||||
|
||||
type TopQueriesStateContextType = { state: TopQueriesState, dispatch: Dispatch<Action> };
|
||||
|
||||
export const TopQueriesStateContext = createContext<TopQueriesStateContextType>({} as TopQueriesStateContextType);
|
||||
|
||||
export const useTopQueriesState = (): TopQueriesState => useContext(TopQueriesStateContext).state;
|
||||
export const useTopQueriesDispatch = (): Dispatch<Action> => useContext(TopQueriesStateContext).dispatch;
|
||||
|
||||
export const TopQueriesStateProvider: FC = ({children}) => {
|
||||
const location = useLocation();
|
||||
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
useEffect(() => {
|
||||
if (location.pathname !== router.topQueries) return;
|
||||
setQueryStringValue(state as unknown as Record<string, unknown>);
|
||||
}, [state, location]);
|
||||
|
||||
const contextValue = useMemo(() => {
|
||||
return { state, dispatch };
|
||||
}, [state, dispatch]);
|
||||
|
||||
|
||||
return <TopQueriesStateContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</TopQueriesStateContext.Provider>;
|
||||
};
|
||||
|
||||
|
41
app/vmui/packages/vmui/src/state/topQueries/reducer.ts
Normal file
41
app/vmui/packages/vmui/src/state/topQueries/reducer.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import {getQueryStringValue} from "../../utils/query-string";
|
||||
|
||||
export interface TopQueriesState {
|
||||
maxLifetime: string,
|
||||
topN: number | null,
|
||||
runQuery: number
|
||||
}
|
||||
|
||||
export type Action =
|
||||
| { type: "SET_TOP_N", payload: number | null }
|
||||
| { type: "SET_MAX_LIFE_TIME", payload: string }
|
||||
| { type: "SET_RUN_QUERY" }
|
||||
|
||||
|
||||
export const initialState: TopQueriesState = {
|
||||
topN: getQueryStringValue("topN", null) as number,
|
||||
maxLifetime: getQueryStringValue("maxLifetime", "") as string,
|
||||
runQuery: 0
|
||||
};
|
||||
|
||||
export function reducer(state: TopQueriesState, action: Action): TopQueriesState {
|
||||
switch (action.type) {
|
||||
case "SET_TOP_N":
|
||||
return {
|
||||
...state,
|
||||
topN: action.payload
|
||||
};
|
||||
case "SET_MAX_LIFE_TIME":
|
||||
return {
|
||||
...state,
|
||||
maxLifetime: action.payload
|
||||
};
|
||||
case "SET_RUN_QUERY":
|
||||
return {
|
||||
...state,
|
||||
runQuery: state.runQuery + 1
|
||||
};
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
}
|
@ -3,7 +3,8 @@ import {createTheme} from "@mui/material/styles";
|
||||
const THEME = createTheme({
|
||||
palette: {
|
||||
primary: {
|
||||
main: "#3F51B5"
|
||||
main: "#3F51B5",
|
||||
light: "#e3f2fd"
|
||||
},
|
||||
secondary: {
|
||||
main: "#F50057"
|
||||
@ -17,7 +18,7 @@ const THEME = createTheme({
|
||||
styleOverrides: {
|
||||
root: {
|
||||
position: "absolute",
|
||||
top: "36px",
|
||||
bottom: "-16px",
|
||||
left: "2px",
|
||||
margin: 0,
|
||||
}
|
||||
@ -110,4 +111,4 @@ const THEME = createTheme({
|
||||
}
|
||||
});
|
||||
|
||||
export default THEME;
|
||||
export default THEME;
|
||||
|
@ -69,3 +69,25 @@ export interface RelativeTimeOption {
|
||||
title: string,
|
||||
isDefault?: boolean,
|
||||
}
|
||||
|
||||
export interface TopQuery {
|
||||
accountID: number
|
||||
avgDurationSeconds: number
|
||||
count: number
|
||||
projectID: number
|
||||
query: string
|
||||
timeRangeSeconds: number
|
||||
}
|
||||
|
||||
export interface TopQueryStats {
|
||||
"search.queryStats.lastQueriesCount": number
|
||||
"search.queryStats.minQueryDuration": string
|
||||
}
|
||||
|
||||
export interface TopQueriesData extends TopQueryStats{
|
||||
maxLifetime: string
|
||||
topN: string
|
||||
topByAvgDuration: TopQuery[]
|
||||
topByCount: TopQuery[]
|
||||
topBySumDuration: TopQuery[]
|
||||
}
|
||||
|
@ -19,6 +19,10 @@ const stateToUrlParams = {
|
||||
"match": "match[]",
|
||||
"extraLabel": "extra_label",
|
||||
"focusLabel": "focusLabel"
|
||||
},
|
||||
[router.topQueries]: {
|
||||
"topN": "topN",
|
||||
"maxLifetime": "maxLifetime",
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -20,6 +20,7 @@ The following tip changes can be tested by building VictoriaMetrics components f
|
||||
* FEATURE: check the correctess of raw sample timestamps stored on disk when reading them. This reduces the probability of possible silent corruption of the data stored on disk. This should help [this](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2998) and [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3011).
|
||||
* FEATURE: set the `start` arg to `end - 5 minutes` if isn't passed explicitly to [/api/v1/labels](https://docs.victoriametrics.com/url-examples.html#apiv1labels) and [/api/v1/label/.../values](https://docs.victoriametrics.com/url-examples.html#apiv1labelvalues). See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3052).
|
||||
* FEATURE: [vmctl](https://docs.victoriametrics.com/vmctl.html): add `vm-native-step-interval` command line flag for `vm-native` mode. New option allows splitting the import process into chunks by time interval. This helps migrating data sets with high churn rate and provides better control over the process. See [feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2733).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add `top queries` tab, which shows various stats for recently executed queries. See [these docs](https://docs.victoriametrics.com/#top-queries) and [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2707).
|
||||
|
||||
* BUGFIX: [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html): properly calculate `rate_over_sum(m[d])` as `sum_over_time(m[d])/d`. Previously the `sum_over_time(m[d])` could be improperly divided by smaller than `d` time range. See [rate_over_sum() docs](https://docs.victoriametrics.com/MetricsQL.html#rate_over_sum) and [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3045).
|
||||
* BUGFIX: [VictoriaMetrics cluster](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html): properly calculate query results at `vmselect`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3067). The issue has been introduced in [v1.81.0](https://docs.victoriametrics.com/CHANGELOG.html#v1810).
|
||||
|
@ -260,7 +260,10 @@ Prometheus doesn't drop data during VictoriaMetrics restart. See [this article](
|
||||
|
||||
VictoriaMetrics provides UI for query troubleshooting and exploration. The UI is available at `http://victoriametrics:8428/vmui`.
|
||||
The UI allows exploring query results via graphs and tables.
|
||||
It also provides the ability to [explore cardinality](#cardinality-explorer) and to [investigate query traces](#query-tracing).
|
||||
It also provides the following features:
|
||||
- [cardinality explorer](#cardinality-explorer)
|
||||
- [query tracer](#query-tracing)
|
||||
- [top queries explorer](#top-queries)
|
||||
|
||||
Graphs in vmui support scrolling and zooming:
|
||||
|
||||
@ -280,6 +283,13 @@ VMUI allows investigating correlations between two queries on the same graph. Ju
|
||||
|
||||
See the [example VMUI at VictoriaMetrics playground](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/prometheus/graph/?g0.expr=100%20*%20sum(rate(process_cpu_seconds_total))%20by%20(job)&g0.range_input=1d).
|
||||
|
||||
## Top queries
|
||||
|
||||
[VMUI](#vmui) provides `top queries` tab, which can help determining the following query types:
|
||||
|
||||
* the most frequently executed queries;
|
||||
* queries with the biggest average execution duration;
|
||||
* queries that took the most summary time for execution.
|
||||
|
||||
## Cardinality explorer
|
||||
|
||||
|
@ -264,7 +264,10 @@ Prometheus doesn't drop data during VictoriaMetrics restart. See [this article](
|
||||
|
||||
VictoriaMetrics provides UI for query troubleshooting and exploration. The UI is available at `http://victoriametrics:8428/vmui`.
|
||||
The UI allows exploring query results via graphs and tables.
|
||||
It also provides the ability to [explore cardinality](#cardinality-explorer) and to [investigate query traces](#query-tracing).
|
||||
It also provides the following features:
|
||||
- [cardinality explorer](#cardinality-explorer)
|
||||
- [query tracer](#query-tracing)
|
||||
- [top queries explorer](#top-queries)
|
||||
|
||||
Graphs in vmui support scrolling and zooming:
|
||||
|
||||
@ -284,6 +287,13 @@ VMUI allows investigating correlations between two queries on the same graph. Ju
|
||||
|
||||
See the [example VMUI at VictoriaMetrics playground](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/prometheus/graph/?g0.expr=100%20*%20sum(rate(process_cpu_seconds_total))%20by%20(job)&g0.range_input=1d).
|
||||
|
||||
## Top queries
|
||||
|
||||
[VMUI](#vmui) provides `top queries` tab, which can help determining the following query types:
|
||||
|
||||
* the most frequently executed queries;
|
||||
* queries with the biggest average execution duration;
|
||||
* queries that took the most summary time for execution.
|
||||
|
||||
## Cardinality explorer
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user