Merge branch 'public-single-node' into pmm-6401-read-prometheus-data-files
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
8
app/vmctl/terminal/windows..go
Normal file
|
@ -0,0 +1,8 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package terminal
|
||||
|
||||
func isTerminal(fd int) bool {
|
||||
return true
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
export interface CardinalityRequestsParams {
|
||||
topN: number,
|
||||
extraLabel: string | null,
|
||||
match: string | null,
|
||||
date: string | null,
|
||||
focusLabel: string | null,
|
||||
|
|
|
@ -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 }) => {
|
|||
}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -58,6 +58,8 @@
|
|||
&__error {
|
||||
top: calc((100% - ($font-size-small/2)) - 2px);
|
||||
color: $color-error;
|
||||
pointer-events: auto;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
&__helper-text {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
@ -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> "label=value" 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>;
|
||||
};
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
<Link
|
||||
href='https://docs.victoriametrics.com/FAQ.html#what-is-high-cardinality'
|
||||
target={"_blank"}
|
||||
>cardinality</Link> and
|
||||
<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
|
||||
<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
|
||||
<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
|
||||
<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 you’re 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 you’d like to inspect.
|
||||
For the selected label name, you’ll see the label values that have the highest number of series associated with
|
||||
them.
|
||||
So if you’ve 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, you’d know, for example, that host-01 was responsible for sending
|
||||
the majority of the time series.
|
||||
</p>
|
||||
</TipCard>
|
||||
);
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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, []);
|
||||
};
|
|
@ -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]}
|
||||
/>
|
||||
))}
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>;
|
||||
};
|
||||
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
BIN
docs/guides/guide-vmanomaly-alert-rule.png
Normal file
After Width: | Height: | Size: 150 KiB |
BIN
docs/guides/guide-vmanomaly-alerts-firing.png
Normal file
After Width: | Height: | Size: 347 KiB |
BIN
docs/guides/guide-vmanomaly-anomaly-score.png
Normal file
After Width: | Height: | Size: 364 KiB |
BIN
docs/guides/guide-vmanomaly-docker-compose.png
Normal file
After Width: | Height: | Size: 140 KiB |
BIN
docs/guides/guide-vmanomaly-files.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
docs/guides/guide-vmanomaly-node-cpu-rate-graph.png
Normal file
After Width: | Height: | Size: 634 KiB |
414
docs/guides/guide-vmanomaly-vmalert.md
Normal 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.
|
BIN
docs/guides/guide-vmanomaly-yhat-lower-upper.png
Normal file
After Width: | Height: | Size: 340 KiB |
BIN
docs/guides/guide-vmanomaly-yhat.png
Normal file
After Width: | Height: | Size: 440 KiB |
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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_\-.]*$`)
|
||||
|
|
|
@ -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}")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|