vmui: added query tracing (#2748)

* vmui: added query tracing

* vmui: updated ui

* vmui: update tracing logic, fix bugs, disable tracing by default

* vmui: use empty message as props

* vmui: fixed ui, added delete for each tacing data, show query in header

* vmui: added timelines

* vmui: speedup render

* vmui: use memo for sorting

* vmui: use Trace model, remove unused functions, simplify part of code

* vmui: update recursive logic

* vmui: fix set query to header

* vmui: code cleanup, remove unused code

* vmui: remove unused type, rename component

* wip

* wip

Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
Dmytro Kozlov 2022-06-23 22:59:20 +03:00 committed by Aliaksandr Valialkin
parent ee5c502446
commit f28cbcc7b5
No known key found for this signature in database
GPG key ID: A72BEC6CD3D0DED1
19 changed files with 300 additions and 41 deletions

View file

@ -1,12 +1,12 @@
{
"files": {
"main.css": "./static/css/main.7e6d0c89.css",
"main.js": "./static/js/main.fdf5a65f.js",
"main.js": "./static/js/main.645fe611.js",
"static/js/27.939f971b.chunk.js": "./static/js/27.939f971b.chunk.js",
"index.html": "./index.html"
},
"entrypoints": [
"static/css/main.7e6d0c89.css",
"static/js/main.fdf5a65f.js"
"static/js/main.645fe611.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.fdf5a65f.js"></script><link href="./static/css/main.7e6d0c89.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.645fe611.js"></script><link href="./static/css/main.7e6d0c89.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

@ -1,9 +1,9 @@
import {TimeParams} from "../types";
export const getQueryRangeUrl = (server: string, query: string, period: TimeParams, nocache: boolean): string =>
`${server}/api/v1/query_range?query=${encodeURIComponent(query)}&start=${period.start}&end=${period.end}&step=${period.step}${nocache ? "&nocache=1" : ""}`;
export const getQueryRangeUrl = (server: string, query: string, period: TimeParams, nocache: boolean, queryTracing: boolean): string =>
`${server}/api/v1/query_range?query=${encodeURIComponent(query)}&start=${period.start}&end=${period.end}&step=${period.step}${nocache ? "&nocache=1" : ""}${queryTracing ? "&trace=1" : ""}`;
export const getQueryUrl = (server: string, query: string, period: TimeParams): string =>
`${server}/api/v1/query?query=${encodeURIComponent(query)}&start=${period.start}&end=${period.end}&step=${period.step}`;
export const getQueryUrl = (server: string, query: string, period: TimeParams, queryTracing: boolean): string =>
`${server}/api/v1/query?query=${encodeURIComponent(query)}&start=${period.start}&end=${period.end}&step=${period.step}${queryTracing ? "&trace=1" : ""}`;
export const getQueryOptions = (server: string) => `${server}/api/v1/label/__name__/values`;

View file

@ -14,10 +14,8 @@ export interface InstantMetricResult extends MetricBase {
value: [number, string]
}
export interface QueryRangeResponse {
status: string;
data: {
result: MetricResult[];
resultType: "matrix";
}
}
export interface TracingData {
message: string;
duration_msec: number;
children: TracingData[];
}

View file

@ -12,7 +12,7 @@ const AdditionalSettings: FC = () => {
const {customStep} = useGraphState();
const graphDispatch = useGraphDispatch();
const {queryControls: {autocomplete, nocache}, time: {period: {step}}} = useAppState();
const {queryControls: {autocomplete, nocache, isTracingEnabled}, time: {period: {step}}} = useAppState();
const dispatch = useAppDispatch();
const onChangeAutocomplete = () => {
@ -25,6 +25,11 @@ const AdditionalSettings: FC = () => {
saveToStorage("NO_CACHE", !nocache);
};
const onChangeQueryTracing = () => {
dispatch({type: "TOGGLE_QUERY_TRACING"});
saveToStorage("QUERY_TRACING", !isTracingEnabled);
};
return <Box display="flex" alignItems="center">
<Box>
<FormControlLabel label="Enable autocomplete"
@ -36,6 +41,11 @@ const AdditionalSettings: FC = () => {
control={<BasicSwitch checked={!nocache} onChange={onChangeCache}/>}
/>
</Box>
<Box ml={2}>
<FormControlLabel label="Enable query tracing"
control={<BasicSwitch checked={isTracingEnabled} onChange={onChangeQueryTracing} />}
/>
</Box>
<Box ml={2}>
<StepConfigurator defaultStep={step} customStepEnable={customStep.enable}
setStep={(value) => {
@ -48,4 +58,4 @@ const AdditionalSettings: FC = () => {
</Box>;
};
export default AdditionalSettings;
export default AdditionalSettings;

View file

@ -1,4 +1,4 @@
import React, {FC} from "preact/compat";
import React, {FC, useState, useEffect} from "preact/compat";
import Alert from "@mui/material/Alert";
import Box from "@mui/material/Box";
import GraphView from "./Views/GraphView";
@ -13,10 +13,13 @@ import {useGraphDispatch, useGraphState} from "../../state/graph/GraphStateConte
import {AxisRange} from "../../state/graph/reducer";
import Spinner from "../common/Spinner";
import {useFetchQueryOptions} from "../../hooks/useFetchQueryOptions";
import TracingsView from "./Views/TracingsView";
import Trace from "./Trace/Trace";
const CustomPanel: FC = () => {
const {displayType, time: {period}, query} = useAppState();
const [tracingsData, setTracingData] = useState<Trace[]>([]);
const {displayType, time: {period}, query, queryControls: {isTracingEnabled}} = useAppState();
const { customStep, yaxis } = useGraphState();
const dispatch = useAppDispatch();
@ -35,11 +38,26 @@ const CustomPanel: FC = () => {
};
const {queryOptions} = useFetchQueryOptions();
const {isLoading, liveData, graphData, error} = useFetchQuery({
const {isLoading, liveData, graphData, error, tracingData} = useFetchQuery({
visible: true,
customStep
});
const handleTraceDelete = (tracingData: Trace) => {
const updatedTracings = tracingsData.filter((data) => data.idValue !== tracingData.idValue);
setTracingData([...updatedTracings]);
};
useEffect(() => {
if (tracingData) {
setTracingData([...tracingsData, tracingData]);
}
}, [tracingData]);
useEffect(() => {
setTracingData([]);
}, [displayType]);
return (
<Box p={4} display="grid" gridTemplateRows="auto 1fr" style={{minHeight: "calc(100vh - 64px)"}}>
<QueryConfigurator error={error} queryOptions={queryOptions}/>
@ -49,18 +67,31 @@ const CustomPanel: FC = () => {
<Box display="grid" gridTemplateColumns="1fr auto" alignItems="center" mx={-4} px={4} mb={2}
borderBottom={1} borderColor="divider">
<DisplayTypeSwitch/>
{displayType === "chart" && <GraphSettings
yaxis={yaxis}
setYaxisLimits={setYaxisLimits}
toggleEnableLimits={toggleEnableLimits}
/>}
<Box display={"flex"}>
{displayType === "chart" && <GraphSettings
yaxis={yaxis}
setYaxisLimits={setYaxisLimits}
toggleEnableLimits={toggleEnableLimits}
/>}
</Box>
</Box>
{error && <Alert color="error" severity="error" sx={{whiteSpace: "pre-wrap", mt: 2}}>{error}</Alert>}
{graphData && period && (displayType === "chart") &&
{graphData && period && (displayType === "chart") && <>
{isTracingEnabled && <TracingsView
tracingsData={tracingsData}
onDeleteClick={handleTraceDelete}
/>}
<GraphView data={graphData} period={period} customStep={customStep} query={query} yaxis={yaxis}
setYaxisLimits={setYaxisLimits} setPeriod={setPeriod}/>}
setYaxisLimits={setYaxisLimits} setPeriod={setPeriod}/>
</>}
{liveData && (displayType === "code") && <JsonView data={liveData}/>}
{liveData && (displayType === "table") && <TableView data={liveData}/>}
{liveData && (displayType === "table") && <>
{isTracingEnabled && <TracingsView
tracingsData={tracingsData}
onDeleteClick={handleTraceDelete}
/>}
<TableView data={liveData}/>
</>}
</Box>}
</Box>
</Box>

View file

@ -0,0 +1,63 @@
import React, {FC} from "preact/compat";
import Box from "@mui/material/Box";
import ListItem from "@mui/material/ListItem";
import ListItemText from "@mui/material/ListItemText";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import ExpandLess from "@mui/icons-material/ExpandLess";
import AddCircleRoundedIcon from "@mui/icons-material/AddCircleRounded";
import Collapse from "@mui/material/Collapse";
import List from "@mui/material/List";
import {BorderLinearProgressWithLabel} from "../../BorderLineProgress/BorderLinearProgress";
import Trace from "../Trace/Trace";
interface RecursiveProps {
trace: Trace;
totalMsec: number;
openLevels: Record<number, boolean>;
onChange: (level: number) => void;
}
const NestedNav: FC<RecursiveProps> = ({ trace, openLevels, totalMsec, onChange}) => {
const handleListClick = (traceID: number) => () => onChange(traceID);
const hasChildren = trace.children && trace.children.length;
const progress = trace.duration / totalMsec * 100;
return (
<Box sx={{ bgcolor: "rgba(201, 227, 246, 0.4)" }}>
<ListItem onClick={handleListClick(trace.duration)} sx={!hasChildren ? {p:0, pl: 7} : {p:0}}>
<ListItemButton alignItems={"flex-start"} sx={{ pt: 0, pb: 0}}>
{hasChildren ? <ListItemIcon>
{openLevels[trace.duration] ?
<ExpandLess fontSize={"large"} color={"info"} /> :
<AddCircleRoundedIcon fontSize={"large"} color={"info"} />}
</ListItemIcon>: null}
<Box display="flex" flexDirection="column" flexGrow={0.5} sx={{ ml: 4, mr: 4, width: "100%" }}>
<ListItemText
primary={`duration: ${trace.duration} ms`}
secondary={trace.message}
/>
<ListItemText>
<BorderLinearProgressWithLabel variant="determinate" value={progress} />
</ListItemText>
</Box>
</ListItemButton>
</ListItem>
<>
<Collapse in={openLevels[trace.duration]} timeout="auto" unmountOnExit>
<List component="div" disablePadding sx={{ pl: 4 }}>
{hasChildren ?
trace.children.map((trace) => <NestedNav
key={trace.duration}
trace={trace}
openLevels={openLevels}
totalMsec={totalMsec}
onChange={onChange}
/>) : null}
</List>
</Collapse>
</>
</Box>
);
};
export default NestedNav;

View file

@ -0,0 +1,49 @@
import {TracingData} from "../../../api/types";
export default class Trace {
private readonly tracing: TracingData;
private readonly query: string;
private readonly id: number;
constructor(tracingData: TracingData, query: string) {
this.tracing = tracingData;
this.query = query;
this.id = new Date().getTime();
}
recursiveMap(oldArray: TracingData[], callback: (tr: TracingData) => Trace, newArray: Trace[]): Trace[] {
if (!oldArray) return [];
//base case: check if there are any items left in the original array to process
if (oldArray && oldArray.length <= 0){
//if all items have been processed return the new array
return newArray;
} else {
//destructure the first item from old array and put remaining in a separate array
const [item, ...theRest] = oldArray;
// create an array of the current new array and the result of the current item and the callback function
const interimArray = [...newArray, callback(item)];
// return a recursive call to to map to process the next item.
return this.recursiveMap(theRest, callback, interimArray);
}
}
createTrace(traceData: TracingData) {
return new Trace(traceData, "");
}
get queryValue(): string {
return this.query;
}
get idValue(): number {
return this.id;
}
get children(): Trace[] {
const arr: Trace[] = [];
return this.recursiveMap(this.tracing.children, this.createTrace, arr);
}
get message(): string {
return this.tracing.message;
}
get duration(): number {
return this.tracing.duration_msec;
}
}

View file

@ -0,0 +1,30 @@
import React, {FC, useState} from "preact/compat";
import List from "@mui/material/List";
import NestedNav from "../NestedNav/NestedNav";
import Trace from "../Trace/Trace";
interface TraceViewProps {
trace: Trace;
}
interface OpenLevels {
[x: number]: boolean
}
const TraceView: FC<TraceViewProps> = ({trace}) => {
const [openLevels, setOpenLevels] = useState({} as OpenLevels);
const handleClick = (level: number) => {
setOpenLevels((prevState:OpenLevels) => ({...prevState, [level]: !prevState[level]}));
};
return (<List sx={{ width: "100%" }} component="nav">
<NestedNav
trace={trace}
openLevels={openLevels}
totalMsec={trace.duration}
onChange={handleClick}
/>
</List>);
};
export default TraceView;

View file

@ -0,0 +1,40 @@
import React, {FC} from "preact/compat";
import Typography from "@mui/material/Typography";
import TraceView from "./TraceView";
import Alert from "@mui/material/Alert";
import RemoveCircleIcon from "@mui/icons-material/RemoveCircle";
import Button from "@mui/material/Button";
import Trace from "../Trace/Trace";
interface TraceViewProps {
tracingsData: Trace[];
onDeleteClick: (tracingData: Trace) => void;
}
const EMPTY_MESSAGE = "Please re-run the query to see results of the tracing";
const TracingsView: FC<TraceViewProps> = ({tracingsData, onDeleteClick}) => {
if (!tracingsData.length) {
return (
<Alert color={"info"} severity="info" sx={{whiteSpace: "pre-wrap", mt: 2}}>
{EMPTY_MESSAGE}
</Alert>
);
}
const handleDeleteClick = (tracingData: Trace) => () => {
onDeleteClick(tracingData);
};
return <>{tracingsData.map((tracingData) => <>
<Typography variant="h4" gutterBottom component="div">
{"Tracing for"} {tracingData.queryValue}
<Button onClick={handleDeleteClick(tracingData)}>
<RemoveCircleIcon fontSize={"large"} color={"error"} />
</Button>
</Typography>
<TraceView trace={tracingData} />
</>)}</>;
};
export default TracingsView;

View file

@ -1,7 +1,7 @@
import {useEffect, useMemo, useCallback, useState} from "preact/compat";
import {useCallback, useEffect, useMemo, useState} from "preact/compat";
import {getQueryRangeUrl, getQueryUrl} from "../api/query-range";
import {useAppState} from "../state/common/StateContext";
import {InstantMetricResult, MetricBase, MetricResult} from "../api/types";
import {InstantMetricResult, MetricBase, MetricResult, TracingData} from "../api/types";
import {isValidHttpUrl} from "../utils/url";
import {ErrorTypes} from "../types";
import {getAppModeEnable, getAppModeParams} from "../utils/app-mode";
@ -10,6 +10,7 @@ import {DisplayType} from "../components/CustomPanel/Configurator/DisplayTypeSwi
import {CustomStep} from "../state/graph/reducer";
import usePrevious from "./usePrevious";
import {arrayEquals} from "../utils/array";
import Trace from "../components/CustomPanel/Trace/Trace";
interface FetchQueryParams {
predefinedQuery?: string[]
@ -27,12 +28,14 @@ export const useFetchQuery = ({predefinedQuery, visible, display, customStep}: F
graphData?: MetricResult[],
liveData?: InstantMetricResult[],
error?: ErrorTypes | string,
tracingData?: Trace,
} => {
const {query, displayType, serverUrl, time: {period}, queryControls: {nocache}} = useAppState();
const {query, displayType, serverUrl, time: {period}, queryControls: {nocache, isTracingEnabled}} = useAppState();
const [isLoading, setIsLoading] = useState(false);
const [graphData, setGraphData] = useState<MetricResult[]>();
const [liveData, setLiveData] = useState<InstantMetricResult[]>();
const [tracingData, setTracingData] = useState<Trace>();
const [error, setError] = useState<ErrorTypes | string>();
const [fetchQueue, setFetchQueue] = useState<AbortController[]>([]);
@ -40,10 +43,22 @@ export const useFetchQuery = ({predefinedQuery, visible, display, customStep}: F
if (error) {
setGraphData(undefined);
setLiveData(undefined);
setTracingData(undefined);
}
}, [error]);
const fetchData = async (fetchUrl: string[], fetchQueue: AbortController[], displayType: DisplayType) => {
const updateTracingData = (tracing: TracingData, queries: string[]) => {
if (tracing) {
queries.forEach((query) => {
const {message} = tracing;
if (message.includes(query)) {
setTracingData(new Trace(tracing, query));
}
});
}
};
const fetchData = async (fetchUrl: string[], fetchQueue: AbortController[], displayType: DisplayType, query: string[]) => {
const controller = new AbortController();
setFetchQueue([...fetchQueue, controller]);
try {
@ -54,6 +69,7 @@ export const useFetchQuery = ({predefinedQuery, visible, display, customStep}: F
const resp = await response.json();
if (response.ok) {
setError(undefined);
updateTracingData(resp.trace, query);
tempData.push(...resp.data.result.map((d: MetricBase) => {
d.group = counter;
return d;
@ -87,8 +103,8 @@ export const useFetchQuery = ({predefinedQuery, visible, display, customStep}: F
const updatedPeriod = {...period};
if (customStep.enable) updatedPeriod.step = customStep.value;
return expr.filter(q => q.trim()).map(q => displayChart
? getQueryRangeUrl(server, q, updatedPeriod, nocache)
: getQueryUrl(server, q, updatedPeriod));
? getQueryRangeUrl(server, q, updatedPeriod, nocache, isTracingEnabled)
: getQueryUrl(server, q, updatedPeriod, isTracingEnabled));
} else {
setError(ErrorTypes.validServer);
}
@ -100,7 +116,8 @@ export const useFetchQuery = ({predefinedQuery, visible, display, customStep}: F
useEffect(() => {
if (!visible || (fetchUrl && prevFetchUrl && arrayEquals(fetchUrl, prevFetchUrl)) || !fetchUrl?.length) return;
setIsLoading(true);
throttledFetchData(fetchUrl, fetchQueue, (display || displayType));
const expr = predefinedQuery ?? query;
throttledFetchData(fetchUrl, fetchQueue, (display || displayType), expr);
}, [fetchUrl, visible]);
useEffect(() => {
@ -110,5 +127,5 @@ export const useFetchQuery = ({predefinedQuery, visible, display, customStep}: F
setFetchQueue(fetchQueue.filter(f => !f.signal.aborted));
}, [fetchQueue]);
return { fetchUrl, isLoading, graphData, liveData, error };
return {fetchUrl, isLoading, graphData, liveData, error, tracingData};
};

View file

@ -34,8 +34,9 @@ export interface AppState {
queryHistory: QueryHistory[],
queryControls: {
autoRefresh: boolean;
autocomplete: boolean,
nocache: boolean
autocomplete: boolean;
nocache: boolean;
isTracingEnabled: boolean;
}
}
@ -55,6 +56,7 @@ export type Action =
| { type: "TOGGLE_AUTOREFRESH"}
| { type: "TOGGLE_AUTOCOMPLETE"}
| { type: "NO_CACHE"}
| { type: "TOGGLE_QUERY_TRACING" }
const {duration, endInput, relativeTimeId} = getRelativeTime({
@ -79,6 +81,7 @@ export const initialState: AppState = {
autoRefresh: false,
autocomplete: getFromStorage("AUTOCOMPLETE") as boolean || false,
nocache: getFromStorage("NO_CACHE") as boolean || false,
isTracingEnabled: getFromStorage("QUERY_TRACING") as boolean || false,
}
};
@ -187,6 +190,14 @@ export function reducer(state: AppState, action: Action): AppState {
autocomplete: !state.queryControls.autocomplete
}
};
case "TOGGLE_QUERY_TRACING":
return {
...state,
queryControls: {
...state.queryControls,
isTracingEnabled: !state.queryControls.isTracingEnabled,
}
};
case "NO_CACHE":
return {
...state,

View file

@ -3,6 +3,7 @@ export type StorageKeys = "BASIC_AUTH_DATA"
| "AUTH_TYPE"
| "AUTOCOMPLETE"
| "NO_CACHE"
| "QUERY_TRACING"
export const saveToStorage = (key: StorageKeys, value: string | boolean | Record<string, unknown>): void => {
if (value) {

View file

@ -17,6 +17,7 @@ The following tip changes can be tested by building VictoriaMetrics components f
* FEATURE: add `-search.setLookbackToStep` command-line flag, which enables InfluxDB-like gap filling during querying. See [these docs](https://docs.victoriametrics.com/guides/migrate-from-influx.html) for details.
* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): add ability to specify additional HTTP headers to send to scrape targets via `headers` section in `scrape_configs`. This can be used when the scrape target requires custom authorization and authentication like in [this stackoverflow question](https://stackoverflow.com/questions/66032498/prometheus-scrape-metric-with-custom-header). For example, the following config instructs sending `My-Auth: top-secret` and `TenantID: FooBar` headers with each request to `http://host123:8080/metrics`:
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add an UI for [query tracing](https://docs.victoriametrics.com/#query-tracing). It can be enabled by clicking `enable query tracing` checkbox and re-running the query. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2703).
```yaml
scrape_configs:

View file

@ -243,7 +243,8 @@ Prometheus doesn't drop data during VictoriaMetrics restart. See [this article](
## 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. It also provides support for [cardinality explorer](#cardinality-explorer).
The UI allows exploring query results via graphs and tables.
It also provides the ability to [explore cardinality](#cardinality-explorer) and to [investigate query tracec](#query-tracing).
Graphs in vmui support scrolling and zooming:
@ -1470,6 +1471,7 @@ VictoriaMetrics provides an UI on top of `/api/v1/status/tsdb` - see [cardinalit
## Query tracing
VictoriaMetrics supports query tracing, which can be used for determining bottlenecks during query processing.
This is like `EXPLAIN ANALYZE` from Postgresql.
Query tracing can be enabled for a specific query by passing `trace=1` query arg.
In this case VictoriaMetrics puts query trace into `trace` field in the output JSON.
@ -1529,6 +1531,8 @@ All the durations and timestamps in traces are in milliseconds.
Query tracing is allowed by default. It can be denied by passing `-denyQueryTracing` command-line flag to VictoriaMetrics.
[VMUI](#vmui) provides an UI for query tracing - just click `Enable query tracing` checkbox and re-run the query in order to investigate its' trace.
## Cardinality limiter

View file

@ -247,7 +247,8 @@ Prometheus doesn't drop data during VictoriaMetrics restart. See [this article](
## 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. It also provides support for [cardinality explorer](#cardinality-explorer).
The UI allows exploring query results via graphs and tables.
It also provides the ability to [explore cardinality](#cardinality-explorer) and to [investigate query tracec](#query-tracing).
Graphs in vmui support scrolling and zooming:
@ -1474,6 +1475,7 @@ VictoriaMetrics provides an UI on top of `/api/v1/status/tsdb` - see [cardinalit
## Query tracing
VictoriaMetrics supports query tracing, which can be used for determining bottlenecks during query processing.
This is like `EXPLAIN ANALYZE` from Postgresql.
Query tracing can be enabled for a specific query by passing `trace=1` query arg.
In this case VictoriaMetrics puts query trace into `trace` field in the output JSON.
@ -1533,6 +1535,8 @@ All the durations and timestamps in traces are in milliseconds.
Query tracing is allowed by default. It can be denied by passing `-denyQueryTracing` command-line flag to VictoriaMetrics.
[VMUI](#vmui) provides an UI for query tracing - just click `Enable query tracing` checkbox and re-run the query in order to investigate its' trace.
## Cardinality limiter