vmui: add last/max/avg values (#3789)

* feat: add last/max/avg values (#3706)

* fix: change filter exclude values

* app/vmui: wip

- improve the visualization for avg/max/last values
- make getAvgFromArray() function resilient against inf/undefined/nil
- export getLastFromArray() function, which is resilient against inf/undefined/nil
- run `make vmui-update`

---------

Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
Yury Molodov 2023-02-09 07:41:20 +01:00 committed by GitHub
parent 114c14febf
commit 8afc0aef8d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 111 additions and 48 deletions

View file

@ -1,14 +1,14 @@
{ {
"files": { "files": {
"main.css": "./static/css/main.a6dcf95f.css", "main.css": "./static/css/main.f22be84b.css",
"main.js": "./static/js/main.83e96a22.js", "main.js": "./static/js/main.eca4a392.js",
"static/js/27.c1ccfd29.chunk.js": "./static/js/27.c1ccfd29.chunk.js", "static/js/27.c1ccfd29.chunk.js": "./static/js/27.c1ccfd29.chunk.js",
"static/media/Lato-Regular.ttf": "./static/media/Lato-Regular.d714fec1633b69a9c2e9.ttf", "static/media/Lato-Regular.ttf": "./static/media/Lato-Regular.d714fec1633b69a9c2e9.ttf",
"static/media/Lato-Bold.ttf": "./static/media/Lato-Bold.32360ba4b57802daa4d6.ttf", "static/media/Lato-Bold.ttf": "./static/media/Lato-Bold.32360ba4b57802daa4d6.ttf",
"index.html": "./index.html" "index.html": "./index.html"
}, },
"entrypoints": [ "entrypoints": [
"static/css/main.a6dcf95f.css", "static/css/main.f22be84b.css",
"static/js/main.83e96a22.js" "static/js/main.eca4a392.js"
] ]
} }

View file

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"/><meta name="theme-color" content="#000000"/><meta name="description" content="UI for VictoriaMetrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><script src="./dashboards/index.js" type="module"></script><meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="./preview.jpg"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:site" content="@VictoriaMetrics"><meta property="og:title" content="Metric explorer for VictoriaMetrics"><meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta property="og:image" content="./preview.jpg"><meta property="og:type" content="website"><script defer="defer" src="./static/js/main.83e96a22.js"></script><link href="./static/css/main.a6dcf95f.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html> <!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"/><meta name="theme-color" content="#000000"/><meta name="description" content="UI for VictoriaMetrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><script src="./dashboards/index.js" type="module"></script><meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="./preview.jpg"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:site" content="@VictoriaMetrics"><meta property="og:title" content="Metric explorer for VictoriaMetrics"><meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta property="og:image" content="./preview.jpg"><meta property="og:type" content="website"><script defer="defer" src="./static/js/main.eca4a392.js"></script><link href="./static/css/main.f22be84b.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

View file

@ -1,5 +1,5 @@
import React, { FC, useEffect, useMemo, useRef, useState } from "preact/compat"; import React, { FC, useEffect, useMemo, useRef, useState } from "preact/compat";
import uPlot, { Series } from "uplot"; import uPlot from "uplot";
import { MetricResult } from "../../../api/types"; import { MetricResult } from "../../../api/types";
import { formatPrettyNumber } from "../../../utils/uplot/helpers"; import { formatPrettyNumber } from "../../../utils/uplot/helpers";
import dayjs from "dayjs"; import dayjs from "dayjs";
@ -11,12 +11,13 @@ import { CloseIcon, DragIcon } from "../../Main/Icons";
import classNames from "classnames"; import classNames from "classnames";
import { MouseEvent as ReactMouseEvent } from "react"; import { MouseEvent as ReactMouseEvent } from "react";
import "./style.scss"; import "./style.scss";
import { SeriesItem } from "../../../utils/uplot/series";
export interface ChartTooltipProps { export interface ChartTooltipProps {
id: string, id: string,
u: uPlot, u: uPlot,
metrics: MetricResult[], metrics: MetricResult[],
series: Series[], series: SeriesItem[],
yRange: number[]; yRange: number[];
unit?: string, unit?: string,
isSticky?: boolean, isSticky?: boolean,
@ -55,6 +56,8 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
const color = series[seriesIdx]?.stroke+""; const color = series[seriesIdx]?.stroke+"";
const calculations = series[seriesIdx]?.calculations || {};
const groups = new Set(metrics.map(m => m.group)); const groups = new Set(metrics.map(m => m.group));
const showQueryNum = groups.size > 1; const showQueryNum = groups.size > 1;
const group = metrics[seriesIdx-1]?.group || 0; const group = metrics[seriesIdx-1]?.group || 0;
@ -175,9 +178,14 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
style={{ background: color }} style={{ background: color }}
/> />
<p> <p>
{metricName}: {calculations.last !== undefined && (
<b className="vm-chart-tooltip-data__value">{valueFormat}</b> <div>
{unit} avg:<b>{calculations.avg}</b>,
max:<b>{calculations.max}</b>,
last:<b>{calculations.last}</b>
</div>
)}
{metricName}:<b>{valueFormat}{unit}</b>
</p> </p>
</div> </div>
{!!fields.length && ( {!!fields.length && (

View file

@ -61,20 +61,9 @@ $chart-tooltip-y: -1 * ($padding-small + $chart-tooltip-half-icon);
word-break: break-all; word-break: break-all;
line-height: 12px; line-height: 12px;
&__value {
padding: 4px;
font-weight: bold;
}
&__marker { &__marker {
width: 12px; width: 12px;
height: 12px; height: 12px;
} }
} }
&-info {
display: grid;
grid-gap: 4px;
word-break: break-all;
}
} }

View file

@ -14,6 +14,7 @@ interface LegendItemProps {
const LegendItem: FC<LegendItemProps> = ({ legend, onChange }) => { const LegendItem: FC<LegendItemProps> = ({ legend, onChange }) => {
const [copiedValue, setCopiedValue] = useState(""); const [copiedValue, setCopiedValue] = useState("");
const freeFormFields = useMemo(() => getFreeFields(legend), [legend]); const freeFormFields = useMemo(() => getFreeFields(legend), [legend]);
const calculations = legend.calculations;
const handleClickFreeField = async (val: string, id: string) => { const handleClickFreeField = async (val: string, id: string) => {
await navigator.clipboard.writeText(val); await navigator.clipboard.writeText(val);
@ -30,11 +31,11 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange }) => {
handleClickFreeField(freeField, id); handleClickFreeField(freeField, id);
}; };
return ( return (
<div <div
className={classNames({ className={classNames({
"vm-legend-item": true, "vm-legend-item": true,
"vm-legend-row": true,
"vm-legend-item_hide": !legend.checked, "vm-legend-item_hide": !legend.checked,
})} })}
onClick={createHandlerClick(legend)} onClick={createHandlerClick(legend)}
@ -70,6 +71,11 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange }) => {
</span> </span>
} }
</div> </div>
{calculations.last !== undefined && (
<div className="vm-legend-item-values">
avg:{calculations.avg}, max:{calculations.max}, last:{calculations.last}
</div>
)}
</div> </div>
); );
}; };

View file

@ -10,6 +10,7 @@
background-color: $color-background-block; background-color: $color-background-block;
cursor: pointer; cursor: pointer;
transition: 0.2s ease; transition: 0.2s ease;
margin-bottom: $padding-small;
&:hover { &:hover {
background-color: rgba(0, 0, 0, 0.1); background-color: rgba(0, 0, 0, 0.1);
@ -33,10 +34,11 @@
word-break: break-all; word-break: break-all;
&__label { &__label {
margin-right: 2px;
} }
&__free-fields { &__free-fields {
padding: 3px; padding: 2px;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
@ -48,4 +50,11 @@
} }
} }
} }
&-values {
grid-column: 2;
display: flex;
align-items: center;
gap: $padding-small;
}
} }

View file

@ -9,12 +9,13 @@
&-group { &-group {
min-width: 23%; min-width: 23%;
width: 100%;
margin: 0 $padding-global $padding-global 0; margin: 0 $padding-global $padding-global 0;
&-title { &-title {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 $padding-small $padding-small; padding: $padding-small;
margin-bottom: 1px; margin-bottom: 1px;
border-bottom: $border-divider; border-bottom: $border-divider;

View file

@ -22,6 +22,7 @@ import classNames from "classnames";
import ChartTooltip, { ChartTooltipProps } from "../ChartTooltip/ChartTooltip"; import ChartTooltip, { ChartTooltipProps } from "../ChartTooltip/ChartTooltip";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useAppState } from "../../../state/common/StateContext"; import { useAppState } from "../../../state/common/StateContext";
import { SeriesItem } from "../../../utils/uplot/series";
export interface LineChartProps { export interface LineChartProps {
metrics: MetricResult[]; metrics: MetricResult[];
@ -316,7 +317,7 @@ const LineChart: FC<LineChartProps> = ({
<ChartTooltip <ChartTooltip
unit={unit} unit={unit}
u={uPlotInst} u={uPlotInst}
series={series} series={series as SeriesItem[]}
metrics={metrics} metrics={metrics}
yRange={yRange} yRange={yRange}
tooltipIdx={tooltipIdx} tooltipIdx={tooltipIdx}

View file

@ -12,6 +12,7 @@ import { getAvgFromArray, getMaxFromArray, getMinFromArray } from "../../../util
import classNames from "classnames"; import classNames from "classnames";
import { useTimeState } from "../../../state/time/TimeStateContext"; import { useTimeState } from "../../../state/time/TimeStateContext";
import "./style.scss"; import "./style.scss";
import { promValueToNumber } from "../../../utils/metric";
export interface GraphViewProps { export interface GraphViewProps {
data?: MetricResult[]; data?: MetricResult[];
@ -28,21 +29,6 @@ export interface GraphViewProps {
height?: number height?: number
} }
const promValueToNumber = (s: string): number => {
// See https://prometheus.io/docs/prometheus/latest/querying/api/#expression-query-result-formats
switch (s) {
case "NaN":
return NaN;
case "Inf":
case "+Inf":
return Infinity;
case "-Inf":
return -Infinity;
default:
return parseFloat(s);
}
};
const GraphView: FC<GraphViewProps> = ({ const GraphView: FC<GraphViewProps> = ({
data = [], data = [],
period, period,

View file

@ -40,7 +40,7 @@ const Index: FC = () => {
const getQueryStatsTitle = (key: keyof TopQueryStats) => { const getQueryStatsTitle = (key: keyof TopQueryStats) => {
if (!data) return key; if (!data) return key;
const value = data[key]; const value = data[key];
if (typeof value === "number") return formatPrettyNumber(value); if (typeof value === "number") return formatPrettyNumber(value, value, value);
return value || key; return value || key;
}; };

View file

@ -22,4 +22,25 @@ export const getMinFromArray = (a: number[]) => {
return Number.isFinite(min) ? min : null; return Number.isFinite(min) ? min : null;
}; };
export const getAvgFromArray = (a: number[]) => a.reduce((a,b) => a+b) / a.length; export const getAvgFromArray = (a: number[]) => {
let mean = a[0];
let n = 1;
for (let i = 1; i < a.length; i++) {
const v = a[i];
if (Number.isFinite(v)) {
mean = mean * (n-1)/n + v / n;
n++;
}
}
return mean;
};
export const getLastFromArray = (a: number[]) => {
let len = a.length;
while (len--) {
const v = a[len];
if (Number.isFinite(v)) {
return v;
}
}
};

View file

@ -13,3 +13,18 @@ export const getNameForMetric = (result: MetricBase, alias?: string, showQueryNu
`${e[0]}=${JSON.stringify(e[1])}` `${e[0]}=${JSON.stringify(e[1])}`
).join(", ")}}`; ).join(", ")}}`;
}; };
export const promValueToNumber = (s: string): number => {
// See https://prometheus.io/docs/prometheus/latest/querying/api/#expression-query-result-formats
switch (s) {
case "NaN":
return NaN;
case "Inf":
case "+Inf":
return Infinity;
case "-Inf":
return -Infinity;
default:
return parseFloat(s);
}
};

View file

@ -32,10 +32,12 @@ export const formatTicks = (u: uPlot, ticks: number[], unit = ""): string[] => {
return ticks.map(v => `${formatPrettyNumber(v, min, max)} ${unit}`); return ticks.map(v => `${formatPrettyNumber(v, min, max)} ${unit}`);
}; };
export const formatPrettyNumber = (n: number | null | undefined, min = 0, max = 0): string => { export const formatPrettyNumber = (n: number | null | undefined, min: number | null | undefined, max: number | null | undefined): string => {
if (n === undefined || n === null) { if (n === undefined || n === null) {
return ""; return "";
} }
max = max || 0;
min = min || 0;
const range = Math.abs(max - min); const range = Math.abs(max - min);
if (isNaN(range) || range == 0) { if (isNaN(range) || range == 0) {
// Return the constant number as is if the range isn't set of it is too small. // Return the constant number as is if the range isn't set of it is too small.

View file

@ -1,12 +1,19 @@
import { MetricResult } from "../../api/types"; import { MetricResult } from "../../api/types";
import { Series } from "uplot"; import { Series } from "uplot";
import { getNameForMetric } from "../metric"; import { getNameForMetric, promValueToNumber } from "../metric";
import { BarSeriesItem, Disp, Fill, LegendItemType, Stroke } from "./types"; import { BarSeriesItem, Disp, Fill, LegendItemType, Stroke } from "./types";
import { HideSeriesArgs } from "./types"; import { HideSeriesArgs } from "./types";
import { baseContrastColors, getColorFromString } from "../color"; import { baseContrastColors, getColorFromString } from "../color";
import { getAvgFromArray, getMaxFromArray, getMinFromArray, getLastFromArray } from "../math";
import { formatPrettyNumber } from "./helpers";
interface SeriesItem extends Series { export interface SeriesItem extends Series {
freeFormFields: {[key: string]: string}; freeFormFields: {[key: string]: string};
calculations: {
max: string,
avg: string,
last: string
}
} }
export const getSeriesItemContext = () => { export const getSeriesItemContext = () => {
@ -18,6 +25,12 @@ export const getSeriesItemContext = () => {
const hasBasicColors = countSavedColors < baseContrastColors.length; const hasBasicColors = countSavedColors < baseContrastColors.length;
if (hasBasicColors) colorState[label] = colorState[label] || baseContrastColors[countSavedColors]; if (hasBasicColors) colorState[label] = colorState[label] || baseContrastColors[countSavedColors];
const values = d.values.map(v => promValueToNumber(v[1]));
const min = getMinFromArray(values);
const max = getMaxFromArray(values);
const avg = getAvgFromArray(values);
const last = getLastFromArray(values);
return { return {
label, label,
freeFormFields: d.metric, freeFormFields: d.metric,
@ -28,6 +41,11 @@ export const getSeriesItemContext = () => {
points: { points: {
size: 4.2, size: 4.2,
width: 1.4 width: 1.4
},
calculations: {
max: formatPrettyNumber(max, min, max),
avg: formatPrettyNumber(avg, min, max),
last: formatPrettyNumber(last, min, max),
} }
}; };
}; };
@ -39,6 +57,7 @@ export const getLegendItem = (s: SeriesItem, group: number): LegendItemType => (
color: s.stroke as string, color: s.stroke as string,
checked: s.show || false, checked: s.show || false,
freeFormFields: s.freeFormFields, freeFormFields: s.freeFormFields,
calculations: s.calculations,
}); });
export const getHideSeries = ({ hideSeries, legend, metaKey, series }: HideSeriesArgs): string[] => { export const getHideSeries = ({ hideSeries, legend, metaKey, series }: HideSeriesArgs): string[] => {

View file

@ -21,6 +21,11 @@ export interface LegendItemType {
color: string; color: string;
checked: boolean; checked: boolean;
freeFormFields: {[key: string]: string}; freeFormFields: {[key: string]: string};
calculations: {
max: string;
avg: string;
last: string;
}
} }
export interface BarSeriesItem { export interface BarSeriesItem {

View file

@ -19,6 +19,7 @@ The following tip changes can be tested by building VictoriaMetrics components f
This also means that `-remoteRead.ignoreRestoreErrors` command-line flag becomes deprecated now and will have no effect if configured. This also means that `-remoteRead.ignoreRestoreErrors` command-line flag becomes deprecated now and will have no effect if configured.
While previously state restore attempt was made for all the loaded alerting rules, now it is called only for alerts which became active after the first evaluation. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2608). While previously state restore attempt was made for all the loaded alerting rules, now it is called only for alerts which became active after the first evaluation. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2608).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): optimize VMUI for use from smarthones and tablets. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3707). * FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): optimize VMUI for use from smarthones and tablets. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3707).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add avg/max/last values to line legends and tooltips for graphs. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3706).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): hide the default `per-job resource usage` dashboard if there is a custom dashboard exists at the directory specified via `-vmui.customDashboardsPath` command-line flag. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3740). * FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): hide the default `per-job resource usage` dashboard if there is a custom dashboard exists at the directory specified via `-vmui.customDashboardsPath` command-line flag. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3740).
* BUGFIX: [vmagent](https://docs.victoriametrics.com/vmagent.html): fix panic in [HashiCorp Nomad service discovery](https://docs.victoriametrics.com/sd_configs.html#nomad_sd_configs). Thanks to @mr-karan for the [pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3784). * BUGFIX: [vmagent](https://docs.victoriametrics.com/vmagent.html): fix panic in [HashiCorp Nomad service discovery](https://docs.victoriametrics.com/sd_configs.html#nomad_sd_configs). Thanks to @mr-karan for the [pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3784).