mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
vmui: add a comparison of data to the Cardinality Explorer
(#4123)
* feat: add button "show today" to date picker * feat: add comparison with the prev day (#3967) * vmui/docs: add comparison of data to cardinality page
This commit is contained in:
parent
382a2ff649
commit
3c45256736
19 changed files with 227 additions and 59 deletions
|
@ -1,4 +1,4 @@
|
|||
@use "../../../../styles/variables" as *;
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-tenant-input {
|
||||
position: relative;
|
||||
|
|
|
@ -8,6 +8,7 @@ import "./style.scss";
|
|||
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
|
||||
import classNames from "classnames";
|
||||
import MonthsList from "./MonthsList/MonthsList";
|
||||
import Button from "../../Button/Button";
|
||||
|
||||
interface DatePickerProps {
|
||||
date: Date | Dayjs
|
||||
|
@ -29,6 +30,9 @@ const Calendar: FC<DatePickerProps> = ({
|
|||
const [viewType, setViewType] = useState<CalendarTypeView>(CalendarTypeView.days);
|
||||
const [viewDate, setViewDate] = useState(dayjs.tz(date));
|
||||
const [selectDate, setSelectDate] = useState(dayjs.tz(date));
|
||||
|
||||
const today = dayjs().startOf("day").tz();
|
||||
const viewDateIsToday = today.format() === viewDate.format();
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const toggleDisplayYears = () => {
|
||||
|
@ -44,6 +48,10 @@ const Calendar: FC<DatePickerProps> = ({
|
|||
setSelectDate(date);
|
||||
};
|
||||
|
||||
const handleToday = () => {
|
||||
setViewDate(today);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectDate.format() === dayjs.tz(date).format()) return;
|
||||
onChange(selectDate.format(format));
|
||||
|
@ -88,6 +96,17 @@ const Calendar: FC<DatePickerProps> = ({
|
|||
onChangeViewDate={handleChangeViewDate}
|
||||
/>
|
||||
)}
|
||||
{!viewDateIsToday && (viewType === CalendarTypeView.days) && (
|
||||
<div className="vm-calendar-footer">
|
||||
<Button
|
||||
variant="text"
|
||||
size="small"
|
||||
onClick={handleToday}
|
||||
>
|
||||
show today
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@ interface CalendarBodyProps {
|
|||
const weekday = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
||||
|
||||
const CalendarBody: FC<CalendarBodyProps> = ({ viewDate, selectDate, onChangeSelectDate }) => {
|
||||
const format = "YYYY-MM-DD";
|
||||
const today = dayjs().tz().startOf("day");
|
||||
|
||||
const days: (Dayjs|null)[] = useMemo(() => {
|
||||
|
@ -45,10 +46,10 @@ const CalendarBody: FC<CalendarBodyProps> = ({ viewDate, selectDate, onChangeSel
|
|||
"vm-calendar-body-cell": true,
|
||||
"vm-calendar-body-cell_day": true,
|
||||
"vm-calendar-body-cell_day_empty": !d,
|
||||
"vm-calendar-body-cell_day_active": (d && d.toISOString()) === selectDate.startOf("day").toISOString(),
|
||||
"vm-calendar-body-cell_day_today": (d && d.toISOString()) === today.toISOString()
|
||||
"vm-calendar-body-cell_day_active": (d && d.format(format)) === selectDate.format(format),
|
||||
"vm-calendar-body-cell_day_today": (d && d.format(format)) === today.format(format)
|
||||
})}
|
||||
key={d ? d.toISOString() : i}
|
||||
key={d ? d.format(format) : i}
|
||||
onClick={createHandlerSelectDate(d)}
|
||||
>
|
||||
{d && d.format("D")}
|
||||
|
|
|
@ -166,4 +166,10 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,12 +10,14 @@ import "./style.scss";
|
|||
export interface CardinalityTotalsProps {
|
||||
totalSeries: number;
|
||||
totalSeriesAll: number;
|
||||
totalSeriesPrev: number;
|
||||
totalLabelValuePairs: number;
|
||||
seriesCountByMetricName: TopHeapEntry[];
|
||||
}
|
||||
|
||||
const CardinalityTotals: FC<CardinalityTotalsProps> = ({
|
||||
totalSeries,
|
||||
totalSeriesPrev,
|
||||
totalSeriesAll,
|
||||
seriesCountByMetricName
|
||||
}) => {
|
||||
|
@ -27,11 +29,14 @@ const CardinalityTotals: FC<CardinalityTotalsProps> = ({
|
|||
const isMetric = /__name__/.test(match || "");
|
||||
|
||||
const progress = seriesCountByMetricName[0]?.value / totalSeriesAll * 100;
|
||||
const diff = totalSeries - totalSeriesPrev;
|
||||
const dynamic = Math.abs(diff) / totalSeriesPrev * 100;
|
||||
|
||||
const totals = [
|
||||
{
|
||||
title: "Total series",
|
||||
value: totalSeries.toLocaleString("en-US"),
|
||||
dynamic: !totalSeries || !totalSeriesPrev ? "" : `${dynamic.toFixed(2)}%`,
|
||||
display: !focusLabel,
|
||||
info: `The total number of active time series.
|
||||
A time series is uniquely identified by its name plus a set of its labels.
|
||||
|
@ -57,20 +62,33 @@ const CardinalityTotals: FC<CardinalityTotalsProps> = ({
|
|||
"vm-cardinality-totals_mobile": isMobile
|
||||
})}
|
||||
>
|
||||
{totals.map(({ title, value, info }) => (
|
||||
{totals.map(({ title, value, info, dynamic }) => (
|
||||
<div
|
||||
className="vm-cardinality-totals-card"
|
||||
key={title}
|
||||
>
|
||||
<div className="vm-cardinality-totals-card-header">
|
||||
<h4 className="vm-cardinality-totals-card__title">
|
||||
{title}
|
||||
{info && (
|
||||
<Tooltip title={<p className="vm-cardinality-totals-card-header__tooltip">{info}</p>}>
|
||||
<div className="vm-cardinality-totals-card-header__info-icon"><InfoIcon/></div>
|
||||
<Tooltip title={<p className="vm-cardinality-totals-card__tooltip">{info}</p>}>
|
||||
<div className="vm-cardinality-totals-card__info-icon"><InfoIcon/></div>
|
||||
</Tooltip>
|
||||
)}
|
||||
<h4 className="vm-cardinality-totals-card-header__title">{title}</h4>
|
||||
</div>
|
||||
</h4>
|
||||
<span className="vm-cardinality-totals-card__value">{value}</span>
|
||||
{!!dynamic && (
|
||||
<Tooltip title={`in relation to the previous day: ${totalSeriesPrev.toLocaleString("en-US")}`}>
|
||||
<span
|
||||
className={classNames({
|
||||
"vm-dynamic-number": true,
|
||||
"vm-dynamic-number_positive vm-dynamic-number_down": diff < 0,
|
||||
"vm-dynamic-number_negative vm-dynamic-number_up": diff > 0,
|
||||
})}
|
||||
>
|
||||
{dynamic}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -5,56 +5,52 @@
|
|||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
justify-content: flex-start;
|
||||
gap: $padding-global;
|
||||
gap: $padding-medium;
|
||||
flex-grow: 1;
|
||||
|
||||
&_mobile {
|
||||
gap: $padding-small;
|
||||
gap: $padding-global;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&-card {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
gap: $padding-small 4px;
|
||||
|
||||
&-header {
|
||||
&__info-icon {
|
||||
width: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
&__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 4px;
|
||||
grid-column: 1/-1;
|
||||
color: $color-text;
|
||||
}
|
||||
|
||||
&__info-icon {
|
||||
width: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-weight: bold;
|
||||
color: $color-text;
|
||||
|
||||
&:after {
|
||||
content: ':';
|
||||
}
|
||||
}
|
||||
|
||||
&__tooltip {
|
||||
max-width: 280px;
|
||||
white-space: normal;
|
||||
padding: $padding-small;
|
||||
line-height: 130%;
|
||||
font-size: $font-size;
|
||||
}
|
||||
&__tooltip {
|
||||
max-width: 280px;
|
||||
white-space: normal;
|
||||
padding: $padding-small;
|
||||
line-height: $font-size;
|
||||
font-size: $font-size;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-weight: bold;
|
||||
color: $color-primary;
|
||||
font-size: $font-size-medium;
|
||||
font-size: $font-size-large;
|
||||
line-height: $font-size;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ interface MetricsProperties {
|
|||
tabs: string[];
|
||||
chartContainer: MutableRef<HTMLDivElement> | undefined;
|
||||
totalSeries: number,
|
||||
totalSeriesPrev: number,
|
||||
sectionTitle: string;
|
||||
tip?: string;
|
||||
tableHeaderCells: HeadCell[];
|
||||
|
@ -28,6 +29,7 @@ const MetricsContent: FC<MetricsProperties> = ({
|
|||
tabs: tabsProps = [],
|
||||
chartContainer,
|
||||
totalSeries,
|
||||
totalSeriesPrev,
|
||||
onActionClick,
|
||||
sectionTitle,
|
||||
tip,
|
||||
|
@ -40,6 +42,7 @@ const MetricsContent: FC<MetricsProperties> = ({
|
|||
<TableCells
|
||||
row={row}
|
||||
totalSeries={totalSeries}
|
||||
totalSeriesPrev={totalSeriesPrev}
|
||||
onActionClick={onActionClick}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -26,7 +26,7 @@ const EnhancedTable: FC<TableProps> = ({
|
|||
const sortedData = stableSort(rows, getComparator(order, orderBy));
|
||||
|
||||
return (
|
||||
<table className="vm-table">
|
||||
<table className="vm-table vm-cardinality-panel-table">
|
||||
<EnhancedTableHead
|
||||
order={order}
|
||||
orderBy={orderBy}
|
||||
|
|
|
@ -4,15 +4,27 @@ import LineProgress from "../../../../components/Main/LineProgress/LineProgress"
|
|||
import { PlayCircleOutlineIcon } from "../../../../components/Main/Icons";
|
||||
import Button from "../../../../components/Main/Button/Button";
|
||||
import Tooltip from "../../../../components/Main/Tooltip/Tooltip";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface CardinalityTableCells {
|
||||
row: Data,
|
||||
totalSeries: number;
|
||||
totalSeriesPrev: number;
|
||||
onActionClick: (name: string) => void;
|
||||
}
|
||||
|
||||
const TableCells: FC<CardinalityTableCells> = ({ row, totalSeries, onActionClick }) => {
|
||||
const TableCells: FC<CardinalityTableCells> = ({
|
||||
row,
|
||||
totalSeries,
|
||||
totalSeriesPrev,
|
||||
onActionClick
|
||||
}) => {
|
||||
const progress = totalSeries > 0 ? row.value / totalSeries * 100 : -1;
|
||||
const progressPrev = totalSeriesPrev > 0 ? row.valuePrev / totalSeriesPrev * 100 : -1;
|
||||
const hasProgresses = [progress, progressPrev].some(p => p === -1);
|
||||
|
||||
const diffPercent = progress - progressPrev;
|
||||
const relationPrevDay = hasProgresses ? "" : `${diffPercent.toFixed(2)}%`;
|
||||
|
||||
const handleActionClick = () => {
|
||||
onActionClick(row.name);
|
||||
|
@ -35,13 +47,42 @@ const TableCells: FC<CardinalityTableCells> = ({ row, totalSeries, onActionClick
|
|||
key={row.value}
|
||||
>
|
||||
{row.value}
|
||||
|
||||
{!!row.diff && (
|
||||
<Tooltip title={`in relation to the previous day: ${row.valuePrev}`}>
|
||||
<span
|
||||
className={classNames({
|
||||
"vm-dynamic-number": true,
|
||||
"vm-dynamic-number_positive": row.diff < 0,
|
||||
"vm-dynamic-number_negative": row.diff > 0,
|
||||
})}
|
||||
>
|
||||
{row.diff > 0 ? "+" : ""}{row.diff}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</td>
|
||||
{progress > 0 && (
|
||||
<td
|
||||
className="vm-table-cell"
|
||||
key={row.progressValue}
|
||||
>
|
||||
<LineProgress value={progress}/>
|
||||
<div className="vm-cardinality-panel-table__progress">
|
||||
<LineProgress value={progress}/>
|
||||
{relationPrevDay && (
|
||||
<Tooltip title={"in relation to the previous day"}>
|
||||
<span
|
||||
className={classNames({
|
||||
"vm-dynamic-number": true,
|
||||
"vm-dynamic-number_positive vm-dynamic-number_down": diffPercent < 0,
|
||||
"vm-dynamic-number_negative vm-dynamic-number_up": diffPercent > 0,
|
||||
})}
|
||||
>
|
||||
{relationPrevDay}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
<td
|
||||
|
|
|
@ -12,7 +12,7 @@ export function EnhancedTableHead(props: EnhancedHeaderTableProps) {
|
|||
};
|
||||
|
||||
return (
|
||||
<thead className="vm-table-header">
|
||||
<thead className="vm-table-header vm-cardinality-panel-table__header">
|
||||
<tr className="vm-table__row vm-table__row_header">
|
||||
{headerCells.map((headCell) => (
|
||||
<th
|
||||
|
|
|
@ -28,6 +28,8 @@ export interface TableProps {
|
|||
export interface Data {
|
||||
name: string;
|
||||
value: number;
|
||||
diff: number;
|
||||
valuePrev: number;
|
||||
progressValue: number;
|
||||
actions: string;
|
||||
}
|
||||
|
|
|
@ -28,8 +28,9 @@ export default class AppConfigurator {
|
|||
get defaultTSDBStatus(): TSDBStatus {
|
||||
return {
|
||||
totalSeries: 0,
|
||||
totalLabelValuePairs: 0,
|
||||
totalSeriesPrev: 0,
|
||||
totalSeriesByAll: 0,
|
||||
totalLabelValuePairs: 0,
|
||||
seriesCountByMetricName: [],
|
||||
seriesCountByLabelName: [],
|
||||
seriesCountByFocusLabelValue: [],
|
||||
|
@ -142,11 +143,11 @@ export default class AppConfigurator {
|
|||
};
|
||||
}
|
||||
|
||||
totalSeries(keyName: string): number {
|
||||
totalSeries(keyName: string, prev = false): number {
|
||||
if (keyName === "labelValueCountByLabelName") {
|
||||
return -1;
|
||||
}
|
||||
return this.tsdbStatus.totalSeries;
|
||||
return prev ? this.tsdbStatus.totalSeriesPrev : this.tsdbStatus.totalSeries;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -33,24 +33,51 @@ export const useFetchQuery = (): {
|
|||
setIsLoading(true);
|
||||
setTSDBStatus(appConfigurator.defaultTSDBStatus);
|
||||
|
||||
const defaultParams = { date: requestParams.date, topN: 0, match: "", focusLabel: "" } as CardinalityRequestsParams;
|
||||
const url = getCardinalityInfo(serverUrl, requestParams);
|
||||
const urlDefault = getCardinalityInfo(serverUrl, defaultParams);
|
||||
const totalParams = {
|
||||
date: requestParams.date,
|
||||
topN: 0,
|
||||
match: "",
|
||||
focusLabel: ""
|
||||
} as CardinalityRequestsParams;
|
||||
|
||||
const prevDayParams = {
|
||||
...requestParams,
|
||||
date: dayjs(requestParams.date).subtract(1, "day").tz().format(DATE_FORMAT),
|
||||
} as CardinalityRequestsParams;
|
||||
|
||||
|
||||
const urlBase = getCardinalityInfo(serverUrl, requestParams);
|
||||
const urlPrev = getCardinalityInfo(serverUrl, prevDayParams);
|
||||
const uslTotal = getCardinalityInfo(serverUrl, totalParams);
|
||||
const urls = [urlBase, urlPrev, uslTotal];
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const resp = await response.json();
|
||||
const responseTotal = await fetch(urlDefault);
|
||||
const respTotals = await responseTotal.json();
|
||||
if (response.ok) {
|
||||
const { data } = resp;
|
||||
const { totalSeries } = respTotals.data;
|
||||
const result = { ...data } as TSDBStatus;
|
||||
result.totalSeriesByAll = totalSeries;
|
||||
const responses = await Promise.all(urls.map(url => fetch(url)));
|
||||
const [resp, respPrev, respTotals] = await Promise.all(responses.map(resp => resp.json()));
|
||||
if (responses[0].ok) {
|
||||
const { data: dataTotal } = respTotals;
|
||||
const prevResult = { ...respPrev.data } as TSDBStatus;
|
||||
const result = { ...resp.data } as TSDBStatus;
|
||||
result.totalSeriesByAll = dataTotal?.totalSeries;
|
||||
result.totalSeriesPrev = prevResult?.totalSeries;
|
||||
|
||||
const name = match?.replace(/[{}"]/g, "");
|
||||
result.seriesCountByLabelValuePair = result.seriesCountByLabelValuePair.filter(s => s.name !== name);
|
||||
|
||||
Object.keys(result).forEach(k => {
|
||||
const key = k as keyof TSDBStatus;
|
||||
const entries = result[key];
|
||||
const prevEntries = prevResult[key];
|
||||
|
||||
if (Array.isArray(entries) && Array.isArray(prevEntries)) {
|
||||
entries.forEach((entry) => {
|
||||
const valuePrev = prevEntries.find(prevEntry => prevEntry.name === entry.name)?.value;
|
||||
entry.diff = valuePrev ? entry.value - valuePrev : 0;
|
||||
entry.valuePrev = valuePrev || 0;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
setTSDBStatus(result);
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
|
|
|
@ -55,6 +55,7 @@ const CardinalityPanel: FC = () => {
|
|||
{isLoading && <Spinner message={spinnerMessage}/>}
|
||||
<CardinalityConfigurator
|
||||
totalSeries={tsdbStatusData.totalSeries}
|
||||
totalSeriesPrev={tsdbStatusData.totalSeriesPrev}
|
||||
totalSeriesAll={tsdbStatusData.totalSeriesByAll}
|
||||
totalLabelValuePairs={tsdbStatusData.totalLabelValuePairs}
|
||||
seriesCountByMetricName={tsdbStatusData.seriesCountByMetricName}
|
||||
|
@ -80,6 +81,7 @@ const CardinalityPanel: FC = () => {
|
|||
onActionClick={handleFilterClick(keyName)}
|
||||
tabs={defaultState.tabs[keyName as keyof Tabs]}
|
||||
chartContainer={defaultState.containerRefs[keyName as keyof Containers<HTMLDivElement>]}
|
||||
totalSeriesPrev={appConfigurator.totalSeries(keyName, true)}
|
||||
totalSeries={appConfigurator.totalSeries(keyName)}
|
||||
tableHeaderCells={tablesHeaders[keyName]}
|
||||
/>
|
||||
|
|
|
@ -18,4 +18,25 @@
|
|||
flex-grow: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-table {
|
||||
&__header {
|
||||
th:first-child {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
th:not(:first-child) {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&__progress {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(200px, 1fr) 70px;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: $padding-small;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ export interface TSDBStatus {
|
|||
totalSeries: number;
|
||||
totalLabelValuePairs: number;
|
||||
totalSeriesByAll: number,
|
||||
totalSeriesPrev: number,
|
||||
seriesCountByMetricName: TopHeapEntry[];
|
||||
seriesCountByLabelName: TopHeapEntry[];
|
||||
seriesCountByFocusLabelValue: TopHeapEntry[];
|
||||
|
@ -14,6 +15,8 @@ export interface TSDBStatus {
|
|||
export interface TopHeapEntry {
|
||||
name: string;
|
||||
value: number;
|
||||
diff: number;
|
||||
valuePrev: number;
|
||||
}
|
||||
|
||||
interface QueryUpdaterArgs {
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-dynamic-number {
|
||||
font-size: $font-size-small;
|
||||
color: $color-text-disabled;
|
||||
|
||||
&_positive {
|
||||
color: $color-success;
|
||||
}
|
||||
|
||||
&_negative {
|
||||
color: $color-error;
|
||||
}
|
||||
|
||||
&_down {
|
||||
&:before {
|
||||
content: "↓";
|
||||
}
|
||||
}
|
||||
|
||||
&_up {
|
||||
&:before {
|
||||
content: "↑";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@
|
|||
@forward "./components/sectionheader";
|
||||
@forward "./components/table";
|
||||
@forward "./components/link";
|
||||
@forward "./components/dynamic-number";
|
||||
|
||||
:root {
|
||||
/* base palette */
|
||||
|
|
|
@ -25,6 +25,7 @@ The following tip changes can be tested by building VictoriaMetrics components f
|
|||
* FEATURE: introduce `-http.maxConcurrentRequests` command-line flag to protect VM components from resource exhaustion during unexpected spikes of HTTP requests. By default, the new flag's value is set to 0 which means no limits are applied.
|
||||
* FEATURE: [vmctl](https://docs.victoriametrics.com/vmctl.html): add support for the different time formats for `--vm-native-filter-time-start` and `--vm-native-filter-time-end` flags if the native binary protocol is used for migration. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4091).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): integrate WITH template playground. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3811).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add a comparison of data from the previous day with data from the current day to the `Cardinality Explorer`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3967).
|
||||
* FEATURE: [vmauth](https://docs.victoriametrics.com/vmauth.html): add ability to filter incoming requests by IP. See [these docs](https://docs.victoriametrics.com/vmauth.html#ip-filters) and [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3491).
|
||||
|
||||
* BUGFIX: reduce the probability of sudden increase in the number of small parts on systems with small number of CPU cores.
|
||||
|
|
Loading…
Reference in a new issue