mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2025-01-10 15:14:09 +00:00
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:
parent
7675bc8f27
commit
e00d313333
6 changed files with 85 additions and 80 deletions
|
@ -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 />}
|
||||||
|
|
|
@ -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}/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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([]);
|
||||||
|
|
Loading…
Reference in a new issue