vmui/logs: fix the update of the relative time range (#6517)

### Describe Your Changes

- Fixed the update of the relative time range when `Execute Query` is
clicked
- Optimized server requests: now, if an error occurs in the `/query`
request, the `/hits` request will not be executed.

#6345 (duplicates: #6440, #6312)
This commit is contained in:
Yury Molodov 2024-06-26 11:23:22 +02:00 committed by GitHub
parent 6652fb630f
commit 43342745ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 67 additions and 51 deletions

View file

@ -1,4 +1,4 @@
import React, { FC, useEffect } from "preact/compat"; import React, { FC, useCallback, useEffect } from "preact/compat";
import ExploreLogsBody from "./ExploreLogsBody/ExploreLogsBody"; import ExploreLogsBody from "./ExploreLogsBody/ExploreLogsBody";
import useStateSearchParams from "../../hooks/useStateSearchParams"; import useStateSearchParams from "../../hooks/useStateSearchParams";
import useSearchParamsFromObject from "../../hooks/useSearchParamsFromObject"; import useSearchParamsFromObject from "../../hooks/useSearchParamsFromObject";
@ -8,45 +8,54 @@ import Spinner from "../../components/Main/Spinner/Spinner";
import Alert from "../../components/Main/Alert/Alert"; import Alert from "../../components/Main/Alert/Alert";
import ExploreLogsHeader from "./ExploreLogsHeader/ExploreLogsHeader"; import ExploreLogsHeader from "./ExploreLogsHeader/ExploreLogsHeader";
import "./style.scss"; import "./style.scss";
import { ErrorTypes } from "../../types"; import { ErrorTypes, TimeParams } from "../../types";
import { useState } from "react"; import { useState } from "react";
import { useTimeState } from "../../state/time/TimeStateContext"; import { useTimeState } from "../../state/time/TimeStateContext";
import { getFromStorage, saveToStorage } from "../../utils/storage"; import { getFromStorage, saveToStorage } from "../../utils/storage";
import ExploreLogsBarChart from "./ExploreLogsBarChart/ExploreLogsBarChart"; import ExploreLogsBarChart from "./ExploreLogsBarChart/ExploreLogsBarChart";
import { useFetchLogHits } from "./hooks/useFetchLogHits"; import { useFetchLogHits } from "./hooks/useFetchLogHits";
import { LOGS_ENTRIES_LIMIT } from "../../constants/logs"; import { LOGS_ENTRIES_LIMIT } from "../../constants/logs";
import { getTimeperiodForDuration, relativeTimeOptions } from "../../utils/time";
const storageLimit = Number(getFromStorage("LOGS_LIMIT")); const storageLimit = Number(getFromStorage("LOGS_LIMIT"));
const defaultLimit = isNaN(storageLimit) ? LOGS_ENTRIES_LIMIT : storageLimit; const defaultLimit = isNaN(storageLimit) ? LOGS_ENTRIES_LIMIT : storageLimit;
const ExploreLogs: FC = () => { const ExploreLogs: FC = () => {
const { serverUrl } = useAppState(); const { serverUrl } = useAppState();
const { duration, relativeTime, period } = useTimeState(); const { duration, relativeTime, period: periodState } = useTimeState();
const { setSearchParamsFromKeys } = useSearchParamsFromObject(); const { setSearchParamsFromKeys } = useSearchParamsFromObject();
const [limit, setLimit] = useStateSearchParams(defaultLimit, "limit"); const [limit, setLimit] = useStateSearchParams(defaultLimit, "limit");
const [query, setQuery] = useStateSearchParams("*", "query"); const [query, setQuery] = useStateSearchParams("*", "query");
const [period, setPeriod] = useState<TimeParams>(periodState);
const { logs, isLoading, error, fetchLogs } = useFetchLogs(serverUrl, query, limit); const { logs, isLoading, error, fetchLogs } = useFetchLogs(serverUrl, query, limit);
const { fetchLogHits, ...dataLogHits } = useFetchLogHits(serverUrl, query); const { fetchLogHits, ...dataLogHits } = useFetchLogHits(serverUrl, query);
const [queryError, setQueryError] = useState<ErrorTypes | string>(""); const [queryError, setQueryError] = useState<ErrorTypes | string>("");
const [loaded, isLoaded] = useState(false);
const [markdownParsing, setMarkdownParsing] = useState(getFromStorage("LOGS_MARKDOWN") === "true"); const [markdownParsing, setMarkdownParsing] = useState(getFromStorage("LOGS_MARKDOWN") === "true");
const getPeriod = useCallback(() => {
const relativeTimeOpts = relativeTimeOptions.find(d => d.id === relativeTime);
if (!relativeTimeOpts) return periodState;
const { duration, until } = relativeTimeOpts;
return getTimeperiodForDuration(duration, until());
}, [periodState, relativeTime]);
const handleRunQuery = () => { const handleRunQuery = () => {
if (!query) { if (!query) {
setQueryError(ErrorTypes.validQuery); setQueryError(ErrorTypes.validQuery);
return; return;
} }
fetchLogs().then(() => { const newPeriod = getPeriod();
isLoaded(true); setPeriod(newPeriod);
}); fetchLogs(newPeriod).then(() => {
fetchLogHits(); fetchLogHits(newPeriod);
}).catch(e => e);
setSearchParamsFromKeys( { setSearchParamsFromKeys( {
query, query,
"g0.range_input": duration, "g0.range_input": duration,
"g0.end_input": period.date, "g0.end_input": newPeriod.date,
"g0.relative_time": relativeTime || "none", "g0.relative_time": relativeTime || "none",
}); });
}; };
@ -64,7 +73,7 @@ const ExploreLogs: FC = () => {
useEffect(() => { useEffect(() => {
if (query) handleRunQuery(); if (query) handleRunQuery();
}, [period]); }, [periodState]);
useEffect(() => { useEffect(() => {
setQueryError(""); setQueryError("");
@ -84,14 +93,15 @@ const ExploreLogs: FC = () => {
/> />
{isLoading && <Spinner />} {isLoading && <Spinner />}
{error && <Alert variant="error">{error}</Alert>} {error && <Alert variant="error">{error}</Alert>}
<ExploreLogsBarChart {!error && (
query={query} <ExploreLogsBarChart
loaded={loaded} query={query}
{...dataLogHits} period={period}
/> {...dataLogHits}
/>
)}
<ExploreLogsBody <ExploreLogsBody
data={logs} data={logs}
loaded={loaded}
markdownParsing={markdownParsing} markdownParsing={markdownParsing}
/> />
</div> </div>

View file

@ -4,22 +4,22 @@ import useDeviceDetect from "../../../hooks/useDeviceDetect";
import classNames from "classnames"; import classNames from "classnames";
import { LogHits } from "../../../api/types"; import { LogHits } from "../../../api/types";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useTimeDispatch, useTimeState } from "../../../state/time/TimeStateContext"; import { useTimeDispatch } from "../../../state/time/TimeStateContext";
import { AlignedData } from "uplot"; import { AlignedData } from "uplot";
import BarHitsChart from "../../../components/Chart/BarHitsChart/BarHitsChart"; import BarHitsChart from "../../../components/Chart/BarHitsChart/BarHitsChart";
import Alert from "../../../components/Main/Alert/Alert"; import Alert from "../../../components/Main/Alert/Alert";
import { TimeParams } from "../../../types";
interface Props { interface Props {
query: string; query: string;
logHits: LogHits[]; logHits: LogHits[];
period: TimeParams;
error?: string; error?: string;
isLoading: boolean; isLoading: boolean;
loaded: boolean;
} }
const ExploreLogsBarChart: FC<Props> = ({ logHits, error, loaded }) => { const ExploreLogsBarChart: FC<Props> = ({ logHits, period, error }) => {
const { isMobile } = useDeviceDetect(); const { isMobile } = useDeviceDetect();
const { period } = useTimeState();
const timeDispatch = useTimeDispatch(); const timeDispatch = useTimeDispatch();
const data = useMemo(() => { const data = useMemo(() => {
@ -56,13 +56,13 @@ const ExploreLogsBarChart: FC<Props> = ({ logHits, error, loaded }) => {
"vm-block_mobile": isMobile, "vm-block_mobile": isMobile,
})} })}
> >
{!error && loaded && noDataMessage && ( {!error && noDataMessage && (
<div className="vm-explore-logs-chart__empty"> <div className="vm-explore-logs-chart__empty">
<Alert variant="info">{noDataMessage}</Alert> <Alert variant="info">{noDataMessage}</Alert>
</div> </div>
)} )}
{error && loaded && noDataMessage && ( {error && noDataMessage && (
<div className="vm-explore-logs-chart__empty"> <div className="vm-explore-logs-chart__empty">
<Alert variant="error">{error}</Alert> <Alert variant="error">{error}</Alert>
</div> </div>

View file

@ -19,7 +19,6 @@ import { marked } from "marked";
export interface ExploreLogBodyProps { export interface ExploreLogBodyProps {
data: Logs[]; data: Logs[];
loaded?: boolean;
markdownParsing: boolean; markdownParsing: boolean;
} }
@ -35,7 +34,7 @@ const tabs = [
{ label: "JSON", value: DisplayType.json, icon: <CodeIcon/> }, { label: "JSON", value: DisplayType.json, icon: <CodeIcon/> },
]; ];
const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, loaded, markdownParsing }) => { const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, markdownParsing }) => {
const { isMobile } = useDeviceDetect(); const { isMobile } = useDeviceDetect();
const { timezone } = useTimeState(); const { timezone } = useTimeState();
const { setSearchParamsFromKeys } = useSearchParamsFromObject(); const { setSearchParamsFromKeys } = useSearchParamsFromObject();
@ -109,11 +108,7 @@ const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, loaded, markdownParsin
"vm-explore-logs-body__table_mobile": isMobile, "vm-explore-logs-body__table_mobile": isMobile,
})} })}
> >
{!data.length && ( {!data.length && <div className="vm-explore-logs-body__empty">No logs found</div>}
<div className="vm-explore-logs-body__empty">
{loaded ? "No logs found" : "Run query to see logs"}
</div>
)}
{!!data.length && ( {!!data.length && (
<> <>
{activeTab === DisplayType.table && ( {activeTab === DisplayType.table && (

View file

@ -1,13 +1,11 @@
import { useCallback, useMemo, useRef, useState } from "preact/compat"; import { useCallback, useMemo, useRef, useState } from "preact/compat";
import { getLogHitsUrl } from "../../../api/logs"; import { getLogHitsUrl } from "../../../api/logs";
import { ErrorTypes } from "../../../types"; import { ErrorTypes, TimeParams } from "../../../types";
import { LogHits } from "../../../api/types"; import { LogHits } from "../../../api/types";
import { useTimeState } from "../../../state/time/TimeStateContext";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { LOGS_BARS_VIEW } from "../../../constants/logs"; import { LOGS_BARS_VIEW } from "../../../constants/logs";
export const useFetchLogHits = (server: string, query: string) => { export const useFetchLogHits = (server: string, query: string) => {
const { period } = useTimeState();
const [logHits, setLogHits] = useState<LogHits[]>([]); const [logHits, setLogHits] = useState<LogHits[]>([]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<ErrorTypes | string>(); const [error, setError] = useState<ErrorTypes | string>();
@ -15,13 +13,14 @@ export const useFetchLogHits = (server: string, query: string) => {
const url = useMemo(() => getLogHitsUrl(server), [server]); const url = useMemo(() => getLogHitsUrl(server), [server]);
const options = useMemo(() => { const getOptions = (query: string, period: TimeParams, signal: AbortSignal) => {
const start = dayjs(period.start * 1000); const start = dayjs(period.start * 1000);
const end = dayjs(period.end * 1000); const end = dayjs(period.end * 1000);
const totalSeconds = end.diff(start, "milliseconds"); const totalSeconds = end.diff(start, "milliseconds");
const step = Math.ceil(totalSeconds / LOGS_BARS_VIEW) || 1; const step = Math.ceil(totalSeconds / LOGS_BARS_VIEW) || 1;
return { return {
signal,
method: "POST", method: "POST",
body: new URLSearchParams({ body: new URLSearchParams({
query: query.trim(), query: query.trim(),
@ -30,30 +29,34 @@ export const useFetchLogHits = (server: string, query: string) => {
end: end.toISOString(), end: end.toISOString(),
}) })
}; };
}, [query, period]); };
const fetchLogHits = useCallback(async () => { const fetchLogHits = useCallback(async (period: TimeParams) => {
abortControllerRef.current.abort(); abortControllerRef.current.abort();
abortControllerRef.current = new AbortController(); abortControllerRef.current = new AbortController();
const { signal } = abortControllerRef.current; const { signal } = abortControllerRef.current;
setIsLoading(true); setIsLoading(true);
setError(undefined); setError(undefined);
try { try {
const response = await fetch(url, { ...options, signal }); const options = getOptions(query, period, signal);
const response = await fetch(url, options);
if (!response.ok || !response.body) { if (!response.ok || !response.body) {
const text = await response.text(); const text = await response.text();
setError(text); setError(text);
setLogHits([]); setLogHits([]);
setIsLoading(false); setIsLoading(false);
return; return Promise.reject(new Error(text));
} }
const data = await response.json(); const data = await response.json();
const hits = data?.hits as LogHits[]; const hits = data?.hits as LogHits[];
if (!hits) { if (!hits) {
setError("Error: No 'hits' field in response"); const error = "Error: No 'hits' field in response";
return; setError(error);
return Promise.reject(new Error(error));
} }
setLogHits(hits); setLogHits(hits);
@ -63,9 +66,11 @@ export const useFetchLogHits = (server: string, query: string) => {
console.error(e); console.error(e);
setLogHits([]); setLogHits([]);
} }
return Promise.reject(e);
} finally {
setIsLoading(false);
} }
// setIsLoading(false); }, [url, query]);
}, [url, options]);
return { return {
logHits, logHits,

View file

@ -1,12 +1,10 @@
import { useCallback, useMemo, useRef, useState } from "preact/compat"; import { useCallback, useMemo, useRef, useState } from "preact/compat";
import { getLogsUrl } from "../../../api/logs"; import { getLogsUrl } from "../../../api/logs";
import { ErrorTypes } from "../../../types"; import { ErrorTypes, TimeParams } from "../../../types";
import { Logs } from "../../../api/types"; import { Logs } from "../../../api/types";
import { useTimeState } from "../../../state/time/TimeStateContext";
import dayjs from "dayjs"; import dayjs from "dayjs";
export const useFetchLogs = (server: string, query: string, limit: number) => { export const useFetchLogs = (server: string, query: string, limit: number) => {
const { period } = useTimeState();
const [logs, setLogs] = useState<Logs[]>([]); const [logs, setLogs] = useState<Logs[]>([]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<ErrorTypes | string>(); const [error, setError] = useState<ErrorTypes | string>();
@ -14,7 +12,8 @@ export const useFetchLogs = (server: string, query: string, limit: number) => {
const url = useMemo(() => getLogsUrl(server), [server]); const url = useMemo(() => getLogsUrl(server), [server]);
const options = useMemo(() => ({ const getOptions = (query: string, period: TimeParams, limit: number, signal: AbortSignal) => ({
signal,
method: "POST", method: "POST",
headers: { headers: {
"Accept": "application/stream+json", "Accept": "application/stream+json",
@ -25,7 +24,7 @@ export const useFetchLogs = (server: string, query: string, limit: number) => {
start: dayjs(period.start * 1000).tz().toISOString(), start: dayjs(period.start * 1000).tz().toISOString(),
end: dayjs(period.end * 1000).tz().toISOString() end: dayjs(period.end * 1000).tz().toISOString()
}) })
}), [query, limit, period]); });
const parseLineToJSON = (line: string): Logs | null => { const parseLineToJSON = (line: string): Logs | null => {
try { try {
@ -35,22 +34,24 @@ export const useFetchLogs = (server: string, query: string, limit: number) => {
} }
}; };
const fetchLogs = useCallback(async () => { const fetchLogs = useCallback(async (period: TimeParams) => {
abortControllerRef.current.abort(); abortControllerRef.current.abort();
abortControllerRef.current = new AbortController(); abortControllerRef.current = new AbortController();
const { signal } = abortControllerRef.current; const { signal } = abortControllerRef.current;
const limit = Number(options.body.get("limit"));
setIsLoading(true); setIsLoading(true);
setError(undefined); setError(undefined);
try { try {
const response = await fetch(url, { ...options, signal }); const options = getOptions(query, period, limit, signal);
const response = await fetch(url, options);
const text = await response.text(); const text = await response.text();
if (!response.ok || !response.body) { if (!response.ok || !response.body) {
setError(text); setError(text);
setLogs([]); setLogs([]);
setIsLoading(false); setIsLoading(false);
return; return Promise.reject(new Error(text));
} }
const lines = text.split("\n").filter(line => line).slice(0, limit); const lines = text.split("\n").filter(line => line).slice(0, limit);
@ -62,9 +63,12 @@ export const useFetchLogs = (server: string, query: string, limit: number) => {
console.error(e); console.error(e);
setLogs([]); setLogs([]);
} }
return Promise.reject(e);
} finally {
setIsLoading(false);
} }
setIsLoading(false); setIsLoading(false);
}, [url, options]); }, [url, query, limit]);
return { return {
logs, logs,

View file

@ -21,6 +21,8 @@ according to [these docs](https://docs.victoriametrics.com/victorialogs/quicksta
* FEATURE: add `-retention.maxDiskSpaceUsageBytes` command-line flag, which allows limiting disk space usage for [VictoriaLogs data](https://docs.victoriametrics.com/victorialogs/#storage) by automatic dropping the oldest per-day partitions if the storage disk space usage becomes bigger than the `-retention.maxDiskSpaceUsageBytes`. See [these docs](https://docs.victoriametrics.com/victorialogs/#retention-by-disk-space-usage). * FEATURE: add `-retention.maxDiskSpaceUsageBytes` command-line flag, which allows limiting disk space usage for [VictoriaLogs data](https://docs.victoriametrics.com/victorialogs/#storage) by automatic dropping the oldest per-day partitions if the storage disk space usage becomes bigger than the `-retention.maxDiskSpaceUsageBytes`. See [these docs](https://docs.victoriametrics.com/victorialogs/#retention-by-disk-space-usage).
* BUGFIX: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): fix the update of the relative time range when `Execute Query` is clicked. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6345).
## [v0.23.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v0.23.0-victorialogs) ## [v0.23.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v0.23.0-victorialogs)
Released at 2024-06-25 Released at 2024-06-25