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:
Yury Molodov 2022-09-08 21:43:37 +03:00 committed by Aliaksandr Valialkin
parent eb0b2ef3d9
commit c8de98e03f
No known key found for this signature in database
GPG key ID: A72BEC6CD3D0DED1
24 changed files with 552 additions and 32 deletions

View file

@ -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"
]
}

View file

@ -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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -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>

View 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 || ""}`
);

View file

@ -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}) => {

View file

@ -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}>

View file

@ -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 />}

View 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&nbsp;
<Tooltip arrow title={<Typography>search.queryStats.lastQueriesCount</Typography>}>
<b style={{cursor: "default"}}>
{getQueryStatsTitle("search.queryStats.lastQueriesCount")}
</b>
</Tooltip>
&nbsp;queries with durations at least&nbsp;
<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;

View file

@ -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;

View file

@ -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;

View 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
};
};

View file

@ -2,6 +2,7 @@ const router = {
home: "/",
dashboards: "/dashboards",
cardinality: "/cardinality",
topQueries: "/top-queries",
};
export interface RouterOptions {

View file

@ -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 };

View file

@ -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>;
};

View 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();
}
}

View file

@ -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;

View file

@ -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[]
}

View file

@ -19,6 +19,10 @@ const stateToUrlParams = {
"match": "match[]",
"extraLabel": "extra_label",
"focusLabel": "focusLabel"
},
[router.topQueries]: {
"topN": "topN",
"maxLifetime": "maxLifetime",
}
};

View file

@ -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).

View file

@ -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

View file

@ -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