mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00: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
eb0b2ef3d9
commit
c8de98e03f
24 changed files with 552 additions and 32 deletions
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"files": {
|
"files": {
|
||||||
"main.css": "./static/css/main.9b22c3e0.css",
|
"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",
|
"static/js/27.939f971b.chunk.js": "./static/js/27.939f971b.chunk.js",
|
||||||
"index.html": "./index.html"
|
"index.html": "./index.html"
|
||||||
},
|
},
|
||||||
"entrypoints": [
|
"entrypoints": [
|
||||||
"static/css/main.9b22c3e0.css",
|
"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 {AuthStateProvider} from "./state/auth/AuthStateContext";
|
||||||
import {GraphStateProvider} from "./state/graph/GraphStateContext";
|
import {GraphStateProvider} from "./state/graph/GraphStateContext";
|
||||||
import {CardinalityStateProvider} from "./state/cardinality/CardinalityStateContext";
|
import {CardinalityStateProvider} from "./state/cardinality/CardinalityStateContext";
|
||||||
|
import {TopQueriesStateProvider} from "./state/topQueries/TopQueriesStateContext";
|
||||||
import THEME from "./theme/theme";
|
import THEME from "./theme/theme";
|
||||||
import { ThemeProvider, StyledEngineProvider } from "@mui/material/styles";
|
import { ThemeProvider, StyledEngineProvider } from "@mui/material/styles";
|
||||||
import CssBaseline from "@mui/material/CssBaseline";
|
import CssBaseline from "@mui/material/CssBaseline";
|
||||||
|
@ -16,6 +17,7 @@ import CustomPanel from "./components/CustomPanel/CustomPanel";
|
||||||
import HomeLayout from "./components/Home/HomeLayout";
|
import HomeLayout from "./components/Home/HomeLayout";
|
||||||
import DashboardsLayout from "./components/PredefinedPanels/DashboardsLayout";
|
import DashboardsLayout from "./components/PredefinedPanels/DashboardsLayout";
|
||||||
import CardinalityPanel from "./components/CardinalityPanel/CardinalityPanel";
|
import CardinalityPanel from "./components/CardinalityPanel/CardinalityPanel";
|
||||||
|
import TopQueries from "./components/TopQueries/TopQueries";
|
||||||
|
|
||||||
|
|
||||||
const App: FC = () => {
|
const App: FC = () => {
|
||||||
|
@ -30,15 +32,18 @@ const App: FC = () => {
|
||||||
<AuthStateProvider> {/* Auth related info - optionally persisted to Local Storage */}
|
<AuthStateProvider> {/* Auth related info - optionally persisted to Local Storage */}
|
||||||
<GraphStateProvider> {/* Graph settings */}
|
<GraphStateProvider> {/* Graph settings */}
|
||||||
<CardinalityStateProvider> {/* Cardinality settings */}
|
<CardinalityStateProvider> {/* Cardinality settings */}
|
||||||
|
<TopQueriesStateProvider> {/* Top Queries settings */}
|
||||||
<SnackbarProvider> {/* Display various snackbars */}
|
<SnackbarProvider> {/* Display various snackbars */}
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path={"/"} element={<HomeLayout/>}>
|
<Route path={"/"} element={<HomeLayout/>}>
|
||||||
<Route path={router.home} element={<CustomPanel/>}/>
|
<Route path={router.home} element={<CustomPanel/>}/>
|
||||||
<Route path={router.dashboards} element={<DashboardsLayout/>}/>
|
<Route path={router.dashboards} element={<DashboardsLayout/>}/>
|
||||||
<Route path={router.cardinality} element={<CardinalityPanel/>} />
|
<Route path={router.cardinality} element={<CardinalityPanel/>} />
|
||||||
|
<Route path={router.topQueries} element={<TopQueries/>} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</SnackbarProvider>
|
</SnackbarProvider>
|
||||||
|
</TopQueriesStateProvider>
|
||||||
</CardinalityStateProvider>
|
</CardinalityStateProvider>
|
||||||
</GraphStateProvider>
|
</GraphStateProvider>
|
||||||
</AuthStateProvider>
|
</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 Box from "@mui/material/Box";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import {useSnack} from "../../../contexts/Snackbar";
|
import {useSnack} from "../../../contexts/Snackbar";
|
||||||
|
import {TopQuery} from "../../../types";
|
||||||
|
|
||||||
export interface JsonViewProps {
|
export interface JsonViewProps {
|
||||||
data: InstantMetricResult[];
|
data: InstantMetricResult[] | TopQuery[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const JsonView: FC<JsonViewProps> = ({data}) => {
|
const JsonView: FC<JsonViewProps> = ({data}) => {
|
||||||
|
|
|
@ -61,8 +61,26 @@ const Header: FC = () => {
|
||||||
const {date} = useCardinalityState();
|
const {date} = useCardinalityState();
|
||||||
const cardinalityDispatch = useCardinalityDispatch();
|
const cardinalityDispatch = useCardinalityDispatch();
|
||||||
|
|
||||||
const {search, pathname} = useLocation();
|
|
||||||
const navigate = useNavigate();
|
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);
|
const [activeMenu, setActiveMenu] = useState(pathname);
|
||||||
|
|
||||||
|
@ -102,13 +120,15 @@ const Header: FC = () => {
|
||||||
<Box sx={{ml: 8}}>
|
<Box sx={{ml: 8}}>
|
||||||
<Tabs value={activeMenu} textColor="inherit" TabIndicatorProps={{style: {background: "white"}}}
|
<Tabs value={activeMenu} textColor="inherit" TabIndicatorProps={{style: {background: "white"}}}
|
||||||
onChange={(e, val) => setActiveMenu(val)}>
|
onChange={(e, val) => setActiveMenu(val)}>
|
||||||
<Tab label="Custom panel" value={router.home} component={RouterLink} to={`${router.home}${search}`}/>
|
{routes.map(r => (
|
||||||
<Tab label="Dashboards" value={router.dashboards} component={RouterLink} to={`${router.dashboards}${search}`}/>
|
|
||||||
<Tab
|
<Tab
|
||||||
label="Cardinality"
|
key={`${r.label}_${r.value}`}
|
||||||
value={router.cardinality}
|
label={r.label}
|
||||||
|
value={r.value}
|
||||||
component={RouterLink}
|
component={RouterLink}
|
||||||
to={`${router.cardinality}${search}`}/>
|
to={`${r.value}${search}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
<Box display="flex" gap={1} alignItems="center" ml="auto" mr={0}>
|
<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"}}>
|
return <Accordion defaultExpanded={!index} sx={{boxShadow: "none"}}>
|
||||||
<AccordionSummary
|
<AccordionSummary
|
||||||
sx={{px: 3, bgcolor: "rgba(227, 242, 253, 0.6)"}}
|
sx={{px: 3, bgcolor: "primary.light"}}
|
||||||
aria-controls={`panel${index}-content`}
|
aria-controls={`panel${index}-content`}
|
||||||
id={`panel${index}-header`}
|
id={`panel${index}-header`}
|
||||||
expandIcon={<ExpandMoreIcon />}
|
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: "/",
|
home: "/",
|
||||||
dashboards: "/dashboards",
|
dashboards: "/dashboards",
|
||||||
cardinality: "/cardinality",
|
cardinality: "/cardinality",
|
||||||
|
topQueries: "/top-queries",
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface RouterOptions {
|
export interface RouterOptions {
|
||||||
|
|
|
@ -19,14 +19,14 @@ export const initialPrepopulatedState = Object.entries(initialState)
|
||||||
}), {}) as AppState;
|
}), {}) as AppState;
|
||||||
|
|
||||||
export const StateProvider: FC = ({children}) => {
|
export const StateProvider: FC = ({children}) => {
|
||||||
const location = useLocation();
|
const {pathname} = useLocation();
|
||||||
|
|
||||||
const [state, dispatch] = useReducer(reducer, initialPrepopulatedState);
|
const [state, dispatch] = useReducer(reducer, initialPrepopulatedState);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (location.pathname === router.cardinality) return;
|
if (pathname !== router.dashboards || pathname !== router.home) return;
|
||||||
setQueryStringValue(state as unknown as Record<string, unknown>);
|
setQueryStringValue(state as unknown as Record<string, unknown>);
|
||||||
}, [state, location]);
|
}, [state, pathname]);
|
||||||
|
|
||||||
const contextValue = useMemo(() => {
|
const contextValue = useMemo(() => {
|
||||||
return { state, dispatch };
|
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({
|
const THEME = createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
primary: {
|
primary: {
|
||||||
main: "#3F51B5"
|
main: "#3F51B5",
|
||||||
|
light: "#e3f2fd"
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
main: "#F50057"
|
main: "#F50057"
|
||||||
|
@ -17,7 +18,7 @@ const THEME = createTheme({
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: {
|
root: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: "36px",
|
bottom: "-16px",
|
||||||
left: "2px",
|
left: "2px",
|
||||||
margin: 0,
|
margin: 0,
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,3 +69,25 @@ export interface RelativeTimeOption {
|
||||||
title: string,
|
title: string,
|
||||||
isDefault?: boolean,
|
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[]",
|
"match": "match[]",
|
||||||
"extraLabel": "extra_label",
|
"extraLabel": "extra_label",
|
||||||
"focusLabel": "focusLabel"
|
"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: 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: 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: [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: [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).
|
* 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`.
|
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.
|
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:
|
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).
|
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
|
## 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`.
|
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.
|
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:
|
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).
|
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
|
## Cardinality explorer
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue