vmui: support for Prometheus data on the cardinality page (#4713)

* feat: add cardinality support for prometheus (#4320)

* docs/CHANGELOG.md: add cardinality support for prometheus

---------

Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
Yury Molodov 2023-09-01 10:51:44 +02:00 committed by Aliaksandr Valialkin
parent d8258be292
commit 9bd71b0f12
No known key found for this signature in database
GPG key ID: A72BEC6CD3D0DED1
9 changed files with 124 additions and 48 deletions

View file

@ -12,7 +12,7 @@ import CardinalityTotals, { CardinalityTotalsProps } from "../CardinalityTotals/
import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject";
import useStateSearchParams from "../../../hooks/useStateSearchParams";
const CardinalityConfigurator: FC<CardinalityTotalsProps> = (props) => {
const CardinalityConfigurator: FC<CardinalityTotalsProps> = ({ isPrometheus, ...props }) => {
const { isMobile } = useDeviceDetect();
const [searchParams] = useSearchParams();
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
@ -93,15 +93,20 @@ const CardinalityConfigurator: FC<CardinalityTotalsProps> = (props) => {
<TextField
label="Limit entries"
type="number"
value={topN}
value={isPrometheus ? 10 : topN}
error={errorTopN}
disabled={isPrometheus}
helperText={isPrometheus ? "not available for Prometheus" : ""}
onChange={handleTopNChange}
onEnter={handleRunQuery}
/>
</div>
</div>
<div className="vm-cardinality-configurator-bottom">
<CardinalityTotals {...props}/>
<CardinalityTotals
isPrometheus={isPrometheus}
{...props}
/>
<div className="vm-cardinality-configurator-bottom-helpful">
<a

View file

@ -13,13 +13,15 @@ export interface CardinalityTotalsProps {
totalSeriesPrev: number;
totalLabelValuePairs: number;
seriesCountByMetricName: TopHeapEntry[];
isPrometheus?: boolean;
}
const CardinalityTotals: FC<CardinalityTotalsProps> = ({
totalSeries,
totalSeriesPrev,
totalSeriesAll,
seriesCountByMetricName
totalSeries = 0,
totalSeriesPrev = 0,
totalSeriesAll = 0,
seriesCountByMetricName = [],
isPrometheus
}) => {
const { isMobile } = useDeviceDetect();
@ -36,7 +38,7 @@ const CardinalityTotals: FC<CardinalityTotalsProps> = ({
{
title: "Total series",
value: totalSeries.toLocaleString("en-US"),
dynamic: !totalSeries || !totalSeriesPrev ? "" : `${dynamic.toFixed(2)}%`,
dynamic: (!totalSeries || !totalSeriesPrev || isPrometheus) ? "" : `${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.

View file

@ -22,6 +22,7 @@ interface MetricsProperties {
sectionTitle: string;
tip?: string;
tableHeaderCells: HeadCell[];
isPrometheus: boolean;
}
const MetricsContent: FC<MetricsProperties> = ({
@ -34,10 +35,13 @@ const MetricsContent: FC<MetricsProperties> = ({
sectionTitle,
tip,
tableHeaderCells,
isPrometheus,
}) => {
const { isMobile } = useDeviceDetect();
const [activeTab, setActiveTab] = useState("table");
const noDataPrometheus = isPrometheus && !rows.length;
const tableCells = (row: Data) => (
<TableCells
row={row}
@ -90,8 +94,19 @@ const MetricsContent: FC<MetricsProperties> = ({
/>
</div>
</div>
{activeTab === "table" && (
{noDataPrometheus && (
<div className="vm-metrics-content-prom-data">
<div className="vm-metrics-content-prom-data__icon"><InfoIcon/></div>
<h3 className="vm-metrics-content-prom-data__title">
Prometheus Data Limitation
</h3>
<p className="vm-metrics-content-prom-data__text">
Due to missing data from your Prometheus source, some tables may appear empty.<br/>
This does not indicate an issue with your system or our tool.
</p>
</div>
)}
{!noDataPrometheus && activeTab === "table" && (
<div
ref={chartContainer}
className={classNames({
@ -107,7 +122,7 @@ const MetricsContent: FC<MetricsProperties> = ({
/>
</div>
)}
{activeTab === "graph" && (
{!noDataPrometheus && activeTab === "graph" && (
<div className="vm-metrics-content__chart">
<SimpleBarChart data={rows.map(({ name, value }) => ({ name, value }))}/>
</div>

View file

@ -59,4 +59,35 @@
&__chart {
padding-top: $padding-medium;
}
&-prom-data {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
gap: $padding-small;
margin-top: $padding-global;
text-align: center;
&__icon {
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
margin-bottom: $padding-small;
color: $color-primary;
}
&__title {
font-size: $font-size-medium;
font-weight: bold;
}
&__text {
max-width: 700px;
line-height: 1.3;
}
}
}

View file

@ -10,14 +10,17 @@ interface AppState {
export default class AppConfigurator {
private tsdbStatus: TSDBStatus;
private tabsNames: string[];
private isPrometheus: boolean;
constructor() {
this.tsdbStatus = this.defaultTSDBStatus;
this.tabsNames = ["table", "graph"];
this.isPrometheus = false;
this.getDefaultState = this.getDefaultState.bind(this);
}
set tsdbStatusData(tsdbStatus: TSDBStatus) {
this.isPrometheus = !!tsdbStatus?.headStats;
this.tsdbStatus = tsdbStatus;
}
@ -39,6 +42,10 @@ export default class AppConfigurator {
};
}
get isPrometheusData(): boolean {
return this.isPrometheus;
}
keys(match?: string | null, focusLabel?: string | null): string[] {
const isMetric = match && /__name__=".+"/.test(match);
const isLabel = match && /{.+=".+"}/g.test(match);

View file

@ -27,6 +27,30 @@ export const useFetchQuery = (): {
const [error, setError] = useState<ErrorTypes | string>();
const [tsdbStatus, setTSDBStatus] = useState<TSDBStatus>(appConfigurator.defaultTSDBStatus);
const getResponseJson = async (url: string) => {
const response = await fetch(url);
if (response.ok) {
return await response.json();
}
throw new Error(`Request failed with status ${response.status}`);
};
const calculateDiffs = (result: TSDBStatus, prevResult: TSDBStatus) => {
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;
});
}
});
};
const fetchCardinalityInfo = async (requestParams: CardinalityRequestsParams) => {
if (!serverUrl) return;
setError("");
@ -34,64 +58,52 @@ export const useFetchQuery = (): {
setTSDBStatus(appConfigurator.defaultTSDBStatus);
const totalParams = {
...requestParams,
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];
const urls = [
getCardinalityInfo(serverUrl, requestParams),
getCardinalityInfo(serverUrl, prevDayParams),
getCardinalityInfo(serverUrl, totalParams),
];
try {
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 [resp, respPrev, respTotals] = await Promise.all(urls.map(getResponseJson));
const name = match?.replace(/[{}"]/g, "");
result.seriesCountByLabelValuePair = result.seriesCountByLabelValuePair.filter(s => s.name !== name);
const prevResult = { ...respPrev.data };
const { data: dataTotal } = respTotals;
const result: TSDBStatus = {
...resp.data,
totalSeries: resp.data?.totalSeries || resp.data?.headStats?.numSeries || 0,
totalLabelValuePairs: resp.data?.totalLabelValuePairs || resp.data?.headStats?.numLabelValuePairs || 0,
seriesCountByLabelName: resp.data?.seriesCountByLabelName || [],
seriesCountByFocusLabelValue: resp.data?.seriesCountByFocusLabelValue || [],
totalSeriesByAll: dataTotal?.totalSeries || dataTotal?.headStats?.numSeries || 0,
totalSeriesPrev: prevResult?.totalSeries || prevResult?.headStats?.numSeries || 0,
};
Object.keys(result).forEach(k => {
const key = k as keyof TSDBStatus;
const entries = result[key];
const prevEntries = prevResult[key];
const name = match?.replace(/[{}"]/g, "");
result.seriesCountByLabelValuePair = result.seriesCountByLabelValuePair.filter(s => s.name !== name);
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;
});
}
});
calculateDiffs(result, prevResult);
setTSDBStatus(result);
setIsLoading(false);
} else {
setError(resp.error);
setTSDBStatus(appConfigurator.defaultTSDBStatus);
setIsLoading(false);
}
setTSDBStatus(result);
setIsLoading(false);
} catch (e) {
setIsLoading(false);
if (e instanceof Error) setError(`${e.name}: ${e.message}`);
}
};
useEffect(() => {
fetchCardinalityInfo({ topN, match, date, focusLabel });
}, [serverUrl, match, focusLabel, topN, date]);

View file

@ -56,6 +56,7 @@ const CardinalityPanel: FC = () => {
>
{isLoading && <Spinner message={spinnerMessage}/>}
<CardinalityConfigurator
isPrometheus={appConfigurator.isPrometheusData}
totalSeries={tsdbStatusData.totalSeries}
totalSeriesPrev={tsdbStatusData.totalSeriesPrev}
totalSeriesAll={tsdbStatusData.totalSeriesByAll}
@ -86,6 +87,7 @@ const CardinalityPanel: FC = () => {
totalSeriesPrev={appConfigurator.totalSeries(keyName, true)}
totalSeries={appConfigurator.totalSeries(keyName)}
tableHeaderCells={tablesHeaders[keyName]}
isPrometheus={appConfigurator.isPrometheusData}
/>;
})}
</div>

View file

@ -10,6 +10,7 @@ export interface TSDBStatus {
seriesCountByFocusLabelValue: TopHeapEntry[];
seriesCountByLabelValuePair: TopHeapEntry[];
labelValueCountByLabelName: TopHeapEntry[];
headStats?: object;
}
export interface TopHeapEntry {

View file

@ -31,6 +31,7 @@ The following `tip` changes can be tested by building VictoriaMetrics components
* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): retry failed write request on the closed connection immediately, without waiting for backoff. This should improve data delivery speed and reduce amount of error logs emitted by vmagent when using idle connections. See related [issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4139).
* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): reduces load on Kubernetes control plane during initial service discovery. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4855) for details.
* FEATURE: [VictoriaMetrics cluster](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html): reduce the maximum recovery time at `vmselect` and `vminsert` when some of `vmstorage` nodes become unavailable because of networking issues from 60 seconds to 3 seconds by default. The recovery time can be tuned at `vmselect` and `vminsert` nodes with `-vmstorageUserTimeout` command-line flag if needed. Thanks to @wjordan for [the pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/4423).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add Prometheus data support to the "Explore cardinality" page. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4320) for details.
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): make the warning message more noticeable for text fields. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4848).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add button for auto-formatting PromQL/MetricsQL queries. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4681). Thanks to @aramattamara for the [pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/4694).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): improve accessibility score to 100 according to [Google's Lighthouse](https://developer.chrome.com/docs/lighthouse/accessibility/) tests.