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": { "files": {
"main.css": "./static/css/main.7e6d0c89.css", "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", "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.7e6d0c89.css", "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"; import {TimeParams} from "../types";
export const getQueryRangeUrl = (server: string, query: string, period: TimeParams, nocache: boolean): string => 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" : ""}`; `${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 => 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}`; `${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`; 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] value: [number, string]
} }
export interface QueryRangeResponse { export interface TracingData {
status: string; message: string;
data: { duration_msec: number;
result: MetricResult[]; children: TracingData[];
resultType: "matrix";
}
} }

View file

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

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 Alert from "@mui/material/Alert";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import GraphView from "./Views/GraphView"; import GraphView from "./Views/GraphView";
@ -13,10 +13,13 @@ import {useGraphDispatch, useGraphState} from "../../state/graph/GraphStateConte
import {AxisRange} from "../../state/graph/reducer"; import {AxisRange} from "../../state/graph/reducer";
import Spinner from "../common/Spinner"; import Spinner from "../common/Spinner";
import {useFetchQueryOptions} from "../../hooks/useFetchQueryOptions"; import {useFetchQueryOptions} from "../../hooks/useFetchQueryOptions";
import TracingsView from "./Views/TracingsView";
import Trace from "./Trace/Trace";
const CustomPanel: FC = () => { 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 { customStep, yaxis } = useGraphState();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -35,11 +38,26 @@ const CustomPanel: FC = () => {
}; };
const {queryOptions} = useFetchQueryOptions(); const {queryOptions} = useFetchQueryOptions();
const {isLoading, liveData, graphData, error} = useFetchQuery({ const {isLoading, liveData, graphData, error, tracingData} = useFetchQuery({
visible: true, visible: true,
customStep 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 ( return (
<Box p={4} display="grid" gridTemplateRows="auto 1fr" style={{minHeight: "calc(100vh - 64px)"}}> <Box p={4} display="grid" gridTemplateRows="auto 1fr" style={{minHeight: "calc(100vh - 64px)"}}>
<QueryConfigurator error={error} queryOptions={queryOptions}/> <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} <Box display="grid" gridTemplateColumns="1fr auto" alignItems="center" mx={-4} px={4} mb={2}
borderBottom={1} borderColor="divider"> borderBottom={1} borderColor="divider">
<DisplayTypeSwitch/> <DisplayTypeSwitch/>
<Box display={"flex"}>
{displayType === "chart" && <GraphSettings {displayType === "chart" && <GraphSettings
yaxis={yaxis} yaxis={yaxis}
setYaxisLimits={setYaxisLimits} setYaxisLimits={setYaxisLimits}
toggleEnableLimits={toggleEnableLimits} toggleEnableLimits={toggleEnableLimits}
/>} />}
</Box> </Box>
</Box>
{error && <Alert color="error" severity="error" sx={{whiteSpace: "pre-wrap", mt: 2}}>{error}</Alert>} {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} <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 === "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> </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 {getQueryRangeUrl, getQueryUrl} from "../api/query-range";
import {useAppState} from "../state/common/StateContext"; 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 {isValidHttpUrl} from "../utils/url";
import {ErrorTypes} from "../types"; import {ErrorTypes} from "../types";
import {getAppModeEnable, getAppModeParams} from "../utils/app-mode"; 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 {CustomStep} from "../state/graph/reducer";
import usePrevious from "./usePrevious"; import usePrevious from "./usePrevious";
import {arrayEquals} from "../utils/array"; import {arrayEquals} from "../utils/array";
import Trace from "../components/CustomPanel/Trace/Trace";
interface FetchQueryParams { interface FetchQueryParams {
predefinedQuery?: string[] predefinedQuery?: string[]
@ -27,12 +28,14 @@ export const useFetchQuery = ({predefinedQuery, visible, display, customStep}: F
graphData?: MetricResult[], graphData?: MetricResult[],
liveData?: InstantMetricResult[], liveData?: InstantMetricResult[],
error?: ErrorTypes | string, 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 [isLoading, setIsLoading] = useState(false);
const [graphData, setGraphData] = useState<MetricResult[]>(); const [graphData, setGraphData] = useState<MetricResult[]>();
const [liveData, setLiveData] = useState<InstantMetricResult[]>(); const [liveData, setLiveData] = useState<InstantMetricResult[]>();
const [tracingData, setTracingData] = useState<Trace>();
const [error, setError] = useState<ErrorTypes | string>(); const [error, setError] = useState<ErrorTypes | string>();
const [fetchQueue, setFetchQueue] = useState<AbortController[]>([]); const [fetchQueue, setFetchQueue] = useState<AbortController[]>([]);
@ -40,10 +43,22 @@ export const useFetchQuery = ({predefinedQuery, visible, display, customStep}: F
if (error) { if (error) {
setGraphData(undefined); setGraphData(undefined);
setLiveData(undefined); setLiveData(undefined);
setTracingData(undefined);
} }
}, [error]); }, [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(); const controller = new AbortController();
setFetchQueue([...fetchQueue, controller]); setFetchQueue([...fetchQueue, controller]);
try { try {
@ -54,6 +69,7 @@ export const useFetchQuery = ({predefinedQuery, visible, display, customStep}: F
const resp = await response.json(); const resp = await response.json();
if (response.ok) { if (response.ok) {
setError(undefined); setError(undefined);
updateTracingData(resp.trace, query);
tempData.push(...resp.data.result.map((d: MetricBase) => { tempData.push(...resp.data.result.map((d: MetricBase) => {
d.group = counter; d.group = counter;
return d; return d;
@ -87,8 +103,8 @@ export const useFetchQuery = ({predefinedQuery, visible, display, customStep}: F
const updatedPeriod = {...period}; const updatedPeriod = {...period};
if (customStep.enable) updatedPeriod.step = customStep.value; if (customStep.enable) updatedPeriod.step = customStep.value;
return expr.filter(q => q.trim()).map(q => displayChart return expr.filter(q => q.trim()).map(q => displayChart
? getQueryRangeUrl(server, q, updatedPeriod, nocache) ? getQueryRangeUrl(server, q, updatedPeriod, nocache, isTracingEnabled)
: getQueryUrl(server, q, updatedPeriod)); : getQueryUrl(server, q, updatedPeriod, isTracingEnabled));
} else { } else {
setError(ErrorTypes.validServer); setError(ErrorTypes.validServer);
} }
@ -100,7 +116,8 @@ export const useFetchQuery = ({predefinedQuery, visible, display, customStep}: F
useEffect(() => { useEffect(() => {
if (!visible || (fetchUrl && prevFetchUrl && arrayEquals(fetchUrl, prevFetchUrl)) || !fetchUrl?.length) return; if (!visible || (fetchUrl && prevFetchUrl && arrayEquals(fetchUrl, prevFetchUrl)) || !fetchUrl?.length) return;
setIsLoading(true); setIsLoading(true);
throttledFetchData(fetchUrl, fetchQueue, (display || displayType)); const expr = predefinedQuery ?? query;
throttledFetchData(fetchUrl, fetchQueue, (display || displayType), expr);
}, [fetchUrl, visible]); }, [fetchUrl, visible]);
useEffect(() => { useEffect(() => {
@ -110,5 +127,5 @@ export const useFetchQuery = ({predefinedQuery, visible, display, customStep}: F
setFetchQueue(fetchQueue.filter(f => !f.signal.aborted)); setFetchQueue(fetchQueue.filter(f => !f.signal.aborted));
}, [fetchQueue]); }, [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[], queryHistory: QueryHistory[],
queryControls: { queryControls: {
autoRefresh: boolean; autoRefresh: boolean;
autocomplete: boolean, autocomplete: boolean;
nocache: boolean nocache: boolean;
isTracingEnabled: boolean;
} }
} }
@ -55,6 +56,7 @@ export type Action =
| { type: "TOGGLE_AUTOREFRESH"} | { type: "TOGGLE_AUTOREFRESH"}
| { type: "TOGGLE_AUTOCOMPLETE"} | { type: "TOGGLE_AUTOCOMPLETE"}
| { type: "NO_CACHE"} | { type: "NO_CACHE"}
| { type: "TOGGLE_QUERY_TRACING" }
const {duration, endInput, relativeTimeId} = getRelativeTime({ const {duration, endInput, relativeTimeId} = getRelativeTime({
@ -79,6 +81,7 @@ export const initialState: AppState = {
autoRefresh: false, autoRefresh: false,
autocomplete: getFromStorage("AUTOCOMPLETE") as boolean || false, autocomplete: getFromStorage("AUTOCOMPLETE") as boolean || false,
nocache: getFromStorage("NO_CACHE") 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 autocomplete: !state.queryControls.autocomplete
} }
}; };
case "TOGGLE_QUERY_TRACING":
return {
...state,
queryControls: {
...state.queryControls,
isTracingEnabled: !state.queryControls.isTracingEnabled,
}
};
case "NO_CACHE": case "NO_CACHE":
return { return {
...state, ...state,

View file

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

View file

@ -243,7 +243,8 @@ Prometheus doesn't drop data during VictoriaMetrics restart. See [this article](
## vmui ## vmui
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. 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: 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 ## Query tracing
VictoriaMetrics supports query tracing, which can be used for determining bottlenecks during query processing. 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. 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. 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. 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 ## Cardinality limiter

View file

@ -247,7 +247,8 @@ Prometheus doesn't drop data during VictoriaMetrics restart. See [this article](
## vmui ## vmui
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. 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: 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 ## Query tracing
VictoriaMetrics supports query tracing, which can be used for determining bottlenecks during query processing. 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. 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. 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. 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 ## Cardinality limiter