Merge branch 'public-single-node' into pmm-6401-read-prometheus-data-files

This commit is contained in:
Aliaksandr Valialkin 2023-03-24 18:00:37 -07:00
commit dc9537f44e
No known key found for this signature in database
GPG key ID: A72BEC6CD3D0DED1
61 changed files with 1462 additions and 532 deletions

View file

@ -54,10 +54,19 @@ and sending the data to the Prometheus-compatible remote storage:
The path can point either to local file or to http url. `vmagent` doesn't support some sections of Prometheus config file,
so you may need either to delete these sections or to run `vmagent` with `-promscrape.config.strictParse=false` command-line flag.
In this case `vmagent` ignores unsupported sections. See [the list of unsupported sections](#unsupported-prometheus-config-sections).
* `-remoteWrite.url` with Prometheus-compatible remote storage endpoint such as VictoriaMetrics, the `-remoteWrite.url` argument can be specified
multiple times to replicate data concurrently to multiple remote storage systems. See [various use cases](#use-cases).
* `-remoteWrite.url` with Prometheus-compatible remote storage endpoint such as VictoriaMetrics.
Example command line:
Example command for writing the data recieved via [supported push-based protocols](#how-to-push-data-to-vmagent)
to [single-node VictoriaMetrics](https://docs.victoriametrics.com/) located at `victoria-metrics-host:8428`:
```console
/path/to/vmagent -remoteWrite.url=https://victoria-metrics-host:8428/api/v1/write
```
See [these docs](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#url-format) if you need writing
the data to [VictoriaMetrics cluster](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html).
Example command for scraping Prometheus targets and writing the data to single-node VictoriaMetrics:
```console
/path/to/vmagent -promscrape.config=/path/to/prometheus.yml -remoteWrite.url=https://victoria-metrics-host:8428/api/v1/write
@ -68,18 +77,12 @@ See [how to scrape Prometheus-compatible targets](#how-to-collect-metrics-in-pro
If you use single-node VictoriaMetrics, then you can discover and scrape Prometheus-compatible targets directly from VictoriaMetrics
without the need to use `vmagent` - see [these docs](https://docs.victoriametrics.com/#how-to-scrape-prometheus-exporters-such-as-node-exporter).
If you don't need to scrape Prometheus-compatible targets, then the `-promscrape.config` option isn't needed.
For example, the following command is sufficient for accepting data via [supported push-based protocols](#how-to-push-data-to-vmagent)
and sending it to the provided `-remoteWrite.url`:
```console
/path/to/vmagent -remoteWrite.url=https://victoria-metrics-host:8428/api/v1/write
```
`vmagent` can save network bandwidth usage costs under high load when [VictoriaMetrics remote write protocol is enabled](#victoriametrics-remote-write-protocol).
`vmagent` can save network bandwidth usage costs under high load when [VictoriaMetrics remote write protocol is used](#victoriametrics-remote-write-protocol).
See [troubleshooting docs](#troubleshooting) if you encounter common issues with `vmagent`.
See [various use cases](#use-cases) for vmagent.
Pass `-help` to `vmagent` in order to see [the full list of supported command-line flags with their descriptions](#advanced-usage).
## How to push data to vmagent
@ -151,6 +154,11 @@ If a single remote storage instance temporarily is out of service, then the coll
`vmagent` buffers the collected data in files at `-remoteWrite.tmpDataPath` until the remote storage becomes available again
and then it sends the buffered data to the remote storage in order to prevent data gaps.
[VictoriaMetrics cluster](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html) already supports replication,
so there is no need in specifying multiple `-remoteWrite.url` flags when writing data to the same cluster.
See [these docs](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#replication-and-data-safety).
### Relabeling and filtering
`vmagent` can add, remove or update labels on the collected data before sending it to the remote storage. Additionally,

View file

@ -916,7 +916,7 @@ The shortlist of configuration flags is the following:
-evaluationInterval duration
How often to evaluate the rules (default 1m0s)
-external.alert.source string
External Alert Source allows to override the Source link for alerts sent to AlertManager for cases where you want to build a custom link to Grafana, Prometheus or any other service. Supports templating - see https://docs.victoriametrics.com/vmalert.html#templating . For example, link to Grafana: -external.alert.source='explore?orgId=1&left=["now-1h","now","VictoriaMetrics",{"expr":{{$expr|jsonEscape|queryEscape}} },{"mode":"Metrics"},{"ui":[true,true,true,"none"]}]' . If empty 'vmalert/alert?group_id={{.GroupID}}&alert_id={{.AlertID}}' is used.
External Alert Source allows to override the Source link for alerts sent to AlertManager for cases where you want to build a custom link to Grafana, Prometheus or any other service. Supports templating - see https://docs.victoriametrics.com/vmalert.html#templating . For example, link to Grafana: -external.alert.source='explore?orgId=1&left=["now-1h","now","VictoriaMetrics",{"expr":{{$expr|jsonEscape|queryEscape}} },{"mode":"Metrics"},{"ui":[true,true,true,"none"]}]'. Link to VMUI: -external.alert.source='vmui/#/?g0.expr={{.Expr|queryEscape}}'. If empty 'vmalert/alert?group_id={{.GroupID}}&alert_id={{.AlertID}}' is used.
-external.label array
Optional label in the form 'Name=value' to add to all generated recording rules and alerts. Pass multiple -label flags in order to add multiple label sets.
Supports an array of values separated by comma or specified via multiple flags.

View file

@ -73,7 +73,8 @@ absolute path to all .tpl files in root.`)
externalAlertSource = flag.String("external.alert.source", "", `External Alert Source allows to override the Source link for alerts sent to AlertManager `+
`for cases where you want to build a custom link to Grafana, Prometheus or any other service. `+
`Supports templating - see https://docs.victoriametrics.com/vmalert.html#templating . `+
`For example, link to Grafana: -external.alert.source='explore?orgId=1&left=["now-1h","now","VictoriaMetrics",{"expr":{{$expr|jsonEscape|queryEscape}} },{"mode":"Metrics"},{"ui":[true,true,true,"none"]}]' . `+
`For example, link to Grafana: -external.alert.source='explore?orgId=1&left=["now-1h","now","VictoriaMetrics",{"expr":{{$expr|jsonEscape|queryEscape}} },{"mode":"Metrics"},{"ui":[true,true,true,"none"]}]'. `+
`Link to VMUI: -external.alert.source='vmui/#/?g0.expr={{.Expr|queryEscape}}'. `+
`If empty 'vmalert/alert?group_id={{.GroupID}}&alert_id={{.AlertID}}' is used.`)
externalLabels = flagutil.NewArrayString("external.label", "Optional label in the form 'Name=value' to add to all generated recording rules and alerts. "+
"Pass multiple -label flags in order to add multiple label sets.")

View file

@ -52,7 +52,7 @@
{% for _, g := range groups %}
<div class="group-heading{% if rNotOk[g.ID] > 0 %} alert-danger{% endif %}" data-bs-target="rules-{%s g.ID %}">
<span class="anchor" id="group-{%s g.ID %}"></span>
<a href="#group-{%s g.ID %}">{%s g.Name %}{% if g.Type != "prometheus" %} ({%s g.Type %}){% endif %} (every {%f.0 g.Interval %}s)</a>
<a href="#group-{%s g.ID %}">{%s g.Name %}{% if g.Type != "prometheus" %} ({%s g.Type %}){% endif %} (every {%f.0 g.Interval %}s) #</a>
{% if rNotOk[g.ID] > 0 %}<span class="badge bg-danger" title="Number of rules with status Error">{%d rNotOk[g.ID] %}</span> {% endif %}
<span class="badge bg-success" title="Number of rules withs status Ok">{%d rOk[g.ID] %}</span>
<p class="fs-6 fw-lighter">{%s g.File %}</p>

View file

@ -227,7 +227,7 @@ func StreamListGroups(qw422016 *qt422016.Writer, r *http.Request, groups []APIGr
//line app/vmalert/web.qtpl:55
qw422016.N().FPrec(g.Interval, 0)
//line app/vmalert/web.qtpl:55
qw422016.N().S(`s)</a>
qw422016.N().S(`s) #</a>
`)
//line app/vmalert/web.qtpl:56
if rNotOk[g.ID] > 0 {

View file

@ -17,7 +17,12 @@ and pass the following flag to `vmauth` binary in order to start authorizing and
After that `vmauth` starts accepting HTTP requests on port `8427` and routing them according to the provided [-auth.config](#auth-config).
The port can be modified via `-httpListenAddr` command-line flag.
The auth config can be reloaded either by passing `SIGHUP` signal to `vmauth` or by querying `/-/reload` http endpoint.
The auth config can be reloaded via the following ways:
- By passing `SIGHUP` signal to `vmauth`.
- By querying `/-/reload` http endpoint. This endpoint can be protected with `-reloadAuthKey` command-line flag. See [security docs](#security) for more details.
- By specifying `-configCheckInterval` command-line flag to the interval between config re-reads. For example, `-configCheckInterval=5s` will re-read the config
and apply new changes every 5 seconds.
Docker images for `vmauth` are available [here](https://hub.docker.com/r/victoriametrics/vmauth/tags).
@ -260,6 +265,8 @@ See the docs at https://docs.victoriametrics.com/vmauth.html .
-auth.config string
Path to auth config. It can point either to local file or to http url. See https://docs.victoriametrics.com/vmauth.html for details on the format of this auth config
-configCheckInterval duration
Interval for config file re-read. Zero value disables config re-reading. By default, refreshing is disabled, send SIGHUP for config refresh.
-enableTCP6
Whether to enable IPv6 for listening and dialing. By default only IPv4 TCP and UDP is used
-envflag.enable

View file

@ -11,6 +11,7 @@ import (
"strings"
"sync"
"sync/atomic"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/envtemplate"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
@ -24,6 +25,8 @@ import (
var (
authConfigPath = flag.String("auth.config", "", "Path to auth config. It can point either to local file or to http url. "+
"See https://docs.victoriametrics.com/vmauth.html for details on the format of this auth config")
configCheckInterval = flag.Duration("configCheckInterval", 0, "interval for config file re-read. "+
"Zero value disables config re-reading. By default, refreshing is disabled, send SIGHUP for config refresh.")
)
// AuthConfig represents auth config.
@ -305,10 +308,20 @@ func stopAuthConfig() {
}
func authConfigReloader(sighupCh <-chan os.Signal) {
var refreshCh <-chan time.Time
// initialize auth refresh interval
if *configCheckInterval > 0 {
ticker := time.NewTicker(*configCheckInterval)
defer ticker.Stop()
refreshCh = ticker.C
}
for {
select {
case <-stopCh:
return
case <-refreshCh:
procutil.SelfSIGHUP()
case <-sighupCh:
logger.Infof("SIGHUP received; loading -auth.config=%q", *authConfigPath)
m, err := readAuthConfig(*authConfigPath)

View file

@ -1,14 +1,6 @@
//go:build darwin || linux || solaris
// +build darwin linux solaris
package terminal
import (
"golang.org/x/sys/unix"
)
// IsTerminal returns true if the file descriptor is terminal
func IsTerminal(fd int) bool {
_, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
return err == nil
return isTerminal(fd)
}

View file

@ -6,3 +6,8 @@ package terminal
import "golang.org/x/sys/unix"
const ioctlReadTermios = unix.TCGETS
func isTerminal(fd int) bool {
_, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
return err == nil
}

View file

@ -6,3 +6,8 @@ package terminal
import "golang.org/x/sys/unix"
const ioctlReadTermios = unix.TIOCGETA
func isTerminal(fd int) bool {
_, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
return err == nil
}

View file

@ -0,0 +1,8 @@
//go:build windows
// +build windows
package terminal
func isTerminal(fd int) bool {
return true
}

View file

@ -1,6 +1,5 @@
export interface CardinalityRequestsParams {
topN: number,
extraLabel: string | null,
match: string | null,
date: string | null,
focusLabel: string | null,

View file

@ -15,6 +15,7 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange }) => {
const [copiedValue, setCopiedValue] = useState("");
const freeFormFields = useMemo(() => getFreeFields(legend), [legend]);
const calculations = legend.calculations;
const showCalculations = Object.values(calculations).some(v => v);
const handleClickFreeField = async (val: string, id: string) => {
await navigator.clipboard.writeText(val);
@ -68,9 +69,11 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange }) => {
&#125;
</span>
</div>
<div className="vm-legend-item-values">
median:{calculations.median}, min:{calculations.min}, max:{calculations.max}, last:{calculations.last}
</div>
{showCalculations && (
<div className="vm-legend-item-values">
median:{calculations.median}, min:{calculations.min}, max:{calculations.max}, last:{calculations.last}
</div>
)}
</div>
);
};

View file

@ -0,0 +1,67 @@
import React, { FC, useEffect, useState } from "preact/compat";
import Tooltip from "../../Main/Tooltip/Tooltip";
import "./style.scss";
type BarChartData = {
value: number,
name: string,
percentage?: number,
}
interface SimpleBarChartProps {
data: BarChartData[],
}
const SimpleBarChart: FC<SimpleBarChartProps> = ({ data }) => {
const [bars, setBars] = useState<BarChartData[]>([]);
const [yAxis, setYAxis] = useState([0, 0]);
const generateYAxis = (sortedValues: BarChartData[]) => {
const numbers = sortedValues.map(b => b.value);
const max = Math.ceil(numbers[0] || 1);
const ticks = 10;
const step = max / (ticks - 1);
return new Array(ticks + 1).fill(max + step).map((v, i) => Math.round(v - (step * i)));
};
useEffect(() => {
const sortedValues = data.sort((a, b) => b.value - a.value);
const yAxis = generateYAxis(sortedValues);
setYAxis(yAxis);
setBars(sortedValues.map(b => ({
...b,
percentage: (b.value / yAxis[0]) * 100,
})));
}, [data]);
return (
<div className="vm-simple-bar-chart">
<div className="vm-simple-bar-chart-y-axis">
{yAxis.map(v => (
<div
className="vm-simple-bar-chart-y-axis__tick"
key={v}
>{v}</div>
))}
</div>
<div className="vm-simple-bar-chart-data">
{bars.map(({ name, value, percentage }) => (
<Tooltip
title={`${name}: ${value}`}
key={`${name}_${value}`}
placement="top-center"
>
<div
className="vm-simple-bar-chart-data-item"
style={{ maxHeight: `${percentage || 0}%` }}
/>
</Tooltip>
))}
</div>
</div>
);
};
export default SimpleBarChart;

View file

@ -0,0 +1,74 @@
@use "src/styles/variables" as *;
$color-bar: #33BB55;
$color-bar-highest: #F79420;
.vm-simple-bar-chart {
display: grid;
grid-template-columns: auto 1fr;
height: 100%;
padding-bottom: #{$font-size-small/2};
overflow: hidden;
&-y-axis {
position: relative;
display: grid;
transform: translateY(#{$font-size-small});
&__tick {
position: relative;
display: flex;
align-items: center;
justify-content: flex-end;
transform-style: preserve-3d;
text-align: right;
padding-right: $padding-small;
font-size: $font-size-small;
line-height: 2;
z-index: 1;
&:after {
content: '';
position: absolute;
top: auto;
left: 100%;
width: 100vw;
height: 0;
border-bottom: $border-divider;
transform: translateY(-1px) translateZ(-1);
}
}
}
&-data {
position: relative;
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 1%;
&-item {
display: flex;
align-items: flex-start;
justify-content: center;
flex-grow: 1;
width: 100%;
min-width: 1px;
height: calc(100% - ($font-size-small*4));
background-color: $color-bar;
transition: background-color 200ms ease-in;
&:hover {
background-color: lighten($color-bar, 10%);
}
&:first-child {
background-color: $color-bar-highest;
&:hover {
background-color: lighten($color-bar-highest, 10%);
}
}
}
}
}

View file

@ -1,5 +1,4 @@
import React, { FC, useMemo, useRef } from "preact/compat";
import { useCardinalityState, useCardinalityDispatch } from "../../../state/cardinality/CardinalityStateContext";
import React, { FC, useEffect, useMemo, useRef } from "preact/compat";
import dayjs from "dayjs";
import Button from "../../Main/Button/Button";
import { ArrowDownIcon, CalendarIcon } from "../../Main/Icons";
@ -8,21 +7,28 @@ import { getAppModeEnable } from "../../../utils/app-mode";
import { DATE_FORMAT } from "../../../constants/date";
import DatePicker from "../../Main/DatePicker/DatePicker";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import { useSearchParams } from "react-router-dom";
const CardinalityDatePicker: FC = () => {
const { isMobile } = useDeviceDetect();
const appModeEnable = getAppModeEnable();
const buttonRef = useRef<HTMLDivElement>(null);
const { date } = useCardinalityState();
const cardinalityDispatch = useCardinalityDispatch();
const [searchParams, setSearchParams] = useSearchParams();
const date = searchParams.get("date") || dayjs().tz().format(DATE_FORMAT);
const dateFormatted = useMemo(() => dayjs.tz(date).format(DATE_FORMAT), [date]);
const handleChangeDate = (val: string) => {
cardinalityDispatch({ type: "SET_DATE", payload: val });
searchParams.set("date", val);
setSearchParams(searchParams);
};
useEffect(() => {
handleChangeDate(date);
}, []);
return (
<div>
<div ref={buttonRef}>

View file

@ -387,3 +387,14 @@ export const TuneIcon = () => (
></path>
</svg>
);
export const TipIcon = () => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M7 20h4c0 1.1-.9 2-2 2s-2-.9-2-2zm-2-1h8v-2H5v2zm11.5-9.5c0 3.82-2.66 5.86-3.77 6.5H5.27c-1.11-.64-3.77-2.68-3.77-6.5C1.5 5.36 4.86 2 9 2s7.5 3.36 7.5 7.5zm4.87-2.13L20 8l1.37.63L22 10l.63-1.37L24 8l-1.37-.63L22 6l-.63 1.37zM19 6l.94-2.06L22 3l-2.06-.94L19 0l-.94 2.06L16 3l2.06.94L19 6z"
></path>
</svg>
);

View file

@ -58,6 +58,8 @@
&__error {
top: calc((100% - ($font-size-small/2)) - 2px);
color: $color-error;
pointer-events: auto;
user-select: text;
}
&__helper-text {

View file

@ -3,7 +3,6 @@ import { TimeStateProvider } from "../state/time/TimeStateContext";
import { QueryStateProvider } from "../state/query/QueryStateContext";
import { CustomPanelStateProvider } from "../state/customPanel/CustomPanelStateContext";
import { GraphStateProvider } from "../state/graph/GraphStateContext";
import { CardinalityStateProvider } from "../state/cardinality/CardinalityStateContext";
import { TopQueriesStateProvider } from "../state/topQueries/TopQueriesStateContext";
import { SnackbarProvider } from "./Snackbar";
@ -16,7 +15,6 @@ const providers = [
QueryStateProvider,
CustomPanelStateProvider,
GraphStateProvider,
CardinalityStateProvider,
TopQueriesStateProvider,
SnackbarProvider,
DashboardsStateProvider

View file

@ -26,6 +26,7 @@ interface FetchQueryReturn {
graphData?: MetricResult[],
liveData?: InstantMetricResult[],
error?: ErrorTypes | string,
queryErrors: (ErrorTypes | string)[],
warning?: string,
traces?: Trace[],
}
@ -58,17 +59,10 @@ export const useFetchQuery = ({
const [liveData, setLiveData] = useState<InstantMetricResult[]>();
const [traces, setTraces] = useState<Trace[]>();
const [error, setError] = useState<ErrorTypes | string>();
const [queryErrors, setQueryErrors] = useState<(ErrorTypes | string)[]>([]);
const [warning, setWarning] = useState<string>();
const [fetchQueue, setFetchQueue] = useState<AbortController[]>([]);
useEffect(() => {
if (error) {
setGraphData(undefined);
setLiveData(undefined);
setTraces(undefined);
}
}, [error]);
const fetchData = async ({
fetchUrl,
fetchQueue,
@ -100,7 +94,7 @@ export const useFetchQuery = ({
const resp = await response.json();
if (response.ok) {
setError(undefined);
setQueryErrors(prev => [...prev, ""]);
if (resp.trace) {
const trace = new Trace(resp.trace, query[counter - 1]);
@ -114,10 +108,11 @@ export const useFetchQuery = ({
});
totalLength += resp.data.result.length;
counter++;
} else {
setError(`${resp.errorType}\r\n${resp?.error}`);
tempData.push({ metric: {}, values: [], group: counter } as MetricBase);
setQueryErrors(prev => [...prev, `${resp.errorType}\r\n${resp?.error}`]);
}
counter++;
}
const limitText = `Showing ${seriesLimit} series out of ${totalLength} series due to performance reasons. Please narrow down the query, so it returns less series`;
@ -136,13 +131,15 @@ export const useFetchQuery = ({
const throttledFetchData = useCallback(debounce(fetchData, 300), []);
const fetchUrl = useMemo(() => {
setError("");
setQueryErrors([]);
const expr = predefinedQuery ?? query;
const displayChart = (display || displayType) === "chart";
if (!period) return;
if (!serverUrl) {
setError(ErrorTypes.emptyServer);
} else if (expr.every(q => !q.trim())) {
setError(ErrorTypes.validQuery);
setQueryErrors(expr.map(() => ErrorTypes.validQuery));
} else if (isValidHttpUrl(serverUrl)) {
const updatedPeriod = { ...period };
updatedPeriod.step = customStep;
@ -181,5 +178,5 @@ export const useFetchQuery = ({
setFetchQueue(fetchQueue.filter(f => !f.signal.aborted));
}, [fetchQueue]);
return { fetchUrl, isLoading, graphData, liveData, error, warning, traces };
return { fetchUrl, isLoading, graphData, liveData, error, queryErrors, warning, traces };
};

View file

@ -1,96 +1,76 @@
import React, { FC, useMemo } from "react";
import QueryEditor from "../../../components/Configurators/QueryEditor/QueryEditor";
import { useFetchQueryOptions } from "../../../hooks/useFetchQueryOptions";
import { ErrorTypes } from "../../../types";
import { useQueryDispatch, useQueryState } from "../../../state/query/QueryStateContext";
import Switch from "../../../components/Main/Switch/Switch";
import { InfoIcon, PlayIcon, QuestionIcon, WikiIcon } from "../../../components/Main/Icons";
import { PlayIcon, QuestionIcon, RestartIcon, TipIcon, WikiIcon } from "../../../components/Main/Icons";
import Button from "../../../components/Main/Button/Button";
import TextField from "../../../components/Main/TextField/TextField";
import "./style.scss";
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import classNames from "classnames";
import { useEffect, useState } from "preact/compat";
import { useSearchParams } from "react-router-dom";
import CardinalityTotals, { CardinalityTotalsProps } from "../CardinalityTotals/CardinalityTotals";
export interface CardinalityConfiguratorProps {
onSetHistory: (step: number) => void;
onSetQuery: (query: string) => void;
onRunQuery: () => void;
onTopNChange: (value: string) => void;
onFocusLabelChange: (value: string) => void;
query: string;
topN: number;
error?: ErrorTypes | string;
totalSeries: number;
totalLabelValuePairs: number;
date: string | null;
match: string | null;
focusLabel: string | null;
}
const CardinalityConfigurator: FC<CardinalityConfiguratorProps> = ({
topN,
error,
query,
onSetHistory,
onRunQuery,
onSetQuery,
onTopNChange,
onFocusLabelChange,
totalSeries,
totalLabelValuePairs,
date,
match,
focusLabel
}) => {
const { autocomplete } = useQueryState();
const queryDispatch = useQueryDispatch();
const CardinalityConfigurator: FC<CardinalityTotalsProps> = (props) => {
const { isMobile } = useDeviceDetect();
const [searchParams, setSearchParams] = useSearchParams();
const { queryOptions } = useFetchQueryOptions();
const showTips = searchParams.get("tips") || "";
const [match, setMatch] = useState(searchParams.get("match") || "");
const [focusLabel, setFocusLabel] = useState(searchParams.get("focusLabel") || "");
const [topN, setTopN] = useState(+(searchParams.get("topN") || 10));
const errorTopN = useMemo(() => topN < 1 ? "Number must be bigger than zero" : "", [topN]);
const errorTopN = useMemo(() => topN < 0 ? "Number must be bigger than zero" : "", [topN]);
const onChangeAutocomplete = () => {
queryDispatch({ type: "TOGGLE_AUTOCOMPLETE" });
const handleTopNChange = (val: string) => {
const num = +val;
setTopN(isNaN(num) ? 0 : num);
};
const handleArrowUp = () => {
onSetHistory(-1);
const handleRunQuery = () => {
searchParams.set("match", match);
searchParams.set("topN", topN.toString());
searchParams.set("focusLabel", focusLabel);
setSearchParams(searchParams);
};
const handleArrowDown = () => {
onSetHistory(1);
const handleResetQuery = () => {
searchParams.set("match", "");
searchParams.set("focusLabel", "");
setSearchParams(searchParams);
};
const handleToggleTips = () => {
const showTips = searchParams.get("tips") || "";
if (showTips) searchParams.delete("tips");
else searchParams.set("tips", "true");
setSearchParams(searchParams);
};
useEffect(() => {
const matchQuery = searchParams.get("match");
const topNQuery = +(searchParams.get("topN") || 10);
const focusLabelQuery = searchParams.get("focusLabel");
if (matchQuery !== match) setMatch(matchQuery || "");
if (topNQuery !== topN) setTopN(topNQuery);
if (focusLabelQuery !== focusLabel) setFocusLabel(focusLabelQuery || "");
}, [searchParams]);
return <div
className={classNames({
"vm-cardinality-configurator": true,
"vm-cardinality-configurator_mobile": isMobile,
"vm-block": true,
"vm-block_mobile": isMobile,
})}
>
<div className="vm-cardinality-configurator-controls">
<div className="vm-cardinality-configurator-controls__query">
<QueryEditor
value={query}
autocomplete={autocomplete}
options={queryOptions}
error={error}
onArrowUp={handleArrowUp}
onArrowDown={handleArrowDown}
onEnter={onRunQuery}
onChange={onSetQuery}
label={"Time series selector"}
/>
</div>
<div className="vm-cardinality-configurator-controls__item">
<TextField
label="Number of entries per table"
type="number"
value={topN}
error={errorTopN}
onChange={onTopNChange}
label="Time series selector"
type="string"
value={match}
onChange={setMatch}
onEnter={handleRunQuery}
/>
</div>
<div className="vm-cardinality-configurator-controls__item">
@ -98,41 +78,36 @@ const CardinalityConfigurator: FC<CardinalityConfiguratorProps> = ({
label="Focus label"
type="text"
value={focusLabel || ""}
onChange={onFocusLabelChange}
onChange={setFocusLabel}
onEnter={handleRunQuery}
endIcon={(
<Tooltip
title={(
<div>
<p>To identify values with the highest number of series for the selected label.</p>
<p>Adds a table showing the series with the highest number of series.</p>
</div>
)}
>
<InfoIcon/>
<QuestionIcon/>
</Tooltip>
)}
/>
</div>
</div>
<div className="vm-cardinality-configurator-additional">
<Switch
label={"Autocomplete"}
value={autocomplete}
onChange={onChangeAutocomplete}
/>
</div>
<div
className={classNames({
"vm-cardinality-configurator-bottom": true,
"vm-cardinality-configurator-bottom_mobile": isMobile,
})}
>
<div className="vm-cardinality-configurator-bottom__info">
Analyzed <b>{totalSeries}</b> series with <b>{totalLabelValuePairs}</b> &quot;label=value&quot; pairs
at <b>{date}</b>{match && <span> for series selector <b>{match}</b></span>}.
Show top {topN} entries per table.
<div className="vm-cardinality-configurator-controls__item vm-cardinality-configurator-controls__item_limit">
<TextField
label="Limit entries"
type="number"
value={topN}
error={errorTopN}
onChange={handleTopNChange}
onEnter={handleRunQuery}
/>
</div>
<div className="vm-cardinality-configurator-bottom__docs">
</div>
<div className="vm-cardinality-configurator-bottom">
<CardinalityTotals {...props}/>
<div className="vm-cardinality-configurator-bottom-helpful">
<a
className="vm-link vm-link_with-icon"
target="_blank"
@ -140,25 +115,33 @@ const CardinalityConfigurator: FC<CardinalityConfiguratorProps> = ({
rel="help noreferrer"
>
<WikiIcon/>
Documentation
</a>
<a
className="vm-link vm-link_with-icon"
target="_blank"
href="https://victoriametrics.com/blog/cardinality-explorer/"
rel="help noreferrer"
>
<QuestionIcon/>
Example of using
Documentation
</a>
</div>
<Button
startIcon={<PlayIcon/>}
onClick={onRunQuery}
fullWidth
>
Execute Query
</Button>
<div className="vm-cardinality-configurator-bottom__execute">
<Tooltip title={showTips ? "Hide tips" : "Show tips"}>
<Button
variant="text"
color={showTips ? "warning" : "gray"}
startIcon={<TipIcon/>}
onClick={handleToggleTips}
/>
</Tooltip>
<Button
variant="text"
startIcon={<RestartIcon/>}
onClick={handleResetQuery}
>
Reset
</Button>
<Button
startIcon={<PlayIcon/>}
onClick={handleRunQuery}
>
Execute Query
</Button>
</div>
</div>
</div>;
};

View file

@ -9,58 +9,65 @@
align-items: center;
justify-content: flex-start;
flex-wrap: wrap;
gap: 0 $padding-medium;
gap: $padding-small $padding-medium;
&__query {
flex-grow: 8;
flex-grow: 10;
}
&__item {
flex-grow: 1;
}
}
flex-grow: 2;
&-additional {
display: flex;
align-items: center;
margin-bottom: $padding-small;
&_limit {
flex-grow: 1;
}
svg {
color: $color-text-disabled
}
}
}
&-bottom {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
flex-wrap: wrap;
gap: $padding-global;
width: 100%;
&__docs {
&-helpful {
display: flex;
align-items: center;
gap: $padding-global;
justify-content: flex-end;
flex-wrap: wrap;
gap: $padding-small $padding-global;
a {
color: $color-text-secondary;
}
}
&_mobile &__docs {
justify-content: space-between;
&__execute {
display: flex;
align-items: center;
gap: $padding-small;
}
}
&__info {
&_mobile &-bottom {
justify-content: center;
&-helpful {
flex-grow: 1;
font-size: $font-size;
justify-content: center;
}
a {
color: $color-text-secondary;
}
&__execute {
width: 100%;
button {
margin: 0 0 0 auto;
}
&_mobile {
display: grid;
grid-template-columns: 1fr;
button {
margin: 0;
button:nth-child(3) {
width: 100%;
}
}
}

View file

@ -0,0 +1,124 @@
import { TipIcon } from "../../../components/Main/Icons";
import React, { FC } from "preact/compat";
import { ReactNode } from "react";
import "./style.scss";
const Link: FC<{ href: string, children: ReactNode, target?: string }> = ({ href, children, target }) => (
<a
href={href}
className="vm-link vm-link_colored"
target={target}
>
{children}
</a>
);
const TipCard: FC<{ title?: string, children: ReactNode }> = ({ title, children }) => (
<div className="vm-cardinality-tip">
<div className="vm-cardinality-tip-header">
<div className="vm-cardinality-tip-header__tip-icon"><TipIcon/></div>
<h4 className="vm-cardinality-tip-header__title">{title || "Tips"}</h4>
</div>
<p className="vm-cardinality-tip__description">
{children}
</p>
</div>
);
export const TipDocumentation: FC = () => (
<TipCard title="Cardinality explorer">
<h6>Helpful for analyzing VictoriaMetrics TSDB data</h6>
<ul>
<li>
<Link href="https://docs.victoriametrics.com/#cardinality-explorer">
Cardinality explorer documentation
</Link>
</li>
<li>
See the <Link href="https://victoriametrics.com/blog/cardinality-explorer/">
example of using</Link> the cardinality explorer
</li>
</ul>
</TipCard>
);
export const TipHighNumberOfSeries: FC = () => (
<TipCard title="Metrics with a high number of series">
<ul>
<li>
Identify and eliminate labels with frequently changed values to reduce their&nbsp;
<Link
href='https://docs.victoriametrics.com/FAQ.html#what-is-high-cardinality'
target={"_blank"}
>cardinality</Link>&nbsp;and&nbsp;
<Link
href='https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate'
target={"_blank"}
>high churn rate</Link>
</li>
<li>
Find unused time series and&nbsp;
<Link
href='https://docs.victoriametrics.com/relabeling.html'
target={"_blank"}
>drop entire metrics</Link>
</li>
<li>
Aggregate time series before they got ingested into the database via&nbsp;
<Link
href='https://docs.victoriametrics.com/stream-aggregation.html'
target={"_blank"}
>streaming aggregation</Link>
</li>
</ul>
</TipCard>
);
export const TipHighNumberOfValues: FC = () => (
<TipCard title="Labels with a high number of unique values">
<ul>
<li>Decrease the number of unique label values to reduce cardinality</li>
<li>Drop the label entirely via&nbsp;
<Link
href='https://docs.victoriametrics.com/relabeling.html'
target={"_blank"}
>relabeling</Link></li>
<li>For volatile label values (such as URL path, user session, etc.)
consider printing them to the log file instead of adding to time series</li>
</ul>
</TipCard>
);
export const TipCardinalityOfSingle: FC = () => (
<TipCard title="Dashboard of a single metric">
<p>This dashboard helps to understand the cardinality of a single metric.</p>
<p>
Each time series is a unique combination of key-value label pairs.
Therefore a label key with many values can create a lot of time series for a particular metric.
If youre trying to decrease the cardinality of a metric,
start by looking at the labels with the highest number of values.
</p>
<p>Use the series selector at the top of the page to apply additional filters.</p>
</TipCard>
);
export const TipCardinalityOfLabel: FC = () => (
<TipCard title="Dashboard of a label">
<p>
This dashboard helps you understand the count of time series per label.
</p>
<p>
Use the selector at the top of the page to pick a label name youd like to inspect.
For the selected label name, youll see the label values that have the highest number of series associated with
them.
So if youve chosen `instance` as your label name, you may see that `657` time series have value
host-1 attached to them and `580` time series have value `host-2` attached to them.
</p>
<p>
This can be helpful in allowing you to determine where the bulk of your time series are coming from.
If the label instance=host-1 was applied to 657 series and the label instance=host-2
was only applied to 580 series, youd know, for example, that host-01 was responsible for sending
the majority of the time series.
</p>
</TipCard>
);

View file

@ -0,0 +1,87 @@
@use "src/styles/variables" as *;
.vm-cardinality-tip {
display: grid;
grid-template-rows: auto 1fr;
background-color: $color-background-block;
border-radius: $border-radius-medium;
box-shadow: $box-shadow;
overflow: hidden;
color: $color-text-secondary;
flex-grow: 1;
width: 300px;
&-header {
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: $padding-small $padding-global;
border-bottom: $border-divider;
gap: 4px;
&:after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: $color-warning;
opacity: 0.1;
pointer-events: none
}
&__tip-icon {
width: 12px;
display: flex;
align-items: center;
justify-content: center;
color: $color-warning;
}
&__title {
font-weight: bold;
text-align: center;
color: $color-text;
}
&__tooltip {
max-width: 280px;
white-space: normal;
padding: $padding-small;
line-height: 130%;
font-size: $font-size;
}
}
&__description {
padding: $padding-small $padding-global;
line-height: 130%;
p {
margin-bottom: $padding-small;
&:last-child {
margin-bottom: 0;
}
}
h5 {
font-size: $font-size-medium;
margin-bottom: $padding-small;
}
h6 {
margin-bottom: $padding-small;
}
ul, ol {
list-style-position: inside;
li {
margin-bottom: calc($padding-small/2);
}
}
}
}

View file

@ -0,0 +1,80 @@
import React, { FC } from "preact/compat";
import { InfoIcon } from "../../../components/Main/Icons";
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
import { TopHeapEntry } from "../types";
import { useSearchParams } from "react-router-dom";
import classNames from "classnames";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import "./style.scss";
export interface CardinalityTotalsProps {
totalSeries: number;
totalSeriesAll: number;
totalLabelValuePairs: number;
seriesCountByMetricName: TopHeapEntry[];
}
const CardinalityTotals: FC<CardinalityTotalsProps> = ({
totalSeries,
totalSeriesAll,
seriesCountByMetricName
}) => {
const { isMobile } = useDeviceDetect();
const [searchParams] = useSearchParams();
const match = searchParams.get("match");
const focusLabel = searchParams.get("focusLabel");
const isMetric = /__name__/.test(match || "");
const progress = seriesCountByMetricName[0]?.value / totalSeriesAll * 100;
const totals = [
{
title: "Total series",
value: totalSeries.toLocaleString("en-US"),
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.
For example, temperature{city="NY",country="US"} and temperature{city="SF",country="US"}
are two distinct series, since they differ by the city label.`
},
{
title: "Percentage from total",
value: isNaN(progress) ? "-" : `${progress.toFixed(2)}%`,
display: isMetric,
info: "The share of these series in the total number of time series."
}
].filter(t => t.display);
if (!totals.length) {
return null;
}
return (
<div
className={classNames({
"vm-cardinality-totals": true,
"vm-cardinality-totals_mobile": isMobile
})}
>
{totals.map(({ title, value, info }) => (
<div
className="vm-cardinality-totals-card"
key={title}
>
<div className="vm-cardinality-totals-card-header">
{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>
)}
<h4 className="vm-cardinality-totals-card-header__title">{title}</h4>
</div>
<span className="vm-cardinality-totals-card__value">{value}</span>
</div>
))}
</div>
);
};
export default CardinalityTotals;

View file

@ -0,0 +1,60 @@
@use "src/styles/variables" as *;
.vm-cardinality-totals {
display: inline-flex;
flex-wrap: wrap;
align-content: flex-start;
justify-content: flex-start;
gap: $padding-global;
flex-grow: 1;
&_mobile {
gap: $padding-small;
justify-content: center;
}
&-card {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
&-header {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
&__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;
}
}
&__value {
font-weight: bold;
color: $color-primary;
font-size: $font-size-medium;
}
}
}

View file

@ -1,43 +1,40 @@
import React, { FC } from "react";
import EnhancedTable from "../Table/Table";
import TableCells from "../Table/TableCells/TableCells";
import BarChart from "../../../components/Chart/BarChart/BarChart";
import { barOptions } from "../../../components/Chart/BarChart/consts";
import { Data, HeadCell } from "../Table/types";
import { MutableRef } from "preact/hooks";
import Tabs from "../../../components/Main/Tabs/Tabs";
import { useMemo } from "preact/compat";
import { ChartIcon, TableIcon } from "../../../components/Main/Icons";
import { useMemo, useState } from "preact/compat";
import { ChartIcon, InfoIcon, TableIcon } from "../../../components/Main/Icons";
import "./style.scss";
import classNames from "classnames";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
import SimpleBarChart from "../../../components/Chart/SimpleBarChart/SimpleBarChart";
interface MetricsProperties {
rows: Data[];
activeTab: number;
onChange: (newValue: string, tabId: string) => void;
onActionClick: (name: string) => void;
tabs: string[];
chartContainer: MutableRef<HTMLDivElement> | undefined;
totalSeries: number,
tabId: string;
sectionTitle: string;
tip?: string;
tableHeaderCells: HeadCell[];
}
const MetricsContent: FC<MetricsProperties> = ({
rows,
activeTab,
onChange,
tabs: tabsProps,
tabs: tabsProps = [],
chartContainer,
totalSeries,
tabId,
onActionClick,
sectionTitle,
tip,
tableHeaderCells,
}) => {
const { isMobile } = useDeviceDetect();
const [activeTab, setActiveTab] = useState("table");
const tableCells = (row: Data) => (
<TableCells
@ -48,15 +45,11 @@ const MetricsContent: FC<MetricsProperties> = ({
);
const tabs = useMemo(() => tabsProps.map((t, i) => ({
value: String(i),
value: t,
label: t,
icon: i === 0 ? <TableIcon /> : <ChartIcon />
})), [tabsProps]);
const handleChangeTab = (newValue: string) => {
onChange(newValue, tabId);
};
return (
<div
className={classNames({
@ -69,47 +62,53 @@ const MetricsContent: FC<MetricsProperties> = ({
<div className="vm-metrics-content-header vm-section-header">
<h5
className={classNames({
"vm-metrics-content-header__title": true,
"vm-section-header__title": true,
"vm-section-header__title_mobile": isMobile,
})}
>{sectionTitle}</h5>
>
{!isMobile && tip && (
<Tooltip
title={<p
dangerouslySetInnerHTML={{ __html: tip }}
className="vm-metrics-content-header__tip"
/>}
>
<div className="vm-metrics-content-header__tip-icon"><InfoIcon/></div>
</Tooltip>
)}
{sectionTitle}
</h5>
<div className="vm-section-header__tabs">
<Tabs
activeItem={String(activeTab)}
activeItem={activeTab}
items={tabs}
onChange={handleChangeTab}
onChange={setActiveTab}
/>
</div>
</div>
<div
ref={chartContainer}
className={classNames({
"vm-metrics-content__table": true,
"vm-metrics-content__table_mobile": isMobile
})}
>
{activeTab === 0 && (
{activeTab === "table" && (
<div
ref={chartContainer}
className={classNames({
"vm-metrics-content__table": true,
"vm-metrics-content__table_mobile": isMobile
})}
>
<EnhancedTable
rows={rows}
headerCells={tableHeaderCells}
defaultSortColumn={"value"}
tableCells={tableCells}
/>
)}
{activeTab === 1 && (
<BarChart
data={[
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
rows.map((v) => v.name),
rows.map((v) => v.value),
rows.map((_, i) => i % 12 == 0 ? 1 : i % 10 == 0 ? 2 : 0),
]}
container={chartContainer?.current || null}
configs={barOptions}
/>
)}
</div>
</div>
)}
{activeTab === "graph" && (
<div className="vm-metrics-content__chart">
<SimpleBarChart data={rows.map(({ name, value }) => ({ name, value }))}/>
</div>
)}
</div>
);
};

View file

@ -3,6 +3,33 @@
.vm-metrics-content {
&-header {
margin: -$padding-medium 0-$padding-medium 0;
&__title {
display: flex;
align-items: center;
justify-content: flex-start;
}
&__tip {
max-width: 300px;
white-space: normal;
padding: $padding-small;
line-height: 130%;
font-size: $font-size;
p {
margin-bottom: $padding-small;
}
}
&__tip-icon {
width: 12px;
display: flex;
align-items: center;
justify-content: center;
color: $color-primary;
margin-right: 4px;
}
}
&_mobile &-header {
@ -30,4 +57,8 @@
&_mobile &__table {
width: calc(100vw - ($padding-global * 2) - var(--scrollbar-width));
}
&__chart {
padding-top: $padding-medium;
}
}

View file

@ -1,9 +1,8 @@
import React, { FC, useState } from "preact/compat";
import { ChangeEvent, MouseEvent } from "react";
import { MouseEvent } from "react";
import { Data, Order, TableProps, } from "./types";
import { EnhancedTableHead } from "./TableHead";
import { getComparator, stableSort } from "./helpers";
import classNames from "classnames";
const EnhancedTable: FC<TableProps> = ({
rows,
@ -14,7 +13,6 @@ const EnhancedTable: FC<TableProps> = ({
const [order, setOrder] = useState<Order>("desc");
const [orderBy, setOrderBy] = useState<keyof Data>(defaultSortColumn);
const [selected, setSelected] = useState<readonly string[]>([]);
const handleRequestSort = (
event: MouseEvent<unknown>,
@ -25,45 +23,13 @@ const EnhancedTable: FC<TableProps> = ({
setOrderBy(property);
};
const handleSelectAllClick = (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
const newSelecteds = rows.map((n) => n.name) as string[];
setSelected(newSelecteds);
return;
}
setSelected([]);
};
const handleClick = (name: string) => () => {
const selectedIndex = selected.indexOf(name);
let newSelected: readonly string[] = [];
if (selectedIndex === -1) {
newSelected = newSelected.concat(selected, name);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(selected.slice(1));
} else if (selectedIndex === selected.length - 1) {
newSelected = newSelected.concat(selected.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(
selected.slice(0, selectedIndex),
selected.slice(selectedIndex + 1),
);
}
setSelected(newSelected);
};
const isSelected = (name: string) => selected.indexOf(name) !== -1;
const sortedData = stableSort(rows, getComparator(order, orderBy));
return (
<table className="vm-table">
<EnhancedTableHead
numSelected={selected.length}
order={order}
orderBy={orderBy}
onSelectAllClick={handleSelectAllClick}
onRequestSort={handleRequestSort}
rowCount={rows.length}
headerCells={headerCells}
@ -71,12 +37,8 @@ const EnhancedTable: FC<TableProps> = ({
<tbody className="vm-table-header">
{sortedData.map((row) => (
<tr
className={classNames({
"vm-table__row": true,
"vm-table__row_selected": isSelected(row.name)
})}
className="vm-table__row"
key={row.name}
onClick={handleClick(row.name)}
>
{tableCells(row)}
</tr>

View file

@ -23,7 +23,12 @@ const TableCells: FC<CardinalityTableCells> = ({ row, totalSeries, onActionClick
className="vm-table-cell"
key={row.name}
>
{row.name}
<span
className="vm-link vm-link_colored"
onClick={handleActionClick}
>
{row.name}
</span>
</td>
<td
className="vm-table-cell"

View file

@ -2,7 +2,8 @@ import { MouseEvent } from "react";
import React from "preact/compat";
import { Data, EnhancedHeaderTableProps } from "./types";
import classNames from "classnames";
import { ArrowDropDownIcon } from "../../../components/Main/Icons";
import { ArrowDropDownIcon, InfoIcon } from "../../../components/Main/Icons";
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
export function EnhancedTableHead(props: EnhancedHeaderTableProps) {
const { order, orderBy, onRequestSort, headerCells } = props;
@ -24,7 +25,13 @@ export function EnhancedTableHead(props: EnhancedHeaderTableProps) {
onClick={createSortHandler(headCell.id as keyof Data)}
>
<div className="vm-table-cell__content">
{headCell.label}
{
headCell.info ?
<Tooltip title={headCell.info}>
<div className="vm-metrics-content-header__tip-icon"><InfoIcon /></div>
{headCell.label}
</Tooltip>: <>{headCell.label}</>
}
{headCell.id !== "action" && headCell.id !== "percentage" && (
<div
className={classNames({

View file

@ -1,16 +1,15 @@
import { ChangeEvent, MouseEvent, ReactNode } from "react";
import { MouseEvent, ReactNode } from "react";
export type Order = "asc" | "desc";
export interface HeadCell {
id: string;
label: string | ReactNode;
info?: string;
}
export interface EnhancedHeaderTableProps {
numSelected: number;
onRequestSort: (event: MouseEvent<unknown>, property: keyof Data) => void;
onSelectAllClick: (event: ChangeEvent<HTMLInputElement>) => void;
order: Order;
orderBy: string;
rowCount: number;

View file

@ -1,11 +1,10 @@
import { Containers, DefaultActiveTab, Tabs, TSDBStatus } from "./types";
import { Containers, Tabs, TSDBStatus } from "./types";
import { useRef } from "preact/compat";
import { HeadCell } from "./Table/types";
interface AppState {
tabs: Tabs;
containerRefs: Containers<HTMLDivElement>;
defaultActiveTab: DefaultActiveTab,
}
export default class AppConfigurator {
@ -15,6 +14,7 @@ export default class AppConfigurator {
constructor() {
this.tsdbStatus = this.defaultTSDBStatus;
this.tabsNames = ["table", "graph"];
this.getDefaultState = this.getDefaultState.bind(this);
}
set tsdbStatusData(tsdbStatus: TSDBStatus) {
@ -29,6 +29,7 @@ export default class AppConfigurator {
return {
totalSeries: 0,
totalLabelValuePairs: 0,
totalSeriesByAll: 0,
seriesCountByMetricName: [],
seriesCountByLabelName: [],
seriesCountByFocusLabelValue: [],
@ -37,22 +38,26 @@ export default class AppConfigurator {
};
}
keys(focusLabel: string | null): string[] {
keys(match?: string | null, focusLabel?: string | null): string[] {
const isMetric = match && /__name__=".+"/.test(match);
const isLabel = match && /{.+=".+"}/g.test(match);
const isMetricWithLabel = match && /__name__=".+", .+!=""/g.test(match);
let keys: string[] = [];
if (focusLabel) {
if (focusLabel || isMetricWithLabel) {
keys = keys.concat("seriesCountByFocusLabelValue");
} else if (isMetric) {
keys = keys.concat("labelValueCountByLabelName");
} else if (isLabel) {
keys = keys.concat("seriesCountByMetricName", "seriesCountByLabelName");
} else {
keys = keys.concat("seriesCountByMetricName", "seriesCountByLabelName", "seriesCountByLabelValuePair");
}
keys = keys.concat(
"seriesCountByMetricName",
"seriesCountByLabelName",
"seriesCountByLabelValuePair",
"labelValueCountByLabelName",
);
return keys;
}
get defaultState(): AppState {
return this.keys("job").reduce((acc, cur) => {
getDefaultState(match?: string | null, label?: string | null): AppState {
return this.keys(match, label).reduce((acc, cur) => {
return {
...acc,
tabs: {
@ -63,15 +68,10 @@ export default class AppConfigurator {
...acc.containerRefs,
[cur]: useRef<HTMLDivElement>(null),
},
defaultActiveTab: {
...acc.defaultActiveTab,
[cur]: 0,
},
};
}, {
tabs: {} as Tabs,
containerRefs: {} as Containers<HTMLDivElement>,
defaultActiveTab: {} as DefaultActiveTab,
} as AppState);
}
@ -85,6 +85,53 @@ export default class AppConfigurator {
};
}
get sectionsTips(): Record<string, string> {
return {
seriesCountByMetricName: `
<p>
This table returns a list of metrics with the highest cardinality.
The cardinality of a metric is the number of time series associated with that metric,
where each time series is defined as a unique combination of key-value label pairs.
</p>
<p>
When looking to reduce the number of active series in your data source,
you can start by inspecting individual metrics with high cardinality
(i.e. that have lots of active time series associated with them),
since that single metric contributes a large fraction of the series that make up your total series count.
</p>`,
seriesCountByLabelName: `
<p>
This table returns a list of the labels with the highest number of series.
</p>
<p>
Use this table to identify labels that are storing dimensions with high cardinality
(many different label values).
</p>
<p>
It is recommended to choose labels such that they have a finite set of values,
since every unique combination of key-value label pairs creates a new time series
and therefore can dramatically increase the number of time series in your system.
</p>`,
seriesCountByFocusLabelValue: `
<p>
This table returns a list of unique label values per selected label.
</p>
<p>
Use this table to identify label values that are storing per each selected series.
</p>`,
labelValueCountByLabelName: "",
seriesCountByLabelValuePair: `
<p>
This table returns a list of the label values pairs with the highest number of series.
</p>
<p>
Use this table to identify unique label values pairs. This helps to identify same labels
is applied to count timeseries in your system, since every unique combination of key-value label pairs
creates a new time series and therefore can dramatically increase the number of time series in your system
</p>`,
};
}
get tablesHeaders(): Record<string, HeadCell[]> {
return {
seriesCountByMetricName: METRIC_NAMES_HEADERS,
@ -114,11 +161,12 @@ const METRIC_NAMES_HEADERS = [
},
{
id: "percentage",
label: "Percent of series",
label: "Share in total",
info: "Shows the share of a metric to the total number of series"
},
{
id: "action",
label: "Action",
label: "",
}
] as HeadCell[];
@ -133,11 +181,12 @@ const LABEL_NAMES_HEADERS = [
},
{
id: "percentage",
label: "Percent of series",
label: "Share in total",
info: "Shows the share of the label to the total number of series"
},
{
id: "action",
label: "Action",
label: "",
}
] as HeadCell[];
@ -152,12 +201,12 @@ const FOCUS_LABEL_VALUES_HEADERS = [
},
{
id: "percentage",
label: "Percent of series",
label: "Share in total",
},
{
disablePadding: false,
id: "action",
label: "Action",
label: "",
numeric: false,
}
] as HeadCell[];
@ -173,11 +222,12 @@ export const LABEL_VALUE_PAIRS_HEADERS = [
},
{
id: "percentage",
label: "Percent of series",
label: "Share in total",
info: "Shows the share of the label value pair to the total number of series"
},
{
id: "action",
label: "Action",
label: "",
}
] as HeadCell[];
@ -192,6 +242,6 @@ export const LABEL_NAMES_WITH_UNIQUE_VALUES_HEADERS = [
},
{
id: "action",
label: "Action",
label: "",
}
] as HeadCell[];

View file

@ -1,20 +1,24 @@
import { QueryUpdater } from "./types";
export const queryUpdater: QueryUpdater = {
seriesCountByMetricName: (focusLabel: string | null, query: string): string => {
seriesCountByMetricName: ({ query }): string => {
return getSeriesSelector("__name__", query);
},
seriesCountByLabelName: (focusLabel: string | null, query: string): string => `{${query}!=""}`,
seriesCountByFocusLabelValue: (focusLabel: string | null, query: string): string => {
seriesCountByLabelName: ({ query }): string => {
return `{${query}!=""}`;
},
seriesCountByFocusLabelValue: ({ query, focusLabel }): string => {
return getSeriesSelector(focusLabel, query);
},
seriesCountByLabelValuePair: (focusLabel: string | null, query: string): string => {
seriesCountByLabelValuePair: ({ query }): string => {
const a = query.split("=");
const label = a[0];
const value = a.slice(1).join("=");
return getSeriesSelector(label, value);
},
labelValueCountByLabelName: (focusLabel: string | null, query: string): string => `{${query}!=""}`,
labelValueCountByLabelName: ({ query, match }): string => {
return `${match.replace("}", "")}, ${query}!=""}`;
},
};
const getSeriesSelector = (label: string | null, value: string): string => {

View file

@ -3,8 +3,10 @@ import { useAppState } from "../../../state/common/StateContext";
import { useEffect, useState } from "preact/compat";
import { CardinalityRequestsParams, getCardinalityInfo } from "../../../api/tsdb";
import { TSDBStatus } from "../types";
import { useCardinalityState } from "../../../state/cardinality/CardinalityStateContext";
import AppConfigurator from "../appConfigurator";
import { useSearchParams } from "react-router-dom";
import dayjs from "dayjs";
import { DATE_FORMAT } from "../../../constants/date";
export const useFetchQuery = (): {
fetchUrl?: string[],
@ -13,33 +15,43 @@ export const useFetchQuery = (): {
appConfigurator: AppConfigurator,
} => {
const appConfigurator = new AppConfigurator();
const { topN, extraLabel, match, date, runQuery, focusLabel } = useCardinalityState();
const [searchParams] = useSearchParams();
const match = searchParams.get("match");
const focusLabel = searchParams.get("focusLabel");
const topN = +(searchParams.get("topN") || 10);
const date = searchParams.get("date") || dayjs().tz().format(DATE_FORMAT);
const { serverUrl } = useAppState();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<ErrorTypes | string>();
const [tsdbStatus, setTSDBStatus] = useState<TSDBStatus>(appConfigurator.defaultTSDBStatus);
useEffect(() => {
if (error) {
setTSDBStatus(appConfigurator.defaultTSDBStatus);
setIsLoading(false);
}
}, [error]);
const fetchCardinalityInfo = async (requestParams: CardinalityRequestsParams) => {
if (!serverUrl) return;
setError("");
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);
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;
setTSDBStatus({ ...data });
const { totalSeries } = respTotals.data;
const result = { ...data } as TSDBStatus;
result.totalSeriesByAll = totalSeries;
const name = match?.replace(/[{}"]/g, "");
result.seriesCountByLabelValuePair = result.seriesCountByLabelValuePair.filter(s => s.name !== name);
setTSDBStatus(result);
setIsLoading(false);
} else {
setError(resp.error);
@ -54,8 +66,15 @@ export const useFetchQuery = (): {
useEffect(() => {
fetchCardinalityInfo({ topN, extraLabel, match, date, focusLabel });
}, [serverUrl, runQuery, date]);
fetchCardinalityInfo({ topN, match, date, focusLabel });
}, [serverUrl, match, focusLabel, topN, date]);
useEffect(() => {
if (error) {
setTSDBStatus(appConfigurator.defaultTSDBStatus);
setIsLoading(false);
}
}, [error]);
appConfigurator.tsdbStatusData = tsdbStatus;
return { isLoading, appConfigurator: appConfigurator, error };

View file

@ -1,24 +0,0 @@
import { useEffect } from "react";
import { useCardinalityState } from "../../../state/cardinality/CardinalityStateContext";
import { compactObject } from "../../../utils/object";
import { useSearchParams } from "react-router-dom";
export const useSetQueryParams = () => {
const { topN, match, date, focusLabel, extraLabel } = useCardinalityState();
const [, setSearchParams] = useSearchParams();
const setSearchParamsFromState = () => {
const params = compactObject({
topN,
date,
match,
extraLabel,
focusLabel,
});
setSearchParams(params as Record<string, string>);
};
useEffect(setSearchParamsFromState, [topN, match, date, focusLabel, extraLabel]);
useEffect(setSearchParamsFromState, []);
};

View file

@ -1,75 +1,48 @@
import React, { FC, useState } from "react";
import React, { FC } from "react";
import { useFetchQuery } from "./hooks/useCardinalityFetch";
import { queryUpdater } from "./helpers";
import { Data } from "./Table/types";
import CardinalityConfigurator from "./CardinalityConfigurator/CardinalityConfigurator";
import Spinner from "../../components/Main/Spinner/Spinner";
import { useCardinalityDispatch, useCardinalityState } from "../../state/cardinality/CardinalityStateContext";
import MetricsContent from "./MetricsContent/MetricsContent";
import { DefaultActiveTab, Tabs, TSDBStatus, Containers } from "./types";
import { useSetQueryParams } from "./hooks/useSetQueryParams";
import { Tabs, TSDBStatus, Containers } from "./types";
import Alert from "../../components/Main/Alert/Alert";
import "./style.scss";
import classNames from "classnames";
import useDeviceDetect from "../../hooks/useDeviceDetect";
import { useSearchParams } from "react-router-dom";
import {
TipCardinalityOfLabel,
TipCardinalityOfSingle,
TipHighNumberOfSeries,
TipHighNumberOfValues
} from "./CardinalityTips";
const spinnerMessage = `Please wait while cardinality stats is calculated.
This may take some time if the db contains big number of time series.`;
const Index: FC = () => {
const { isMobile } = useDeviceDetect();
const { topN, match, date, focusLabel } = useCardinalityState();
const cardinalityDispatch = useCardinalityDispatch();
useSetQueryParams();
const configError = "";
const [query, setQuery] = useState(match || "");
const [queryHistoryIndex, setQueryHistoryIndex] = useState(0);
const [queryHistory, setQueryHistory] = useState<string[]>([]);
const onRunQuery = () => {
setQueryHistory(prev => [...prev, query]);
setQueryHistoryIndex(prev => prev + 1);
cardinalityDispatch({ type: "SET_MATCH", payload: query });
cardinalityDispatch({ type: "RUN_QUERY" });
};
const onSetHistory = (step: number) => {
const newIndexHistory = queryHistoryIndex + step;
if (newIndexHistory < 0 || newIndexHistory >= queryHistory.length) return;
setQueryHistoryIndex(newIndexHistory);
setQuery(queryHistory[newIndexHistory]);
};
const onTopNChange = (value: string) => {
cardinalityDispatch({ type: "SET_TOP_N", payload: +value });
};
const onFocusLabelChange = (value: string) => {
cardinalityDispatch({ type: "SET_FOCUS_LABEL", payload: value });
};
const [searchParams, setSearchParams] = useSearchParams();
const showTips = searchParams.get("tips") || "";
const match = searchParams.get("match") || "";
const focusLabel = searchParams.get("focusLabel") || "";
const { isLoading, appConfigurator, error } = useFetchQuery();
const [stateTabs, setTab] = useState(appConfigurator.defaultState.defaultActiveTab);
const { tsdbStatusData, defaultState, tablesHeaders } = appConfigurator;
const handleTabChange = (newValue: string, tabId: string) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
setTab({ ...stateTabs, [tabId]: +newValue });
};
const { tsdbStatusData, getDefaultState, tablesHeaders, sectionsTips } = appConfigurator;
const defaultState = getDefaultState(match, focusLabel);
const handleFilterClick = (key: string) => (name: string) => {
const query = queryUpdater[key](focusLabel, name);
setQuery(query);
setQueryHistory(prev => [...prev, query]);
setQueryHistoryIndex(prev => prev + 1);
cardinalityDispatch({ type: "SET_MATCH", payload: query });
let newFocusLabel = "";
const handleFilterClick = (key: string) => (query: string) => {
const value = queryUpdater[key]({ query, focusLabel, match });
searchParams.set("match", value);
if (key === "labelValueCountByLabelName" || key == "seriesCountByLabelName") {
newFocusLabel = name;
searchParams.set("focusLabel", query);
}
cardinalityDispatch({ type: "SET_FOCUS_LABEL", payload: newFocusLabel });
cardinalityDispatch({ type: "RUN_QUERY" });
if (key == "seriesCountByFocusLabelValue") {
searchParams.set("focusLabel", "");
}
setSearchParams(searchParams);
};
return (
@ -81,35 +54,33 @@ const Index: FC = () => {
>
{isLoading && <Spinner message={spinnerMessage}/>}
<CardinalityConfigurator
error={configError}
query={query}
topN={topN}
date={date}
match={match}
totalSeries={tsdbStatusData.totalSeries}
totalSeriesAll={tsdbStatusData.totalSeriesByAll}
totalLabelValuePairs={tsdbStatusData.totalLabelValuePairs}
focusLabel={focusLabel}
onRunQuery={onRunQuery}
onSetQuery={setQuery}
onSetHistory={onSetHistory}
onTopNChange={onTopNChange}
onFocusLabelChange={onFocusLabelChange}
seriesCountByMetricName={tsdbStatusData.seriesCountByMetricName}
/>
{showTips && (
<div className="vm-cardinality-panel-tips">
{!match && !focusLabel && <TipHighNumberOfSeries/>}
{match && !focusLabel && <TipCardinalityOfSingle/>}
{!match && !focusLabel && <TipHighNumberOfValues/>}
{focusLabel && <TipCardinalityOfLabel/>}
</div>
)}
{error && <Alert variant="error">{error}</Alert>}
{appConfigurator.keys(focusLabel).map((keyName) => (
{appConfigurator.keys(match, focusLabel).map((keyName) => (
<MetricsContent
key={keyName}
sectionTitle={appConfigurator.sectionsTitles(focusLabel)[keyName]}
activeTab={stateTabs[keyName as keyof DefaultActiveTab]}
tip={sectionsTips[keyName]}
rows={tsdbStatusData[keyName as keyof TSDBStatus] as unknown as Data[]}
onChange={handleTabChange}
onActionClick={handleFilterClick(keyName)}
tabs={defaultState.tabs[keyName as keyof Tabs]}
chartContainer={defaultState.containerRefs[keyName as keyof Containers<HTMLDivElement>]}
totalSeries={appConfigurator.totalSeries(keyName)}
tabId={keyName}
tableHeaderCells={tablesHeaders[keyName]}
/>
))}

View file

@ -5,7 +5,17 @@
align-items: flex-start;
gap: $padding-medium;
&_mobile {
&_mobile, &_mobile &-tips {
gap: $padding-small;
}
&-tips {
display: inline-flex;
flex-wrap: wrap;
align-content: flex-start;
justify-content: flex-start;
gap: $padding-medium;
flex-grow: 1;
width: 100%;
}
}

View file

@ -3,6 +3,7 @@ import { MutableRef } from "preact/hooks";
export interface TSDBStatus {
totalSeries: number;
totalLabelValuePairs: number;
totalSeriesByAll: number,
seriesCountByMetricName: TopHeapEntry[];
seriesCountByLabelName: TopHeapEntry[];
seriesCountByFocusLabelValue: TopHeapEntry[];
@ -12,11 +13,17 @@ export interface TSDBStatus {
export interface TopHeapEntry {
name: string;
count: number;
value: number;
}
interface QueryUpdaterArgs {
query: string;
focusLabel: string;
match: string;
}
export type QueryUpdater = {
[key: string]: (focusLabel: string | null, query: string) => string,
[key: string]: (args: QueryUpdaterArgs) => string,
}
export interface Tabs {
@ -34,11 +41,3 @@ export interface Containers<T> {
seriesCountByLabelValuePair: MutableRef<T>;
labelValueCountByLabelName: MutableRef<T>;
}
export interface DefaultActiveTab {
seriesCountByMetricName: number;
seriesCountByLabelName: number;
seriesCountByFocusLabelValue: number;
seriesCountByLabelValuePair: number;
labelValueCountByLabelName: number;
}

View file

@ -16,14 +16,14 @@ import { arrayEquals } from "../../../utils/array";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
export interface QueryConfiguratorProps {
error?: ErrorTypes | string;
errors: (ErrorTypes | string)[];
queryOptions: string[]
onHideQuery: (queries: number[]) => void
onRunQuery: () => void
}
const QueryConfigurator: FC<QueryConfiguratorProps> = ({
error,
errors,
queryOptions,
onHideQuery,
onRunQuery
@ -141,7 +141,7 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
value={stateQuery[i]}
autocomplete={autocomplete}
options={queryOptions}
error={error}
error={errors[i]}
onArrowUp={createHandlerArrow(-1, i)}
onArrowDown={createHandlerArrow(1, i)}
onEnter={handleRunQuery}

View file

@ -41,7 +41,9 @@ const CustomPanel: FC = () => {
const graphDispatch = useGraphDispatch();
const { queryOptions } = useFetchQueryOptions();
const { isLoading, liveData, graphData, error, warning, traces } = useFetchQuery({
const {
isLoading, liveData, graphData, error, queryErrors, warning, traces
} = useFetchQuery({
visible: true,
customStep,
hideQuery,
@ -99,7 +101,7 @@ const CustomPanel: FC = () => {
})}
>
<QueryConfigurator
error={!hideError ? error : ""}
errors={!hideError ? queryErrors : []}
queryOptions={queryOptions}
onHideQuery={handleHideQuery}
onRunQuery={handleRunQuery}

View file

@ -1,24 +0,0 @@
import React, { createContext, FC, useContext, useMemo, useReducer } from "preact/compat";
import { Action, CardinalityState, initialState, reducer } from "./reducer";
import { Dispatch } from "react";
type CardinalityStateContextType = { state: CardinalityState, dispatch: Dispatch<Action> };
export const CardinalityStateContext = createContext<CardinalityStateContextType>({} as CardinalityStateContextType);
export const useCardinalityState = (): CardinalityState => useContext(CardinalityStateContext).state;
export const useCardinalityDispatch = (): Dispatch<Action> => useContext(CardinalityStateContext).dispatch;
export const CardinalityStateProvider: FC = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const contextValue = useMemo(() => {
return { state, dispatch };
}, [state, dispatch]);
return <CardinalityStateContext.Provider value={contextValue}>
{children}
</CardinalityStateContext.Provider>;
};

View file

@ -1,67 +0,0 @@
import dayjs from "dayjs";
import { getQueryStringValue } from "../../utils/query-string";
import { DATE_FORMAT } from "../../constants/date";
export interface CardinalityState {
runQuery: number,
topN: number
date: string | null
match: string | null
extraLabel: string | null
focusLabel: string | null
}
export type Action =
| { type: "SET_TOP_N", payload: number }
| { type: "SET_DATE", payload: string | null }
| { type: "SET_MATCH", payload: string | null }
| { type: "SET_EXTRA_LABEL", payload: string | null }
| { type: "SET_FOCUS_LABEL", payload: string | null }
| { type: "RUN_QUERY" }
export const initialState: CardinalityState = {
runQuery: 0,
topN: getQueryStringValue("topN", 10) as number,
date: getQueryStringValue("date", dayjs().tz().format(DATE_FORMAT)) as string,
focusLabel: getQueryStringValue("focusLabel", "") as string,
match: getQueryStringValue("match", "") as string,
extraLabel: getQueryStringValue("extra_label", "") as string,
};
export function reducer(state: CardinalityState, action: Action): CardinalityState {
switch (action.type) {
case "SET_TOP_N":
return {
...state,
topN: action.payload
};
case "SET_DATE":
return {
...state,
date: action.payload
};
case "SET_MATCH":
return {
...state,
match: action.payload
};
case "SET_EXTRA_LABEL":
return {
...state,
extraLabel: action.payload
};
case "SET_FOCUS_LABEL":
return {
...state,
focusLabel: action.payload,
};
case "RUN_QUERY":
return {
...state,
runQuery: state.runQuery + 1
};
default:
throw new Error();
}
}

View file

@ -23,14 +23,19 @@ created by v1.90.0 or newer versions. The solution is to upgrade to v1.90.0 or n
* FEATURE: log metrics with truncated labels if the length of label value in the ingested metric exceeds `-maxLabelValueLen`. This should simplify debugging for this case.
* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): show target URL when debugging [target relabeling](https://docs.victoriametrics.com/vmagent.html#relabel-debug). This should simplify target relabel debugging a bit. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3882).
* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): add support for [VictoriaMetrics remote write protocol](https://docs.victoriametrics.com/vmagent.html#victoriametrics-remote-write-protocol) when [sending / receiving data to / from Kafka](https://docs.victoriametrics.com/vmagent.html#kafka-integration). This protocol allows saving egress network bandwidth costs when sending data from `vmagent` to `Kafka` located in another datacenter or availability zone. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1225).
* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): add `--kafka.consumer.topic.concurrency` command-line flag. It controls the number of Kafka consumer workers to use by `vmagent`. It should eliminate the need to start multiple `vmagent` instances to improve data transfer rate. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1957).
* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): add `-kafka.consumer.topic.concurrency` command-line flag. It controls the number of Kafka consumer workers to use by `vmagent`. It should eliminate the need to start multiple `vmagent` instances to improve data transfer rate. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1957).
* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): add support for [Kafka producer and consumer](https://docs.victoriametrics.com/vmagent.html#kafka-integration) on `arm64` machines. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2271).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add support for drag'n'drop and paste from clipboard in the "Trace analyzer" page. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3971).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): hide messages longer than 3 lines in the trace. You can view the full message by clicking on the `show more` button. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3971).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add the ability to manually input date and time when selecting a time range. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3968).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): updated usability and the search process in cardinality explorer. Made this process straightforward for user. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3986).
* FEATURE: [vmctl](https://docs.victoriametrics.com/vmctl.html): automatically disable progress bar when TTY isn't available. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3823).
* FEATURE: [vmauth](https://docs.victoriametrics.com/vmauth.html): add `-configCheckInterval` command-line flag, which can be used for automatic re-reading the `-auth.config` file. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3990).
* BUGFIX: prevent from slow [snapshot creating](https://docs.victoriametrics.com/#how-to-work-with-snapshots) under high data ingestion rate. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3551).
* BUGFIX: [vmauth](https://docs.victoriametrics.com/vmauth.html): suppress [proxy protocol](https://www.haproxy.org/download/2.3/doc/proxy-protocol.txt) parsing errors in case of `EOF`. Usually, the error is caused by health checks and is not a sign of an actual error.
* BUGFIX: [vmui](https://docs.victoriametrics.com/#vmui): fix displaying errors for each query. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3987).
* BUGFIX: allow using dashes and dots in environment variables names referred in config files via `%{ENV-VAR.SYNTAX}`. See [these docs](https://docs.victoriametrics.com/#environment-variables) and [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3999).
## [v1.89.1](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.89.1)

View file

@ -42,9 +42,10 @@ If you see unexpected or unreliable query results from VictoriaMetrics, then try
on the given `[start..end]` time range and check whether they are expected:
```console
curl http://victoriametrics:8428/api/v1/export -d 'match[]=http_requests_total' -d 'start=...' -d 'end=...'
single-node: curl http://victoriametrics:8428/api/v1/export -d 'match[]=http_requests_total' -d 'start=...' -d 'end=...'
cluster: curl http://<vmselect>:8481/select/<tenantID>/prometheus/api/v1/export -d 'match[]=http_requests_total' -d 'start=...' -d 'end=...'
```
Note that responses returned from [/api/v1/query](https://docs.victoriametrics.com/keyConcepts.html#instant-query)
and from [/api/v1/query_range](https://docs.victoriametrics.com/keyConcepts.html#range-query) contain **evaluated** data
instead of raw samples stored in VictoriaMetrics. See [these docs](https://prometheus.io/docs/prometheus/latest/querying/basics/#staleness)

View file

@ -14,3 +14,4 @@ sort: 26
8. [How to delete or replace metrics in VictoriaMetrics](https://docs.victoriametrics.com/guides/guide-delete-or-replace-metrics.html)
9. [How to monitor kubernetes cluster using Managed VictoriaMetrics](https://docs.victoriametrics.com/managed-victoriametrics/how-to-monitor-k8s.html)
10. [How to configure vmgateway for multi-tenant access using Grafana and OpenID Connect](https://docs.victoriametrics.com/guides/grafana-vmgateway-openid-configuration.html)
11. [How to setup vmanomaly together with vmalert](https://docs.victoriametrics.com/guide/guide-vmanomaly-vmalert.html)

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 KiB

View file

@ -0,0 +1,414 @@
# vmanomaly Quickstart
**Prerequisites**
- In the tutorial, we'll be using the following VictoriaMetrics components:
- [VictoriaMetrics](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html) (v.1.83.1)
- [vmalert](https://docs.victoriametrics.com/vmalert.html) (v.1.83.1)
- [vmagent](https://docs.victoriametrics.com/vmagent.html) (v.1.83.1)
If you're unfamiliar with the listed components, please read [QuickStart](https://docs.victoriametrics.com/Quick-Start.html) first.
- It is assumed that you are familiar with [Grafana](https://grafana.com/)(v.9.3.1) and [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/).
## What is vmanomaly?
*VictoriaMetrics Anomaly Detection* ([vmanomaly](https://docs.victoriametrics.com/vmanomaly.html)) is a service that continuously scans time series stored in VictoriaMetrics and detects unexpected changes within data patterns in real-time. It does so by utilizing user-configurable machine learning models.
All the service parameters are defined in a config file.
A single config file supports only one model. It is ok to run multiple vmanomaly processes, each using its own config.
**vmanomaly** does the following:
- periodically queries user-specified metrics
- computes an **anomaly score** for them
- pushes back the computed **anomaly score** to VictoriaMetrics.
### What is anomaly score?
**Anomaly score** is a calculated non-negative (in interval [0, +inf)) numeric value. It takes into account how well data fit a predicted distribution, periodical patterns, trends, seasonality, etc.
The value is designed to:
- *fall between 0 and 1* if model consider that datapoint is following usual pattern,
- *exceed 1* if the datapoint is abnormal.
Then, users can enable alerting rules based on the **anomaly score** with [vmalert](#what-is-vmalert).
## What is vmalert?
[vmalert](https://docs.victoriametrics.com/vmalert.html) is an alerting tool for VictoriaMetrics. It executes a list of the given alerting or recording rules against configured `-datasource.url`.
[Alerting rules](https://docs.victoriametrics.com/vmalert.html#alerting-rules) allow you to define conditions that, when met, will notify the user. The alerting condition is defined in a form of a query expression via [MetricsQL query language](https://docs.victoriametrics.com/MetricsQL.html). For example, in our case, the expression `anomaly_score > 1.0` will notify a user when the calculated anomaly score exceeds a threshold of 1.
## How does vmanomaly works with vmalert?
Compared to classical alerting rules, anomaly detection is more "hands-off" and data-aware. Instead of thinking of critical conditions to define, user can rely on catching anomalies that were not expected to happen. In other words, by setting up alerting rules, a user must know what to look for, ahead of time, while anomaly detection looks for any deviations from past behavior.
Practical use case is to put anomaly score generated by vmanomaly into alerting rules with some threshold.
**In this tutorial we are going to:**
- Configure docker-compose file with all needed services ([VictoriaMetrics](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html), [vmalert](https://docs.victoriametrics.com/vmalert.html), [vmagent](https://docs.victoriametrics.com/vmagent.html), [Grafana](https://grafana.com/), [Node Exporter](https://prometheus.io/docs/guides/node-exporter/) and [vmanomaly](https://docs.victoriametrics.com/vmanomaly.html) ).
- Explore configuration files for [vmanomaly](https://docs.victoriametrics.com/vmanomaly.html) and [vmalert](https://docs.victoriametrics.com/vmalert.html).
- Run our own [VictoriaMetrics](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html) database with data scraped from [Node Exporter](https://prometheus.io/docs/guides/node-exporter/).
- Explore data for analysis in [Grafana](https://grafana.com/).
- Explore vmanomaly results.
- Explore vmalert alerts
_____________________________
## Data to analyze
Let's talk about data used for anomaly detection in this tutorial.
We are going to collect our own CPU usage data with [Node Exporter](https://prometheus.io/docs/guides/node-exporter/) into the VictoriaMetrics database.
On a Node Exporter's metrics page, part of the output looks like this:
```
# HELP node_cpu_seconds_total Seconds the CPUs spent in each mode.
# TYPE node_cpu_seconds_total counter
node_cpu_seconds_total{cpu="0",mode="idle"} 94965.14
node_cpu_seconds_total{cpu="0",mode="iowait"} 51.25
node_cpu_seconds_total{cpu="0",mode="irq"} 0
node_cpu_seconds_total{cpu="0",mode="nice"} 0
node_cpu_seconds_total{cpu="0",mode="softirq"} 1682.18
node_cpu_seconds_total{cpu="0",mode="steal"} 0
node_cpu_seconds_total{cpu="0",mode="system"} 995.37
node_cpu_seconds_total{cpu="0",mode="user"} 12378.05
node_cpu_seconds_total{cpu="1",mode="idle"} 94386.53
node_cpu_seconds_total{cpu="1",mode="iowait"} 51.22
...
```
Here, metric `node_cpu_seconds_total` tells us how many seconds each CPU spent in different modes: _user_, _system_, _iowait_, _idle_, _irq&softirq_, _guest_, or _steal_.
These modes are mutually exclusive. A high _iowait_ means that you are disk or network bound, high _user_ or _system_ means that you are CPU bound.
The metric `node_cpu_seconds_total` is a [counter](https://docs.victoriametrics.com/keyConcepts.html#counter) type of metric. If we'd like to see how much time CPU spent in each of the nodes, we need to calculate the per-second values change via [rate function](https://docs.victoriametrics.com/MetricsQL.html#rate): `rate(node_cpu_seconds_total)`.
Here is how this query may look like in Grafana:
![node_cpu_rate_graph](guide-vmanomaly-node-cpu-rate-graph.png "node_cpu_rate_graph")
This query result will generate 8 time series per each cpu, and we will use them as an input for our VM Anomaly Detection. vmanomaly will start learning configured model type separately for each of the time series.
______________________________
## vmanomaly configuration and parameter description
**Parameter description**:
There are 4 main sections in config file:
`scheduler` - defines how often to run and make inferences, as well as what timerange to use to train the model.
`model` - specific model parameters and configurations,
`reader` - how to read data and where it is located
`writer` - where and how to write the generated output.
Let's look into parameters in each section:
* `scheduler`
* `infer_every` - how often trained models will make inferences on new data. Basically, how often to generate new datapoints for anomaly_score. Format examples: 30s, 4m, 2h, 1d. Time granularity ('s' - seconds, 'm' - minutes, 'h' - hours, 'd' - days).
You can look at this as how often a model will write its conclusions on newly added data. Here in example we are asking every 1 minute: based on the previous data, do these new datapoints look abnormal?
* `fit_every` - how often to retrain the models. The higher the frequency -- the fresher the model, but the more CPU it consumes. If omitted, the models will be retrained on each infer_every cycle. Format examples: 30s, 4m, 2h, 1d. Time granularity ('s' - seconds, 'm' - minutes, 'h' - hours, 'd' - days).
* `fit_window` - what data interval to use for model training. Longer intervals capture longer historical behavior and detect seasonalities better, but is slower to adapt to permanent changes to metrics behavior. Recommended value is at least two full seasons. Format examples: 30s, 4m, 2h, 1d. Time granularity ('s' - seconds, 'm' - minutes, 'h' - hours, 'd' - days).
Here is the previous 14 days of data to put into the model training.
* `model`
* `class` - what model to run. You can use your own model or choose from built-in models: Seasonal Trend Decomposition, Facebook Prophet, ZScore, Rolling Quantile, Holt-Winters and ARIMA.
Here we use Facebook Prophet with default parameters (`model.prophet.ProphetModel`). You can put parameters that are available in their [docs](https://facebook.github.io/prophet/docs/quick_start.html).
* `reader`
* `datasource_url` - Data source. An HTTP endpoint that serves `/api/v1/query_range`.
* `queries`: - MetricsQL (extension of PromQL) expressions, where you want to find anomalies.
You can put several queries in a form:
`<QUERY_ALIAS>: "QUERY"`. QUERY_ALIAS will be used as a `for` label in generated metrics and anomaly scores.
* `writer`
* `datasource_url` - Output destination. An HTTP endpoint that serves `/api/v1/import`.
Here is an example of the config file `vmanomaly_config.yml`.
<div class="with-copy" markdown="1">
``` yaml
scheduler:
infer_every: "1m"
fit_every: "2h"
fit_window: "14d"
model:
class: "model.prophet.ProphetModel"
interval_width: 0.98
reader:
datasource_url: "http://victoriametrics:8428/"
queries:
node_cpu_rate: "rate(node_cpu_seconds_total)"
writer:
datasource_url: "http://victoriametrics:8428/"
```
</div>
_____________________________________________
## vmanomaly output
As the result of running vmanomaly, it produces the following metrics:
- `anomaly_score` - the main one. Ideally, if it is between 0.0 and 1.0 it is considered to be a non-anomalous value. If it is greater than 1.0, it is considered an anomaly (but you can reconfigure that in alerting config, of course),
- `yhat` - predicted expected value,
- `yhat_lower` - predicted lower boundary,
- `yhat_upper` - predicted upper boundary,
- `y` - initial query result value.
Here is an example of how output metric will be written into VictoriaMetrics:
`anomaly_score{for="node_cpu_rate", cpu="0", instance="node-xporter:9100", job="node-exporter", mode="idle"} 0.85`
____________________________________________
## vmalert configuration
Here we provide an example of the config for vmalert `vmalert_config.yml`.
<div class="with-copy" markdown="1">
``` yaml
groups:
- name: AnomalyExample
rules:
- alert: HighAnomalyScore
expr: 'anomaly_score > 1.0'
labels:
severity: warning
annotations:
summary: Anomaly Score exceeded 1.0. `rate(node_cpu_seconds_total)` is showing abnormal behavior.
```
</div>
In the query expression we need to put a condition on the generated anomaly scores. Usually if the anomaly score is between 0.0 and 1.0, the analyzed value is not abnormal. The more anomaly score exceeded 1 the more our model is sure that value is an anomaly.
You can choose your threshold value that you consider reasonable based on the anomaly score metric, generated by vmanomaly. One of the best ways is to estimate it visually, by plotting the `anomaly_score` metric, along with predicted "expected" range of `yhat_lower` and `yhat_upper`. Later in this tutorial we will show an example
____________________________________________
## Docker Compose configuration
Now we are going to configure the `docker-compose.yml` file to run all needed services.
Here are all services we are going to run:
<p align="center">
<img src="guide-vmanomaly-docker-compose.png" width="800" alt="Docker compose services">
</p>
* victoriametrics - VictoriaMetrics Time Series Database
* vmagent - is an agent which helps you collect metrics from various sources, relabel and filter the collected metrics and store them in VictoriaMetrics or any other storage systems via Prometheus remote_write protocol.
* [grafana](https://grafana.com/) - visualization tool.
* node-exporter - Prometheus [Node Exporter](https://prometheus.io/docs/guides/node-exporter/) exposes a wide variety of hardware- and kernel-related metrics.
* vmalert - VictoriaMetrics Alerting service.
* vmanomaly - VictoriaMetrics Anomaly Detection service.
### Grafana setup
To enable VictoriaMetrics datasource as the default in Grafana we need to create a file `datasource.yml`
<div class="with-copy" markdown="1">
``` yaml
apiVersion: 1
datasources:
- name: VictoriaMetrics
type: prometheus
access: proxy
url: http://victoriametrics:8428
isDefault: true
```
</div>
### Prometheus config
Let's create `prometheus.yml` file for `vmagent` configuration.
<div class="with-copy" markdown="1">
``` yaml
global:
scrape_interval: 10s
scrape_configs:
- job_name: 'vmagent'
static_configs:
- targets: ['vmagent:8429']
- job_name: 'vmalert'
static_configs:
- targets: ['vmalert:8880']
- job_name: 'victoriametrics'
static_configs:
- targets: ['victoriametrics:8428']
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter:9100']
- job_name: 'vmanomaly'
static_configs:
- targets: [ 'vmanomaly:8500' ]
```
</div>
### Docker-compose
Let's wrap it all up together into the `docker-compose.yml` file
<div class="with-copy" markdown="1">
``` yaml
services:
vmagent:
container_name: vmagent
image: victoriametrics/vmagent:latest
depends_on:
- "victoriametrics"
ports:
- 8429:8429
volumes:
- vmagentdata:/vmagentdata
- ./prometheus.yml:/etc/prometheus/prometheus.yml
command:
- "--promscrape.config=/etc/prometheus/prometheus.yml"
- "--remoteWrite.url=http://victoriametrics:8428/api/v1/write"
networks:
- vm_net
restart: always
victoriametrics:
container_name: victoriametrics
image: victoriametrics/victoria-metrics:v1.83.1
ports:
- 8428:8428
- 8089:8089
- 8089:8089/udp
- 2003:2003
- 2003:2003/udp
- 4242:4242
volumes:
- vmdata:/storage
command:
- "--storageDataPath=/storage"
- "--graphiteListenAddr=:2003"
- "--opentsdbListenAddr=:4242"
- "--httpListenAddr=:8428"
- "--influxListenAddr=:8089"
- "--vmalert.proxyURL=http://vmalert:8880"
networks:
- vm_net
restart: always
grafana:
container_name: grafana
image: grafana/grafana-oss:9.3.1
depends_on:
- "victoriametrics"
ports:
- 3000:3000
volumes:
- grafanadata:/var/lib/grafana
- ./datasource.yml:/etc/grafana/provisioning/datasources/datasource.yml
networks:
- vm_net
restart: always
vmalert:
container_name: vmalert
image: victoriametrics/vmalert:latest
depends_on:
- "victoriametrics"
ports:
- 8880:8880
volumes:
- ./vmalert_config.yml:/etc/alerts/alerts.yml
command:
- "--datasource.url=http://victoriametrics:8428/"
- "--remoteRead.url=http://victoriametrics:8428/"
- "--remoteWrite.url=http://victoriametrics:8428/"
- "--notifier.url=http://alertmanager:9093/"
- "--rule=/etc/alerts/*.yml"
# display source of alerts in grafana
- "--external.url=http://127.0.0.1:3000" #grafana outside container
# when copypaste the line be aware of '$$' for escaping in '$expr'
- '--external.alert.source=explore?orgId=1&left=["now-1h","now","VictoriaMetrics",{"expr":{{$$expr|jsonEscape|queryEscape}} },{"mode":"Metrics"},{"ui":[true,true,true,"none"]}]'
networks:
- vm_net
restart: always
vmanomaly:
container_name: vmanomaly
image: us-docker.pkg.dev/victoriametrics-test/public/vmanomaly-trial:v1.3.0
depends_on:
- "victoriametrics"
ports:
- "8500:8500"
networks:
- vm_net
restart: always
volumes:
- ./vmanomaly_config.yml:/config.yaml
command: [ "/config.yaml" ]
node-exporter:
image: quay.io/prometheus/node-exporter:latest
container_name: node-exporter
ports:
- 9100:9100
pid: host
restart: unless-stopped
networks:
- vm_net
volumes:
vmagentdata: {}
vmdata: {}
grafanadata: {}
networks:
vm_net:
```
</div>
Before running our docker-compose make sure that your directory contains all required files:
<p align="center">
<img src="guide-vmanomaly-files.png" width="400" alt="all files">
</p>
This docker-compose file will pull docker images, set up each service and run them all together with the command:
<div class="with-copy" markdown="1">
```
docker-compose up -d
```
</div>
___________________________________________________________
## Model results
To look at model results we need to go to grafana on the `localhost:3000`. Data
vmanomaly need some time to generate more data to visualize.
Let's investigate model output visualization in Grafana.
In the Grafana Explore tab enter queries:
* `anomaly_score`
* `yhat`
* `yhat_lower`
* `yhat_upper`
Each of these metrics will contain same labels our query `rate(node_cpu_seconds_total)` returns.
### Anomaly scores for each metric with its according labels.
Query: `anomaly_score`
![Anomaly score graph](guide-vmanomaly-anomaly-score.png "Anomaly score graph1")
<br>Check out if the anomaly score is high for datapoints you think are anomalies. If not, you can try other parameters in the config file or try other model type.
As you may notice a lot of data shows anomaly score greater than 1. It is expected as we just started to scrape and store data and there are not enough datapoints to train on. Just wait for some more time for gathering more data to see how well this particular model can find anomalies. In our configs we put 2 days of data required.
### Actual value from input query with predicted `yhat` metric.
Query: `yhat`
![Yhat](guide-vmanomaly-yhat.png "yhat")
<br>Here we are using one particular set of metrics for visualization. Check out the difference between model prediction and actual values. If values are very different from prediction, it can be considered as anomalous.
### Lower and upper boundaries that model predicted.
Queries: `yhat_lower` and `yhat_upper`
![Yhat_lower and upper](guide-vmanomaly-yhat-lower-upper.png "_lower and _upper")
Boundaries of 'normal' metric values according to model inference.
### Alerting
On the page `http://localhost:8880/vmalert/groups` you can find our configured Alerting rule:
![alerting_rule](guide-vmanomaly-alert-rule.png "alert rule")
According to the rule configured for vmalert we will see Alert when anomaly score exceed 1. You will see an alert on Alert tab. `http://localhost:8880/vmalert/alerts`
![alerts](guide-vmanomaly-alerts-firing.png "alerts firing")
## Conclusion
Now we know how to set up Victoria Metric Anomaly Detection tool and use it together with vmalert. We also discovered core vmanomaly generated metrics and behaviour.

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 KiB

View file

@ -58,10 +58,19 @@ and sending the data to the Prometheus-compatible remote storage:
The path can point either to local file or to http url. `vmagent` doesn't support some sections of Prometheus config file,
so you may need either to delete these sections or to run `vmagent` with `-promscrape.config.strictParse=false` command-line flag.
In this case `vmagent` ignores unsupported sections. See [the list of unsupported sections](#unsupported-prometheus-config-sections).
* `-remoteWrite.url` with Prometheus-compatible remote storage endpoint such as VictoriaMetrics, the `-remoteWrite.url` argument can be specified
multiple times to replicate data concurrently to multiple remote storage systems. See [various use cases](#use-cases).
* `-remoteWrite.url` with Prometheus-compatible remote storage endpoint such as VictoriaMetrics.
Example command line:
Example command for writing the data recieved via [supported push-based protocols](#how-to-push-data-to-vmagent)
to [single-node VictoriaMetrics](https://docs.victoriametrics.com/) located at `victoria-metrics-host:8428`:
```console
/path/to/vmagent -remoteWrite.url=https://victoria-metrics-host:8428/api/v1/write
```
See [these docs](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#url-format) if you need writing
the data to [VictoriaMetrics cluster](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html).
Example command for scraping Prometheus targets and writing the data to single-node VictoriaMetrics:
```console
/path/to/vmagent -promscrape.config=/path/to/prometheus.yml -remoteWrite.url=https://victoria-metrics-host:8428/api/v1/write
@ -72,18 +81,12 @@ See [how to scrape Prometheus-compatible targets](#how-to-collect-metrics-in-pro
If you use single-node VictoriaMetrics, then you can discover and scrape Prometheus-compatible targets directly from VictoriaMetrics
without the need to use `vmagent` - see [these docs](https://docs.victoriametrics.com/#how-to-scrape-prometheus-exporters-such-as-node-exporter).
If you don't need to scrape Prometheus-compatible targets, then the `-promscrape.config` option isn't needed.
For example, the following command is sufficient for accepting data via [supported push-based protocols](#how-to-push-data-to-vmagent)
and sending it to the provided `-remoteWrite.url`:
```console
/path/to/vmagent -remoteWrite.url=https://victoria-metrics-host:8428/api/v1/write
```
`vmagent` can save network bandwidth usage costs under high load when [VictoriaMetrics remote write protocol is enabled](#victoriametrics-remote-write-protocol).
`vmagent` can save network bandwidth usage costs under high load when [VictoriaMetrics remote write protocol is used](#victoriametrics-remote-write-protocol).
See [troubleshooting docs](#troubleshooting) if you encounter common issues with `vmagent`.
See [various use cases](#use-cases) for vmagent.
Pass `-help` to `vmagent` in order to see [the full list of supported command-line flags with their descriptions](#advanced-usage).
## How to push data to vmagent
@ -155,6 +158,11 @@ If a single remote storage instance temporarily is out of service, then the coll
`vmagent` buffers the collected data in files at `-remoteWrite.tmpDataPath` until the remote storage becomes available again
and then it sends the buffered data to the remote storage in order to prevent data gaps.
[VictoriaMetrics cluster](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html) already supports replication,
so there is no need in specifying multiple `-remoteWrite.url` flags when writing data to the same cluster.
See [these docs](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#replication-and-data-safety).
### Relabeling and filtering
`vmagent` can add, remove or update labels on the collected data before sending it to the remote storage. Additionally,

View file

@ -920,7 +920,7 @@ The shortlist of configuration flags is the following:
-evaluationInterval duration
How often to evaluate the rules (default 1m0s)
-external.alert.source string
External Alert Source allows to override the Source link for alerts sent to AlertManager for cases where you want to build a custom link to Grafana, Prometheus or any other service. Supports templating - see https://docs.victoriametrics.com/vmalert.html#templating . For example, link to Grafana: -external.alert.source='explore?orgId=1&left=["now-1h","now","VictoriaMetrics",{"expr":{{$expr|jsonEscape|queryEscape}} },{"mode":"Metrics"},{"ui":[true,true,true,"none"]}]' . If empty 'vmalert/alert?group_id={{.GroupID}}&alert_id={{.AlertID}}' is used.
External Alert Source allows to override the Source link for alerts sent to AlertManager for cases where you want to build a custom link to Grafana, Prometheus or any other service. Supports templating - see https://docs.victoriametrics.com/vmalert.html#templating . For example, link to Grafana: -external.alert.source='explore?orgId=1&left=["now-1h","now","VictoriaMetrics",{"expr":{{$expr|jsonEscape|queryEscape}} },{"mode":"Metrics"},{"ui":[true,true,true,"none"]}]'. Link to VMUI: -external.alert.source='vmui/#/?g0.expr={{.Expr|queryEscape}}'. If empty 'vmalert/alert?group_id={{.GroupID}}&alert_id={{.AlertID}}' is used.
-external.label array
Optional label in the form 'Name=value' to add to all generated recording rules and alerts. Pass multiple -label flags in order to add multiple label sets.
Supports an array of values separated by comma or specified via multiple flags.

View file

@ -21,7 +21,12 @@ and pass the following flag to `vmauth` binary in order to start authorizing and
After that `vmauth` starts accepting HTTP requests on port `8427` and routing them according to the provided [-auth.config](#auth-config).
The port can be modified via `-httpListenAddr` command-line flag.
The auth config can be reloaded either by passing `SIGHUP` signal to `vmauth` or by querying `/-/reload` http endpoint.
The auth config can be reloaded via the following ways:
- By passing `SIGHUP` signal to `vmauth`.
- By querying `/-/reload` http endpoint. This endpoint can be protected with `-reloadAuthKey` command-line flag. See [security docs](#security) for more details.
- By specifying `-configCheckInterval` command-line flag to the interval between config re-reads. For example, `-configCheckInterval=5s` will re-read the config
and apply new changes every 5 seconds.
Docker images for `vmauth` are available [here](https://hub.docker.com/r/victoriametrics/vmauth/tags).
@ -264,6 +269,8 @@ See the docs at https://docs.victoriametrics.com/vmauth.html .
-auth.config string
Path to auth config. It can point either to local file or to http url. See https://docs.victoriametrics.com/vmauth.html for details on the format of this auth config
-configCheckInterval duration
Interval for config file re-read. Zero value disables config re-reading. By default, refreshing is disabled, send SIGHUP for config refresh.
-enableTCP6
Whether to enable IPv6 for listening and dialing. By default only IPv4 TCP and UDP is used
-envflag.enable

View file

@ -113,4 +113,7 @@ func isValidEnvVarName(s string) bool {
return envVarNameRegex.MatchString(s)
}
var envVarNameRegex = regexp.MustCompile("^[a-zA-Z0-9_]+$")
// envVarNameRegex is used for validating environment variable names.
//
// Allow dashes and dots in env var names - see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3999
var envVarNameRegex = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_\-.]*$`)

View file

@ -24,7 +24,9 @@ func TestExpandTemplates(t *testing.T) {
f([]string{"foo=%{bar}", "bar=x"}, []string{"bar=x", "foo=x"})
f([]string{"a=x%{b}", "b=y%{c}z%{d}", "c=123", "d=qwe"}, []string{"a=xy123zqwe", "b=y123zqwe", "c=123", "d=qwe"})
f([]string{"a=x%{b}y", "b=z%{a}q", "c"}, []string{"a=xzxzxzxz%{a}qyqyqyqy", "b=zxzxzxzx%{b}yqyqyqyq", "c="})
f([]string{"a=%{x.y}"}, []string{"a=%{x.y}"})
f([]string{"a=%{x.y}", "x.y=test"}, []string{"a=test", "x.y=test"})
f([]string{"a=%{x y}"}, []string{"a=%{x y}"})
f([]string{"a=%{123}"}, []string{"a=%{123}"})
}
func TestLookupEnv(t *testing.T) {
@ -49,7 +51,9 @@ func TestLookupEnv(t *testing.T) {
func TestReplaceSuccess(t *testing.T) {
envVars = map[string]string{
"foo": "bar",
"foo": "bar",
"foo.bar_1": "baz",
"foo-bar_2": "test",
}
f := func(s, resultExpected string) {
t.Helper()
@ -71,7 +75,8 @@ func TestReplaceSuccess(t *testing.T) {
f("", "")
f("foo", "foo")
f("a %{foo}-x", "a bar-x")
f("%{foo.bar}", "%{foo.bar}")
f("%{foo.bar_1}", "baz")
f("qq.%{foo-bar_2}.ww", "qq.test.ww")
}
func TestReplaceFailure(t *testing.T) {
@ -85,4 +90,7 @@ func TestReplaceFailure(t *testing.T) {
}
}
f("foo %{bar} %{baz}")
f("%{Foo_Foo_1}")
f("%{Foo-Bar-2}")
f("%{Foo.Baz.3}")
}

View file

@ -5,6 +5,7 @@ import (
"errors"
"flag"
"fmt"
"io"
"net"
"time"
@ -98,7 +99,9 @@ func (ln *TCPListener) Accept() (net.Conn, error) {
if ln.useProxyProtocol {
pConn, err := newProxyProtocolConn(conn)
if err != nil {
proxyProtocolReadErrorLogger.Errorf("cannot read proxy proto conn for TCP addr %q: %s", ln.Addr(), err)
if !errors.Is(err, io.EOF) {
proxyProtocolReadErrorLogger.Errorf("cannot read proxy proto conn for TCP addr %q: %s", ln.Addr(), err)
}
_ = conn.Close()
continue
}