vmui: add field for log entries limit (#5799)

* vmui: add field for log entries limit (#5674)

* vmui: refactor useFetchLogs

* vmui: fix log query encoding

---------

Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
Yury Molodov 2024-03-01 00:30:32 +01:00 committed by Aliaksandr Valialkin
parent 7675bc8f27
commit e00d313333
No known key found for this signature in database
GPG key ID: 52C003EE2BCDB9EB
6 changed files with 85 additions and 80 deletions

View file

@ -11,14 +11,19 @@ import "./style.scss";
import { ErrorTypes } from "../../types"; import { ErrorTypes } 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";
const storageLimit = Number(getFromStorage("LOGS_LIMIT"));
const defaultLimit = isNaN(storageLimit) ? 1000 : storageLimit;
const ExploreLogs: FC = () => { const ExploreLogs: FC = () => {
const { serverUrl } = useAppState(); const { serverUrl } = useAppState();
const { duration, relativeTime, period } = useTimeState(); const { duration, relativeTime, period } = useTimeState();
const { setSearchParamsFromKeys } = useSearchParamsFromObject(); const { setSearchParamsFromKeys } = useSearchParamsFromObject();
const [limit, setLimit] = useStateSearchParams(defaultLimit, "limit");
const [query, setQuery] = useStateSearchParams("", "query"); const [query, setQuery] = useStateSearchParams("", "query");
const { logs, isLoading, error, fetchLogs } = useFetchLogs(serverUrl, query); const { logs, isLoading, error, fetchLogs } = useFetchLogs(serverUrl, query, limit);
const [queryError, setQueryError] = useState<ErrorTypes | string>(""); const [queryError, setQueryError] = useState<ErrorTypes | string>("");
const [loaded, isLoaded] = useState(false); const [loaded, isLoaded] = useState(false);
@ -40,6 +45,12 @@ const ExploreLogs: FC = () => {
}); });
}; };
const handleChangeLimit = (limit: number) => {
setLimit(limit);
setSearchParamsFromKeys({ limit });
saveToStorage("LOGS_LIMIT", `${limit}`);
};
useEffect(() => { useEffect(() => {
if (query) handleRunQuery(); if (query) handleRunQuery();
}, [period]); }, [period]);
@ -53,7 +64,9 @@ const ExploreLogs: FC = () => {
<ExploreLogsHeader <ExploreLogsHeader
query={query} query={query}
error={queryError} error={queryError}
limit={limit}
onChange={setQuery} onChange={setQuery}
onChangeLimit={handleChangeLimit}
onRun={handleRunQuery} onRun={handleRunQuery}
/> />
{isLoading && <Spinner />} {isLoading && <Spinner />}

View file

@ -8,10 +8,8 @@ import useDeviceDetect from "../../../hooks/useDeviceDetect";
import { Logs } from "../../../api/types"; import { Logs } from "../../../api/types";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useTimeState } from "../../../state/time/TimeStateContext"; import { useTimeState } from "../../../state/time/TimeStateContext";
import SelectLimit from "../../../components/Main/Pagination/SelectLimit/SelectLimit";
import useStateSearchParams from "../../../hooks/useStateSearchParams"; import useStateSearchParams from "../../../hooks/useStateSearchParams";
import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject"; import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject";
import { getFromStorage, saveToStorage } from "../../../utils/storage";
import TableSettings from "../../../components/Table/TableSettings/TableSettings"; import TableSettings from "../../../components/Table/TableSettings/TableSettings";
import useBoolean from "../../../hooks/useBoolean"; import useBoolean from "../../../hooks/useBoolean";
import TableLogs from "./TableLogs"; import TableLogs from "./TableLogs";
@ -29,16 +27,15 @@ enum DisplayType {
} }
const tabs = [ const tabs = [
{ label: "Group", value: DisplayType.group, icon: <ListIcon /> }, { label: "Group", value: DisplayType.group, icon: <ListIcon/> },
{ label: "Table", value: DisplayType.table, icon: <TableIcon /> }, { label: "Table", value: DisplayType.table, icon: <TableIcon/> },
{ label: "JSON", value: DisplayType.json, icon: <CodeIcon /> }, { label: "JSON", value: DisplayType.json, icon: <CodeIcon/> },
]; ];
const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, loaded }) => { const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, loaded }) => {
const { isMobile } = useDeviceDetect(); const { isMobile } = useDeviceDetect();
const { timezone } = useTimeState(); const { timezone } = useTimeState();
const { setSearchParamsFromKeys } = useSearchParamsFromObject(); const { setSearchParamsFromKeys } = useSearchParamsFromObject();
const [limitRows, setLimitRows] = useStateSearchParams(getFromStorage("LOGS_LIMIT") || 50, "limit");
const [activeTab, setActiveTab] = useStateSearchParams(DisplayType.group, "view"); const [activeTab, setActiveTab] = useStateSearchParams(DisplayType.group, "view");
const [displayColumns, setDisplayColumns] = useState<string[]>([]); const [displayColumns, setDisplayColumns] = useState<string[]>([]);
@ -67,12 +64,6 @@ const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, loaded }) => {
setSearchParamsFromKeys({ view }); setSearchParamsFromKeys({ view });
}; };
const handleChangeLimit = (limit: number) => {
setLimitRows(limit);
setSearchParamsFromKeys({ limit });
saveToStorage("LOGS_LIMIT", `${limit}`);
};
return ( return (
<div <div
className={classNames({ className={classNames({
@ -97,10 +88,6 @@ const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, loaded }) => {
</div> </div>
{activeTab === DisplayType.table && ( {activeTab === DisplayType.table && (
<div className="vm-explore-logs-body-header__settings"> <div className="vm-explore-logs-body-header__settings">
<SelectLimit
limit={+limitRows}
onChange={handleChangeLimit}
/>
<TableSettings <TableSettings
columns={columns} columns={columns}
defaultColumns={displayColumns} defaultColumns={displayColumns}
@ -128,7 +115,6 @@ const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, loaded }) => {
{activeTab === DisplayType.table && ( {activeTab === DisplayType.table && (
<TableLogs <TableLogs
logs={logs} logs={logs}
limitRows={+limitRows}
displayColumns={displayColumns} displayColumns={displayColumns}
tableCompact={tableCompact} tableCompact={tableCompact}
columns={columns} columns={columns}
@ -141,7 +127,7 @@ const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, loaded }) => {
/> />
)} )}
{activeTab === DisplayType.json && ( {activeTab === DisplayType.json && (
<JsonView data={data} /> <JsonView data={data}/>
)} )}
</> </>
)} )}

View file

@ -2,22 +2,15 @@ import React, { FC, useMemo } from "preact/compat";
import "./style.scss"; import "./style.scss";
import Table from "../../../components/Table/Table"; import Table from "../../../components/Table/Table";
import { Logs } from "../../../api/types"; import { Logs } from "../../../api/types";
import useStateSearchParams from "../../../hooks/useStateSearchParams";
import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject";
import PaginationControl from "../../../components/Main/Pagination/PaginationControl/PaginationControl";
interface TableLogsProps { interface TableLogsProps {
logs: Logs[]; logs: Logs[];
limitRows: number;
displayColumns: string[]; displayColumns: string[];
tableCompact: boolean; tableCompact: boolean;
columns: string[]; columns: string[];
} }
const TableLogs: FC<TableLogsProps> = ({ logs, limitRows, displayColumns, tableCompact, columns }) => { const TableLogs: FC<TableLogsProps> = ({ logs, displayColumns, tableCompact, columns }) => {
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
const [page, setPage] = useStateSearchParams(1, "page");
const getColumnClass = (key: string) => { const getColumnClass = (key: string) => {
switch (key) { switch (key) {
case "time": case "time":
@ -29,16 +22,6 @@ const TableLogs: FC<TableLogsProps> = ({ logs, limitRows, displayColumns, tableC
} }
}; };
// TODO: Remove when pagination is implemented on the backend.
const paginationOffset = useMemo(() => {
const startIndex = (page - 1) * Number(limitRows);
const endIndex = startIndex + Number(limitRows);
return {
startIndex,
endIndex
};
}, [page, limitRows]);
const tableColumns = useMemo(() => { const tableColumns = useMemo(() => {
if (tableCompact) { if (tableCompact) {
return [{ return [{
@ -60,11 +43,6 @@ const TableLogs: FC<TableLogsProps> = ({ logs, limitRows, displayColumns, tableC
return tableColumns.filter(c => displayColumns.includes(c.key as string)); return tableColumns.filter(c => displayColumns.includes(c.key as string));
}, [tableColumns, displayColumns, tableCompact]); }, [tableColumns, displayColumns, tableCompact]);
const handleChangePage = (page: number) => {
setPage(page);
setSearchParamsFromKeys({ page });
};
return ( return (
<> <>
<Table <Table
@ -72,14 +50,7 @@ const TableLogs: FC<TableLogsProps> = ({ logs, limitRows, displayColumns, tableC
columns={filteredColumns} columns={filteredColumns}
defaultOrderBy={"time"} defaultOrderBy={"time"}
copyToClipboard={"data"} copyToClipboard={"data"}
paginationOffset={paginationOffset} paginationOffset={{ startIndex: 0, endIndex: Infinity }}
/>
<PaginationControl
page={page}
limit={+limitRows}
// TODO: Remove .slice() when pagination is implemented on the backend.
length={logs.slice(paginationOffset.startIndex, paginationOffset.endIndex).length}
onChange={handleChangePage}
/> />
</> </>
); );

View file

@ -1,21 +1,42 @@
import React, { FC } from "react"; import React, { FC, useEffect, useState } from "preact/compat";
import { InfoIcon, PlayIcon, WikiIcon } from "../../../components/Main/Icons"; import { InfoIcon, PlayIcon, WikiIcon } from "../../../components/Main/Icons";
import "./style.scss"; import "./style.scss";
import classNames from "classnames"; import classNames from "classnames";
import useDeviceDetect from "../../../hooks/useDeviceDetect"; import useDeviceDetect from "../../../hooks/useDeviceDetect";
import Button from "../../../components/Main/Button/Button"; import Button from "../../../components/Main/Button/Button";
import QueryEditor from "../../../components/Configurators/QueryEditor/QueryEditor"; import QueryEditor from "../../../components/Configurators/QueryEditor/QueryEditor";
import TextField from "../../../components/Main/TextField/TextField";
export interface ExploreLogHeaderProps { export interface ExploreLogHeaderProps {
query: string; query: string;
limit: number;
error?: string; error?: string;
onChange: (val: string) => void; onChange: (val: string) => void;
onChangeLimit: (val: number) => void;
onRun: () => void; onRun: () => void;
} }
const ExploreLogsHeader: FC<ExploreLogHeaderProps> = ({ query, error, onChange, onRun }) => { const ExploreLogsHeader: FC<ExploreLogHeaderProps> = ({ query, limit, error, onChange, onChangeLimit, onRun }) => {
const { isMobile } = useDeviceDetect(); const { isMobile } = useDeviceDetect();
const [errorLimit, setErrorLimit] = useState("");
const [limitInput, setLimitInput] = useState(limit);
const handleChangeLimit = (val: string) => {
const number = +val;
setLimitInput(number);
if (isNaN(number) || number < 0) {
setErrorLimit("Number must be bigger than zero");
} else {
setErrorLimit("");
onChangeLimit(number);
}
};
useEffect(() => {
setLimitInput(limit);
}, [limit]);
return ( return (
<div <div
className={classNames({ className={classNames({
@ -24,7 +45,7 @@ const ExploreLogsHeader: FC<ExploreLogHeaderProps> = ({ query, error, onChange,
"vm-block_mobile": isMobile, "vm-block_mobile": isMobile,
})} })}
> >
<div className="vm-explore-logs-header__input"> <div className="vm-explore-logs-header-top">
<QueryEditor <QueryEditor
value={query} value={query}
autocomplete={false} autocomplete={false}
@ -35,6 +56,14 @@ const ExploreLogsHeader: FC<ExploreLogHeaderProps> = ({ query, error, onChange,
label={"Log query"} label={"Log query"}
error={error} error={error}
/> />
<TextField
label="Limit entries"
type="number"
value={limitInput}
error={errorLimit}
onChange={handleChangeLimit}
onEnter={onRun}
/>
</div> </div>
<div className="vm-explore-logs-header-bottom"> <div className="vm-explore-logs-header-bottom">
<div className="vm-explore-logs-header-bottom-helpful"> <div className="vm-explore-logs-header-bottom-helpful">

View file

@ -5,7 +5,13 @@
align-items: center; align-items: center;
gap: $padding-global; gap: $padding-global;
&__input {} &-top {
display: grid;
grid-template-columns: 8fr 2fr;
align-items: flex-start;
justify-content: center;
gap: $padding-global;
}
&-bottom { &-bottom {
display: flex; display: flex;

View file

@ -5,11 +5,8 @@ import { Logs } from "../../../api/types";
import { useTimeState } from "../../../state/time/TimeStateContext"; import { useTimeState } from "../../../state/time/TimeStateContext";
import dayjs from "dayjs"; import dayjs from "dayjs";
const MAX_LINES = 1000; export const useFetchLogs = (server: string, query: string, limit: number) => {
export const useFetchLogs = (server: string, query: string) => {
const { period } = useTimeState(); 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>();
@ -22,7 +19,7 @@ export const useFetchLogs = (server: string, query: string) => {
const start = dayjs(period.start * 1000).tz().toISOString(); const start = dayjs(period.start * 1000).tz().toISOString();
const end = dayjs(period.end * 1000).tz().toISOString(); const end = dayjs(period.end * 1000).tz().toISOString();
const timerange = `_time:[${start}, ${end}]`; const timerange = `_time:[${start}, ${end}]`;
return `${timerange} AND (${query})`; return `${timerange} AND ${query}`;
} }
return query; return query;
}, [query, period]); }, [query, period]);
@ -30,13 +27,24 @@ export const useFetchLogs = (server: string, query: string) => {
const options = useMemo(() => ({ const options = useMemo(() => ({
method: "POST", method: "POST",
headers: { headers: {
"Accept": "application/stream+json; charset=utf-8", "Accept": "application/stream+json",
"Content-Type": "application/x-www-form-urlencoded",
}, },
body: `query=${encodeURIComponent(queryWithTime.trim())}` body: new URLSearchParams({
}), [queryWithTime]); query: queryWithTime.trim(),
limit: `${limit}`
})
}), [queryWithTime, limit]);
const parseLineToJSON = (line: string): Logs | null => {
try {
return JSON.parse(line);
} catch (e) {
return null;
}
};
const fetchLogs = useCallback(async () => { const fetchLogs = useCallback(async () => {
const limit = Number(options.body.get("limit")) + 1;
setIsLoading(true); setIsLoading(true);
setError(undefined); setError(undefined);
try { try {
@ -65,29 +73,21 @@ export const useFetchLogs = (server: string, query: string) => {
const lines = decoder.decode(value, { stream: true }).split("\n"); const lines = decoder.decode(value, { stream: true }).split("\n");
result.push(...lines); result.push(...lines);
// Trim result to MAX_LINES // Trim result to limit
if (result.length > MAX_LINES) { // This will lose its meaning with these changes:
result.splice(0, result.length - MAX_LINES); // https://github.com/VictoriaMetrics/VictoriaMetrics/pull/5778
if (result.length > limit) {
result.splice(0, result.length - limit);
} }
if (result.length >= MAX_LINES) { if (result.length >= limit) {
// Reached the maximum line limit // Reached the maximum line limit
reader.cancel(); reader.cancel();
break; break;
} }
} }
const data = result const data = result.map(parseLineToJSON).filter(line => line) as Logs[];
.map((line) => {
try {
return JSON.parse(line);
} catch (e) {
return "";
}
})
.filter(line => line);
setLogs(data); setLogs(data);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
setLogs([]); setLogs([]);