Compare commits
34 Commits
docs/guide
...
issue-1137
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfeb5ce27a | ||
|
|
8a3757d21b | ||
|
|
b6952cf346 | ||
|
|
d142a1682f | ||
|
|
bcb653611c | ||
|
|
6cc9a6a2f3 | ||
|
|
88a94bf84c | ||
|
|
c620cb30b8 | ||
|
|
0033834d3c | ||
|
|
ce7b1fba58 | ||
|
|
8855e983b9 | ||
|
|
6b3dc18654 | ||
|
|
e24adb1501 | ||
|
|
0c2dd583c8 | ||
|
|
029540c356 | ||
|
|
4baba77b15 | ||
|
|
a8759a539c | ||
|
|
f32b743efe | ||
|
|
8fbf865d9e | ||
|
|
80b6b56028 | ||
|
|
5bdcc5050e | ||
|
|
e425aebbc2 | ||
|
|
f52771ceaf | ||
|
|
eeef07836e | ||
|
|
f1a9c61ba0 | ||
|
|
6cb014fde5 | ||
|
|
565ecdc4fb | ||
|
|
06f4fde931 | ||
|
|
203eb3a2b4 | ||
|
|
7827647b96 | ||
|
|
45eb275910 | ||
|
|
776a40fe06 | ||
|
|
64e343ab4f | ||
|
|
891dfd4e52 |
16
.github/workflows/build.yml
vendored
@@ -73,11 +73,19 @@ jobs:
|
||||
id: go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
cache-dependency-path: |
|
||||
go.sum
|
||||
Makefile
|
||||
app/**/Makefile
|
||||
cache: false
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Cache Go build artifacts
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: go-build-${{ matrix.os }}-${{ matrix.arch }}-${{ hashFiles('go.sum', 'Makefile') }}
|
||||
restore-keys: |
|
||||
go-build-${{ matrix.os }}-${{ matrix.arch }}-
|
||||
|
||||
- run: go version
|
||||
|
||||
- name: Build victoria-metrics for ${{ matrix.os }}-${{ matrix.arch }}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
[](https://github.com/VictoriaMetrics/VictoriaMetrics/releases)
|
||||
[](https://hub.docker.com/u/victoriametrics)
|
||||
[](https://goreportcard.com/report/github.com/VictoriaMetrics/VictoriaMetrics)
|
||||
[](https://github.com/VictoriaMetrics/VictoriaMetrics/actions/workflows/build.yml)
|
||||
[](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/LICENSE)
|
||||
[](https://slack.victoriametrics.com)
|
||||
|
||||
108
app/vmagent/remotewrite/obfuscate.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package remotewrite
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
|
||||
)
|
||||
|
||||
type obfuscateLabelsCtx struct {
|
||||
labels []prompb.Label
|
||||
|
||||
// buf holds allocations for cached results below
|
||||
buf []byte
|
||||
|
||||
// cacheResults maps original label values to their SHA-256 hex digests,
|
||||
// avoiding redundant hashing of repeated values within a single batch.
|
||||
cacheResults map[string]string
|
||||
}
|
||||
|
||||
func (olctx *obfuscateLabelsCtx) reset() {
|
||||
promrelabel.CleanLabels(olctx.labels)
|
||||
olctx.labels = olctx.labels[:0]
|
||||
clear(olctx.cacheResults)
|
||||
olctx.buf = olctx.buf[:0]
|
||||
}
|
||||
|
||||
var obfuscateLabelsCtxPool = &sync.Pool{
|
||||
New: func() any {
|
||||
return &obfuscateLabelsCtx{
|
||||
cacheResults: make(map[string]string),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func getObfuscateLabelsCtx() *obfuscateLabelsCtx {
|
||||
return obfuscateLabelsCtxPool.Get().(*obfuscateLabelsCtx)
|
||||
}
|
||||
|
||||
func putObfuscateLabelsCtx(ctx *obfuscateLabelsCtx) {
|
||||
ctx.reset()
|
||||
obfuscateLabelsCtxPool.Put(ctx)
|
||||
}
|
||||
|
||||
func (olctx *obfuscateLabelsCtx) obfuscate(tss []prompb.TimeSeries, obfuscateLabels []string) []prompb.TimeSeries {
|
||||
if len(obfuscateLabels) == 0 || len(tss) == 0 {
|
||||
return tss
|
||||
}
|
||||
labels := olctx.labels
|
||||
for i := range tss {
|
||||
ts := &tss[i]
|
||||
labelsLen := len(labels)
|
||||
labels = append(labels, ts.Labels...)
|
||||
found := false
|
||||
for _, labelName := range obfuscateLabels {
|
||||
tmp := promrelabel.GetLabelByName(labels[labelsLen:], labelName)
|
||||
if tmp == nil {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
if obfuscatedValue, ok := olctx.cacheResults[tmp.Value]; ok {
|
||||
// fast path: the obfuscated result was calculated before
|
||||
tmp.Value = obfuscatedValue
|
||||
continue
|
||||
}
|
||||
|
||||
digest := sha256.Sum256(bytesutil.ToUnsafeBytes(tmp.Value))
|
||||
buf := olctx.buf
|
||||
bufLen := len(buf)
|
||||
buf = hex.AppendEncode(buf, digest[:])
|
||||
obfuscatedValue := bytesutil.ToUnsafeString(buf[bufLen:])
|
||||
|
||||
olctx.buf = buf
|
||||
olctx.cacheResults[tmp.Value] = obfuscatedValue
|
||||
tmp.Value = obfuscatedValue
|
||||
}
|
||||
if found {
|
||||
ts.Labels = labels[labelsLen:]
|
||||
} else {
|
||||
labels = labels[:labelsLen]
|
||||
}
|
||||
}
|
||||
olctx.labels = labels
|
||||
return tss
|
||||
}
|
||||
|
||||
func (rwctx *remoteWriteCtx) initObfuscateLabels() {
|
||||
if len(*obfuscateLabels) == 0 {
|
||||
return
|
||||
}
|
||||
idx := rwctx.idx
|
||||
rwObfuscateLabels := obfuscateLabels.GetOptionalArg(idx)
|
||||
rwObfuscateLabelsList := strings.Split(rwObfuscateLabels, "^^")
|
||||
|
||||
for _, label := range rwObfuscateLabelsList {
|
||||
if label == "" {
|
||||
continue
|
||||
}
|
||||
if !slices.Contains(rwctx.obfuscateLabels, label) {
|
||||
rwctx.obfuscateLabels = append(rwctx.obfuscateLabels, label)
|
||||
}
|
||||
}
|
||||
}
|
||||
186
app/vmagent/remotewrite/obfuscate_test.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package remotewrite
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
|
||||
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
)
|
||||
|
||||
func TestRemoteWriteObfuscateLabels(t *testing.T) {
|
||||
f := func(obfuscateLabelList string, inputTss []prompb.TimeSeries, expectedTss []prompb.TimeSeries) {
|
||||
t.Helper()
|
||||
rwctx := &remoteWriteCtx{
|
||||
idx: 0,
|
||||
}
|
||||
olctx := &obfuscateLabelsCtx{
|
||||
cacheResults: make(map[string]string),
|
||||
}
|
||||
defer metrics.UnregisterAllMetrics()
|
||||
originValue := *obfuscateLabels
|
||||
defer func() {
|
||||
*obfuscateLabels = originValue
|
||||
}()
|
||||
*obfuscateLabels = []string{obfuscateLabelList}
|
||||
rwctx.initObfuscateLabels()
|
||||
|
||||
outputTss := olctx.obfuscate(inputTss, rwctx.obfuscateLabels)
|
||||
|
||||
if !reflect.DeepEqual(expectedTss, outputTss) {
|
||||
t.Fatalf("unexpected samples;\ngot\n%v\nwant\n%v", outputTss, expectedTss)
|
||||
}
|
||||
}
|
||||
|
||||
sha256Result := func(str string) string {
|
||||
sha256Result := sha256.Sum256([]byte(str))
|
||||
return hex.EncodeToString(sha256Result[:])
|
||||
}
|
||||
|
||||
// 1. obfuscation is not set.
|
||||
f("",
|
||||
[]prompb.TimeSeries{
|
||||
{
|
||||
Labels: []prompb.Label{
|
||||
{Name: "ip", Value: "123"},
|
||||
{Name: "instance", Value: "1234"},
|
||||
},
|
||||
Samples: []prompb.Sample{
|
||||
{Value: 1, Timestamp: 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
[]prompb.TimeSeries{
|
||||
{
|
||||
Labels: []prompb.Label{
|
||||
{Name: "ip", Value: "123"},
|
||||
{Name: "instance", Value: "1234"},
|
||||
},
|
||||
Samples: []prompb.Sample{
|
||||
{Value: 1, Timestamp: 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// 1. obfuscation is set for another rwctx.
|
||||
f(",ip",
|
||||
[]prompb.TimeSeries{
|
||||
{
|
||||
Labels: []prompb.Label{
|
||||
{Name: "ip", Value: "123"},
|
||||
{Name: "instance", Value: "1234"},
|
||||
},
|
||||
Samples: []prompb.Sample{
|
||||
{Value: 1, Timestamp: 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
[]prompb.TimeSeries{
|
||||
{
|
||||
Labels: []prompb.Label{
|
||||
{Name: "ip", Value: "123"},
|
||||
{Name: "instance", Value: "1234"},
|
||||
},
|
||||
Samples: []prompb.Sample{
|
||||
{Value: 1, Timestamp: 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// 2. obfuscate the value of "ip" label
|
||||
f("ip",
|
||||
[]prompb.TimeSeries{
|
||||
{
|
||||
Labels: []prompb.Label{
|
||||
{Name: "ip", Value: "123"},
|
||||
{Name: "instance", Value: "1234"},
|
||||
},
|
||||
Samples: []prompb.Sample{
|
||||
{Value: 1, Timestamp: 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
[]prompb.TimeSeries{
|
||||
{
|
||||
Labels: []prompb.Label{
|
||||
{Name: "ip", Value: sha256Result("123")},
|
||||
{Name: "instance", Value: "1234"},
|
||||
},
|
||||
Samples: []prompb.Sample{
|
||||
{Value: 1, Timestamp: 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// 3. obfuscate the values of "ip" and "instance"
|
||||
f("ip^^instance",
|
||||
[]prompb.TimeSeries{
|
||||
{
|
||||
Labels: []prompb.Label{
|
||||
{Name: "ip", Value: "123"},
|
||||
{Name: "instance", Value: "1234"},
|
||||
},
|
||||
Samples: []prompb.Sample{
|
||||
{Value: 1, Timestamp: 0},
|
||||
},
|
||||
},
|
||||
{
|
||||
Labels: []prompb.Label{
|
||||
{Name: "job", Value: "123"},
|
||||
},
|
||||
Samples: []prompb.Sample{
|
||||
{Value: 1, Timestamp: 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
[]prompb.TimeSeries{
|
||||
{
|
||||
Labels: []prompb.Label{
|
||||
{Name: "ip", Value: sha256Result("123")},
|
||||
{Name: "instance", Value: sha256Result("1234")},
|
||||
},
|
||||
Samples: []prompb.Sample{
|
||||
{Value: 1, Timestamp: 0},
|
||||
},
|
||||
},
|
||||
{
|
||||
Labels: []prompb.Label{
|
||||
{Name: "job", Value: "123"},
|
||||
},
|
||||
Samples: []prompb.Sample{
|
||||
{Value: 1, Timestamp: 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// 4. duplicate label names in config must produce single SHA-256, not double
|
||||
f("ip^^ip",
|
||||
[]prompb.TimeSeries{
|
||||
{
|
||||
Labels: []prompb.Label{
|
||||
{Name: "ip", Value: "123"},
|
||||
},
|
||||
Samples: []prompb.Sample{
|
||||
{Value: 1, Timestamp: 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
[]prompb.TimeSeries{
|
||||
{
|
||||
Labels: []prompb.Label{
|
||||
{Name: "ip", Value: sha256Result("123")},
|
||||
},
|
||||
Samples: []prompb.Sample{
|
||||
{Value: 1, Timestamp: 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
94
app/vmagent/remotewrite/obfuscate_timing_test.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package remotewrite
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
|
||||
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
)
|
||||
|
||||
func BenchmarkRemoteWriteObfuscateLabels(b *testing.B) {
|
||||
originValue := *obfuscateLabels
|
||||
defer func() {
|
||||
*obfuscateLabels = originValue
|
||||
}()
|
||||
*obfuscateLabels = []string{"ip^^instance"}
|
||||
sha256Result := func(str string) string {
|
||||
sha256Result := sha256.Sum256([]byte(str))
|
||||
return hex.EncodeToString(sha256Result[:])
|
||||
}
|
||||
expected := []prompb.TimeSeries{
|
||||
{
|
||||
Labels: []prompb.Label{
|
||||
{Name: "ip", Value: sha256Result("123")},
|
||||
{Name: "instance", Value: sha256Result("12345")},
|
||||
{Name: "__name__", Value: "http_requests_total"},
|
||||
},
|
||||
Samples: []prompb.Sample{
|
||||
{Value: 1, Timestamp: 0},
|
||||
},
|
||||
},
|
||||
{
|
||||
Labels: []prompb.Label{
|
||||
{Name: "ip", Value: sha256Result("1236")},
|
||||
{Name: "instance", Value: sha256Result("some-long-instante-string")},
|
||||
{Name: "__name__", Value: "concurrent_requests"},
|
||||
},
|
||||
Samples: []prompb.Sample{
|
||||
{Value: 1, Timestamp: 0},
|
||||
},
|
||||
},
|
||||
}
|
||||
defer metrics.UnregisterAllMetrics()
|
||||
|
||||
inputTss := []prompb.TimeSeries{
|
||||
{
|
||||
Labels: []prompb.Label{
|
||||
{Name: "ip", Value: "123"},
|
||||
{Name: "instance", Value: "12345"},
|
||||
{Name: "__name__", Value: "http_requests_total"},
|
||||
},
|
||||
Samples: []prompb.Sample{
|
||||
{Value: 1, Timestamp: 0},
|
||||
},
|
||||
},
|
||||
{
|
||||
Labels: []prompb.Label{
|
||||
{Name: "ip", Value: "1236"},
|
||||
{Name: "instance", Value: "some-long-instante-string"},
|
||||
{Name: "__name__", Value: "concurrent_requests"},
|
||||
},
|
||||
Samples: []prompb.Sample{
|
||||
{Value: 1, Timestamp: 0},
|
||||
},
|
||||
},
|
||||
}
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
rwctx := &remoteWriteCtx{
|
||||
idx: 0,
|
||||
}
|
||||
olctx := &obfuscateLabelsCtx{
|
||||
cacheResults: make(map[string]string),
|
||||
}
|
||||
rwctx.initObfuscateLabels()
|
||||
var localTss []prompb.TimeSeries
|
||||
for pb.Next() {
|
||||
// always make a shallow copy because obfuscate changes input
|
||||
localTss = localTss[:0]
|
||||
localTss = append(localTss, inputTss...)
|
||||
olctx.reset()
|
||||
outputTss := olctx.obfuscate(localTss, rwctx.obfuscateLabels)
|
||||
if !reflect.DeepEqual(expected, outputTss) {
|
||||
b.Fatalf("unexpected output: got: \n%v\n want: \n%v\n", outputTss, expected)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
@@ -107,7 +107,12 @@ var (
|
||||
"By default, metadata sending is controlled by the global -enableMetadata flag")
|
||||
|
||||
enableMdx = flagutil.NewArrayBool("remoteWrite.mdx.enable", "Whether to only retain metrics from VictoriaMetrics services before sending them to the corresponding -remoteWrite.url. "+
|
||||
"Can be combined with -remoteWrite.obfuscateLabels to hide sensitive label values in the forwarded metrics. "+
|
||||
"Please see https://docs.victoriametrics.com/victoriametrics/vmagent/#monitoring-data-exchange")
|
||||
obfuscateLabels = flagutil.NewArrayString("remoteWrite.obfuscateLabels", "List of label names whose values will be obfuscated before being sent to the corresponding -remoteWrite.url. "+
|
||||
"Multiple label names should be separated by `^^`, e.g. \"job^^instance,ip\". "+
|
||||
"Can be combined with -remoteWrite.mdx.enable to hide sensitive label values in VictoriaMetrics self-monitoring metrics. "+
|
||||
"Please see https://docs.victoriametrics.com/victoriametrics/vmagent/#obfuscating-label-values")
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -881,6 +886,8 @@ type remoteWriteCtx struct {
|
||||
pss []*pendingSeries
|
||||
pssNextIdx atomic.Uint64
|
||||
|
||||
obfuscateLabels []string
|
||||
|
||||
rowsPushedAfterRelabel *metrics.Counter
|
||||
rowsDroppedByRelabel *metrics.Counter
|
||||
mdxRowsPreserved *metrics.Counter
|
||||
@@ -995,6 +1002,7 @@ func newRemoteWriteCtx(argIdx int, remoteWriteURL *url.URL, sanitizedURL string)
|
||||
rowsDroppedOnPushFailure: metrics.GetOrCreateCounter(fmt.Sprintf(`vmagent_remotewrite_samples_dropped_total{path=%q,url=%q}`, queuePath, sanitizedURL)),
|
||||
}
|
||||
rwctx.initStreamAggrConfig()
|
||||
rwctx.initObfuscateLabels()
|
||||
|
||||
if enableMdx.GetOptionalArg(argIdx) {
|
||||
mdxFilter := mdx.NewFilter()
|
||||
@@ -1198,24 +1206,41 @@ func (rwctx *remoteWriteCtx) tryPushMetadataInternal(mms []prompb.MetricMetadata
|
||||
func (rwctx *remoteWriteCtx) tryPushTimeSeriesInternal(tss []prompb.TimeSeries) bool {
|
||||
var rctx *relabelCtx
|
||||
var v *[]prompb.TimeSeries
|
||||
var olctx *obfuscateLabelsCtx
|
||||
defer func() {
|
||||
if rctx == nil {
|
||||
return
|
||||
if v != nil {
|
||||
*v = prompb.ResetTimeSeries(tss)
|
||||
tssPool.Put(v)
|
||||
}
|
||||
if rctx != nil {
|
||||
putRelabelCtx(rctx)
|
||||
}
|
||||
if olctx != nil {
|
||||
putObfuscateLabelsCtx(olctx)
|
||||
}
|
||||
*v = prompb.ResetTimeSeries(tss)
|
||||
tssPool.Put(v)
|
||||
putRelabelCtx(rctx)
|
||||
}()
|
||||
|
||||
copyTimeSeriesIfNeeded := func() {
|
||||
if v == nil {
|
||||
v = tssPool.Get().(*[]prompb.TimeSeries)
|
||||
tss = append(*v, tss...)
|
||||
}
|
||||
}
|
||||
|
||||
if len(labelsGlobal) > 0 {
|
||||
// Make a copy of tss before adding extra labels to prevent
|
||||
// from affecting time series for other remoteWrite.url configs.
|
||||
rctx = getRelabelCtx()
|
||||
v = tssPool.Get().(*[]prompb.TimeSeries)
|
||||
tss = append(*v, tss...)
|
||||
copyTimeSeriesIfNeeded()
|
||||
rctx.appendExtraLabels(tss, labelsGlobal)
|
||||
}
|
||||
|
||||
if len(rwctx.obfuscateLabels) != 0 {
|
||||
copyTimeSeriesIfNeeded()
|
||||
olctx = getObfuscateLabelsCtx()
|
||||
tss = olctx.obfuscate(tss, rwctx.obfuscateLabels)
|
||||
}
|
||||
|
||||
pss := rwctx.pss
|
||||
idx := rwctx.pssNextIdx.Add(1) % uint64(len(pss))
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/netutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
||||
@@ -267,7 +268,11 @@ func (c *Client) do(req *http.Request) (*http.Response, error) {
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
return nil, fmt.Errorf("unexpected response code %d for %s. Response body %s", resp.StatusCode, ru, body)
|
||||
err = &httpserver.ErrorWithStatusCode{
|
||||
StatusCode: resp.StatusCode,
|
||||
Err: fmt.Errorf("unexpected response code %d for %s. Response body %s", resp.StatusCode, ru, body),
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ type Alert struct {
|
||||
State AlertState
|
||||
// Expr contains expression that was executed to generate the Alert
|
||||
Expr string
|
||||
// Interval contains the evaluation interval of the Alert's group
|
||||
Interval time.Duration
|
||||
// ActiveAt defines the moment of time when Alert has become active
|
||||
ActiveAt time.Time
|
||||
// Start defines the moment of time when Alert has become firing
|
||||
@@ -84,6 +86,7 @@ type AlertTplData struct {
|
||||
Labels map[string]string
|
||||
Value float64
|
||||
Expr string
|
||||
Interval time.Duration
|
||||
AlertID uint64
|
||||
GroupID uint64
|
||||
ActiveAt time.Time
|
||||
@@ -96,6 +99,7 @@ var tplHeaders = []string{
|
||||
"{{ $type := .Type }}",
|
||||
"{{ $labels := .Labels }}",
|
||||
"{{ $expr := .Expr }}",
|
||||
"{{ $interval := .Interval }}",
|
||||
"{{ $externalLabels := .ExternalLabels }}",
|
||||
"{{ $externalURL := .ExternalURL }}",
|
||||
"{{ $alertID := .AlertID }}",
|
||||
@@ -115,6 +119,7 @@ func (a *Alert) ExecTemplate(q templates.QueryFn, labels, annotations map[string
|
||||
Type: a.Type,
|
||||
Labels: labels,
|
||||
Expr: a.Expr,
|
||||
Interval: a.Interval,
|
||||
AlertID: a.ID,
|
||||
GroupID: a.GroupID,
|
||||
ActiveAt: a.ActiveAt,
|
||||
|
||||
@@ -129,6 +129,17 @@ func TestAlertExecTemplate(t *testing.T) {
|
||||
"exprEscapedHTML": "vm_rows{"label"="bar"}<0",
|
||||
})
|
||||
|
||||
// interval-template
|
||||
f(&Alert{
|
||||
Interval: 10 * time.Second,
|
||||
}, map[string]string{
|
||||
"interval": "{{ .Interval }}",
|
||||
"intervalVariable": "{{ $interval }}",
|
||||
}, map[string]string{
|
||||
"interval": "10s",
|
||||
"intervalVariable": "10s",
|
||||
})
|
||||
|
||||
// query
|
||||
f(&Alert{
|
||||
Expr: `vm_rows{"label"="bar"}>0`,
|
||||
|
||||
@@ -25,11 +25,12 @@ var (
|
||||
replayMaxDatapoints = flag.Int("replay.maxDatapointsPerQuery", 1e3,
|
||||
"Max number of data points expected in one request. It affects the max time range for every '/query_range' request during the replay. The higher the value, the less requests will be made during replay.")
|
||||
replayRuleRetryAttempts = flag.Int("replay.ruleRetryAttempts", 5,
|
||||
"Defines how many retries to make before giving up on rule if request for it returns an error.")
|
||||
"Defines how many retries to make before giving up on rule if request for it returns a retriable error.")
|
||||
disableProgressBar = flag.Bool("replay.disableProgressBar", false, "Whether to disable rendering progress bars during the replay. "+
|
||||
"Progress bar rendering might be verbose or break the logs parsing, so it is recommended to be disabled when not used in interactive mode.")
|
||||
ruleEvaluationConcurrency = flag.Int("replay.ruleEvaluationConcurrency", 1, "The maximum number of concurrent '/query_range' requests when replay recording rule or alerting rule with for=0. "+
|
||||
"Increasing this value when replaying for a long time, since each request is limited by -replay.maxDatapointsPerQuery.")
|
||||
continueWithExecutionErr = flag.Bool("replay.continueWithExecutionErr", false, "Whether to continue replaying other rules if a rule execution fails with a 422 response code, which can happen due to an expression syntax error or a resource limit being hit.")
|
||||
)
|
||||
|
||||
func replay(groupsCfg []config.Group, qb datasource.QuerierBuilder, rw remotewrite.RWClient) (totalRows, droppedRows int, err error) {
|
||||
@@ -73,7 +74,7 @@ func replay(groupsCfg []config.Group, qb datasource.QuerierBuilder, rw remotewri
|
||||
|
||||
for _, cfg := range groupsCfg {
|
||||
ng := rule.NewGroup(cfg, qb, *evaluationInterval, labels)
|
||||
totalRows += ng.Replay(tFrom, tTo, rw, *replayMaxDatapoints, *replayRuleRetryAttempts, *replayRulesDelay, *disableProgressBar, *ruleEvaluationConcurrency)
|
||||
totalRows += ng.Replay(tFrom, tTo, rw, *replayMaxDatapoints, *replayRuleRetryAttempts, *replayRulesDelay, *disableProgressBar, *ruleEvaluationConcurrency, *continueWithExecutionErr)
|
||||
}
|
||||
logger.Infof("replay evaluation finished, generated %d samples", totalRows)
|
||||
if err := rw.Close(); err != nil {
|
||||
|
||||
@@ -8,8 +8,10 @@ import (
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutil"
|
||||
"github.com/VictoriaMetrics/metricsql"
|
||||
)
|
||||
|
||||
type fakeReplayQuerier struct {
|
||||
@@ -32,6 +34,14 @@ func (fc *fakeRWClient) Close() error {
|
||||
}
|
||||
|
||||
func (fr *fakeReplayQuerier) QueryRange(_ context.Context, q string, from, to time.Time) (res datasource.Result, err error) {
|
||||
_, err = metricsql.Parse(q)
|
||||
if err != nil {
|
||||
return res, &httpserver.ErrorWithStatusCode{
|
||||
StatusCode: 422,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s+%s", from.Format("15:04:05"), to.Format("15:04:05"))
|
||||
dps, ok := fr.registry[q]
|
||||
if !ok {
|
||||
@@ -275,4 +285,28 @@ func TestReplay(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}, 10)
|
||||
|
||||
// rule with wrong expression won't break the other rule with continueWithExecutionErr
|
||||
continueWithExecutionErrOld := *continueWithExecutionErr
|
||||
defer func() {
|
||||
*continueWithExecutionErr = continueWithExecutionErrOld
|
||||
}()
|
||||
*continueWithExecutionErr = true
|
||||
f("2021-01-01T12:00:00.000Z", "2021-01-01T12:02:30.000Z", 1, 1, time.Millisecond, []config.Group{
|
||||
{Rules: []config.Rule{{Record: "foo", Expr: "sum(up)"}}},
|
||||
{Rules: []config.Rule{{Record: "bar", Expr: "up ++"}}},
|
||||
}, &fakeReplayQuerier{
|
||||
registry: map[string]map[string][]datasource.Metric{
|
||||
"sum(up)": {
|
||||
"12:00:00+12:01:00": {
|
||||
{
|
||||
Timestamps: []int64{1, 2},
|
||||
Values: []float64{1, 2},
|
||||
},
|
||||
},
|
||||
"12:01:00+12:02:00": {},
|
||||
"12:02:00+12:02:30": {},
|
||||
},
|
||||
},
|
||||
}, 2)
|
||||
}
|
||||
|
||||
@@ -530,6 +530,7 @@ func (ar *AlertingRule) exec(ctx context.Context, ts time.Time, limit int) ([]pr
|
||||
ar.logDebugf(ts, a, "INACTIVE => PENDING")
|
||||
}
|
||||
a.Value = m.Values[0]
|
||||
a.Interval = ar.EvalInterval
|
||||
a.Annotations = annotations
|
||||
a.KeepFiringSince = time.Time{}
|
||||
continue
|
||||
@@ -612,6 +613,7 @@ func (ar *AlertingRule) expandAnnotationTemplates(m datasource.Metric, qFn templ
|
||||
Type: ar.Type.String(),
|
||||
Labels: ls.origin,
|
||||
Expr: ar.Expr,
|
||||
Interval: ar.EvalInterval,
|
||||
AlertID: hash(ls.processed),
|
||||
GroupID: ar.GroupID,
|
||||
ActiveAt: activeAt,
|
||||
@@ -673,6 +675,7 @@ func (ar *AlertingRule) newAlert(m datasource.Metric, start time.Time, labels, a
|
||||
Name: ar.Name,
|
||||
Type: ar.Type.String(),
|
||||
Expr: ar.Expr,
|
||||
Interval: ar.EvalInterval,
|
||||
For: ar.For,
|
||||
ActiveAt: start,
|
||||
Value: m.Values[0],
|
||||
|
||||
@@ -662,6 +662,7 @@ func TestAlertingRuleExecRange(t *testing.T) {
|
||||
GroupID: fakeGroup.GetID(),
|
||||
Name: "for-pending",
|
||||
Type: config.NewPrometheusType().String(),
|
||||
Interval: time.Second,
|
||||
Labels: map[string]string{"alertname": "for-pending"},
|
||||
Annotations: map[string]string{},
|
||||
State: notifier.StatePending,
|
||||
@@ -682,6 +683,7 @@ func TestAlertingRuleExecRange(t *testing.T) {
|
||||
GroupID: fakeGroup.GetID(),
|
||||
Name: "for-firing",
|
||||
Type: config.NewPrometheusType().String(),
|
||||
Interval: 3 * time.Second,
|
||||
Labels: map[string]string{"alertname": "for-firing"},
|
||||
Annotations: map[string]string{},
|
||||
State: notifier.StateFiring,
|
||||
@@ -703,6 +705,7 @@ func TestAlertingRuleExecRange(t *testing.T) {
|
||||
GroupID: fakeGroup.GetID(),
|
||||
Name: "for-hold-pending",
|
||||
Type: config.NewPrometheusType().String(),
|
||||
Interval: time.Second,
|
||||
Labels: map[string]string{"alertname": "for-hold-pending"},
|
||||
Annotations: map[string]string{},
|
||||
State: notifier.StatePending,
|
||||
@@ -759,6 +762,7 @@ func TestAlertingRuleExecRange(t *testing.T) {
|
||||
GroupID: fakeGroup.GetID(),
|
||||
Name: "multi-series",
|
||||
Type: config.NewPrometheusType().String(),
|
||||
Interval: 3 * time.Second,
|
||||
Labels: map[string]string{"alertname": "multi-series"},
|
||||
Annotations: map[string]string{},
|
||||
State: notifier.StateFiring,
|
||||
@@ -771,6 +775,7 @@ func TestAlertingRuleExecRange(t *testing.T) {
|
||||
GroupID: fakeGroup.GetID(),
|
||||
Name: "multi-series",
|
||||
Type: config.NewPrometheusType().String(),
|
||||
Interval: 3 * time.Second,
|
||||
Labels: map[string]string{"alertname": "multi-series", "foo": "bar"},
|
||||
Annotations: map[string]string{},
|
||||
State: notifier.StatePending,
|
||||
@@ -1134,7 +1139,8 @@ func TestAlertingRule_Template(t *testing.T) {
|
||||
fq.Add(metrics...)
|
||||
fq.SetPartialResponse(isResponsePartial)
|
||||
|
||||
if _, err := rule.exec(context.TODO(), time.Now(), 0); err != nil {
|
||||
ts := time.Unix(3600, 0)
|
||||
if _, err := rule.exec(context.TODO(), ts, 0); err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
for hash, expAlert := range alertsExpected {
|
||||
@@ -1152,12 +1158,14 @@ func TestAlertingRule_Template(t *testing.T) {
|
||||
}
|
||||
|
||||
f(&AlertingRule{
|
||||
Name: "common",
|
||||
Name: "common",
|
||||
EvalInterval: time.Hour,
|
||||
Labels: map[string]string{
|
||||
"region": "east",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"summary": `{{ $labels.alertname }}: Too high connection number for "{{ $labels.instance }}"`,
|
||||
"summary": `{{ $labels.alertname }}: Too high connection number for "{{ $labels.instance }}"`,
|
||||
"dashboard": `&from={{ ($activeAt.Add (parseDurationTime (printf "-%s" .Interval))).UnixMilli }}&to={{ $activeAt.UnixMilli }}`,
|
||||
},
|
||||
alerts: make(map[uint64]*notifier.Alert),
|
||||
}, []datasource.Metric{
|
||||
@@ -1166,7 +1174,8 @@ func TestAlertingRule_Template(t *testing.T) {
|
||||
}, false, map[uint64]*notifier.Alert{
|
||||
hash(map[string]string{alertNameLabel: "common", "region": "east", "instance": "foo"}): {
|
||||
Annotations: map[string]string{
|
||||
"summary": `common: Too high connection number for "foo"`,
|
||||
"summary": `common: Too high connection number for "foo"`,
|
||||
"dashboard": "&from=0&to=3600000",
|
||||
},
|
||||
Labels: map[string]string{
|
||||
alertNameLabel: "common",
|
||||
@@ -1176,7 +1185,8 @@ func TestAlertingRule_Template(t *testing.T) {
|
||||
},
|
||||
hash(map[string]string{alertNameLabel: "common", "region": "east", "instance": "bar"}): {
|
||||
Annotations: map[string]string{
|
||||
"summary": `common: Too high connection number for "bar"`,
|
||||
"summary": `common: Too high connection number for "bar"`,
|
||||
"dashboard": "&from=0&to=3600000",
|
||||
},
|
||||
Labels: map[string]string{
|
||||
alertNameLabel: "common",
|
||||
@@ -1388,7 +1398,7 @@ func TestAlertingRule_ToLabels(t *testing.T) {
|
||||
"alertname": "ConfigurationReloadFailure",
|
||||
"alertgroup": "vmalert",
|
||||
"pod": "vmalert-0",
|
||||
"invalid_label": `error evaluating template: template: :1:298: executing "" at <.Values.mustRuntimeFail>: can't evaluate field Values in type notifier.tplData`,
|
||||
"invalid_label": `error evaluating template: template: :1:326: executing "" at <.Values.mustRuntimeFail>: can't evaluate field Values in type notifier.tplData`,
|
||||
}
|
||||
|
||||
expectedProcessedLabels := map[string]string{
|
||||
@@ -1398,7 +1408,7 @@ func TestAlertingRule_ToLabels(t *testing.T) {
|
||||
"exported_alertname": "ConfigurationReloadFailure",
|
||||
"group": "vmalert",
|
||||
"alertgroup": "vmalert",
|
||||
"invalid_label": `error evaluating template: template: :1:298: executing "" at <.Values.mustRuntimeFail>: can't evaluate field Values in type notifier.tplData`,
|
||||
"invalid_label": `error evaluating template: template: :1:326: executing "" at <.Values.mustRuntimeFail>: can't evaluate field Values in type notifier.tplData`,
|
||||
}
|
||||
|
||||
ls, err := ar.toLabels(metric, nil)
|
||||
|
||||
@@ -290,6 +290,8 @@ func (g *Group) updateWith(newGroup *Group) error {
|
||||
g.Headers = newGroup.Headers
|
||||
g.NotifierHeaders = newGroup.NotifierHeaders
|
||||
g.Labels = newGroup.Labels
|
||||
g.EvalDelay = newGroup.EvalDelay
|
||||
g.evalAlignment = newGroup.evalAlignment
|
||||
g.Limit = newGroup.Limit
|
||||
g.checksum = newGroup.checksum
|
||||
g.Rules = newRules
|
||||
@@ -548,7 +550,7 @@ func (g *Group) infof(format string, args ...any) {
|
||||
}
|
||||
|
||||
// Replay performs group replay
|
||||
func (g *Group) Replay(start, end time.Time, rw remotewrite.RWClient, maxDataPoint, replayRuleRetryAttempts int, replayDelay time.Duration, disableProgressBar bool, ruleEvaluationConcurrency int) int {
|
||||
func (g *Group) Replay(start, end time.Time, rw remotewrite.RWClient, maxDataPoint, replayRuleRetryAttempts int, replayDelay time.Duration, disableProgressBar bool, ruleEvaluationConcurrency int, continueWithExecutionErr bool) int {
|
||||
var total int
|
||||
step := g.Interval * time.Duration(maxDataPoint)
|
||||
ri := rangeIterator{start: start, end: end, step: step}
|
||||
@@ -576,7 +578,7 @@ func (g *Group) Replay(start, end time.Time, rw remotewrite.RWClient, maxDataPoi
|
||||
if !disableProgressBar {
|
||||
bar = pb.StartNew(iterations)
|
||||
}
|
||||
total += replayRuleRange(rule, ri, bar, rw, replayRuleRetryAttempts, ruleEvaluationConcurrency)
|
||||
total += replayRuleRange(rule, ri, bar, rw, replayRuleRetryAttempts, ruleEvaluationConcurrency, continueWithExecutionErr)
|
||||
if bar != nil {
|
||||
bar.Finish()
|
||||
}
|
||||
@@ -598,7 +600,7 @@ func (g *Group) Replay(start, end time.Time, rw remotewrite.RWClient, maxDataPoi
|
||||
rule := g.Rules[i]
|
||||
sem <- struct{}{}
|
||||
wg.Go(func() {
|
||||
res <- replayRuleRange(rule, ri, bar, rw, replayRuleRetryAttempts, ruleEvaluationConcurrency)
|
||||
res <- replayRuleRange(rule, ri, bar, rw, replayRuleRetryAttempts, ruleEvaluationConcurrency, continueWithExecutionErr)
|
||||
<-sem
|
||||
})
|
||||
}
|
||||
@@ -618,7 +620,7 @@ func (g *Group) Replay(start, end time.Time, rw remotewrite.RWClient, maxDataPoi
|
||||
return total
|
||||
}
|
||||
|
||||
func replayRuleRange(r Rule, ri rangeIterator, bar *pb.ProgressBar, rw remotewrite.RWClient, replayRuleRetryAttempts, ruleEvaluationConcurrency int) int {
|
||||
func replayRuleRange(r Rule, ri rangeIterator, bar *pb.ProgressBar, rw remotewrite.RWClient, replayRuleRetryAttempts, ruleEvaluationConcurrency int, continueWithExecutionErr bool) int {
|
||||
fmt.Printf("> Rule %q (ID: %d)\n", r, r.ID())
|
||||
// alerting rule with for>0 can't be replayed concurrently, since the status change might depend on the previous evaluation
|
||||
// see https://github.com/VictoriaMetrics/VictoriaMetrics/commit/abcb21aa5ee918ba9a4e9cde495dba06e1e9564c
|
||||
@@ -633,7 +635,7 @@ func replayRuleRange(r Rule, ri rangeIterator, bar *pb.ProgressBar, rw remotewri
|
||||
start := ri.s
|
||||
end := ri.e
|
||||
wg.Go(func() {
|
||||
n, err := replayRule(r, start, end, rw, replayRuleRetryAttempts)
|
||||
n, err := replayRule(r, start, end, rw, replayRuleRetryAttempts, continueWithExecutionErr)
|
||||
if err != nil {
|
||||
logger.Fatalf("rule %q: %s", r, err)
|
||||
}
|
||||
|
||||
@@ -78,6 +78,12 @@ func TestUpdateWith(t *testing.T) {
|
||||
if g.Debug != expect.Debug {
|
||||
t.Fatalf("expected to have debug %v; got %v", expect.Debug, g.Debug)
|
||||
}
|
||||
if !durationPtrEqual(g.EvalDelay, expect.EvalDelay) {
|
||||
t.Fatalf("expected to have eval_delay %v; got %v", expect.EvalDelay, g.EvalDelay)
|
||||
}
|
||||
if !boolPtrEqual(g.evalAlignment, expect.evalAlignment) {
|
||||
t.Fatalf("expected to have eval_alignment %v; got %v", expect.evalAlignment, g.evalAlignment)
|
||||
}
|
||||
}
|
||||
|
||||
// new rule
|
||||
@@ -237,6 +243,37 @@ func TestUpdateWith(t *testing.T) {
|
||||
{Alert: "foo1", Debug: &debug},
|
||||
},
|
||||
})
|
||||
|
||||
// update group evaluation settings
|
||||
evalDelay := promutil.NewDuration(time.Minute)
|
||||
evalAlignment := false
|
||||
f(config.Group{
|
||||
Rules: []config.Rule{{
|
||||
Record: "foo",
|
||||
Expr: "max(up)",
|
||||
}},
|
||||
}, config.Group{
|
||||
EvalDelay: evalDelay,
|
||||
EvalAlignment: &evalAlignment,
|
||||
Rules: []config.Rule{{
|
||||
Record: "foo",
|
||||
Expr: "min(up)",
|
||||
}},
|
||||
})
|
||||
}
|
||||
|
||||
func durationPtrEqual(a, b *time.Duration) bool {
|
||||
if a == nil || b == nil {
|
||||
return a == b
|
||||
}
|
||||
return *a == *b
|
||||
}
|
||||
|
||||
func boolPtrEqual(a, b *bool) bool {
|
||||
if a == nil || b == nil {
|
||||
return a == b
|
||||
}
|
||||
return *a == *b
|
||||
}
|
||||
|
||||
func TestUpdateDuringRandSleep(t *testing.T) {
|
||||
|
||||
@@ -4,12 +4,14 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/remotewrite"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
|
||||
)
|
||||
@@ -118,7 +120,7 @@ func (s *ruleState) add(e StateEntry) {
|
||||
s.entries[s.cur] = e
|
||||
}
|
||||
|
||||
func replayRule(r Rule, start, end time.Time, rw remotewrite.RWClient, replayRuleRetryAttempts int) (int, error) {
|
||||
func replayRule(r Rule, start, end time.Time, rw remotewrite.RWClient, replayRuleRetryAttempts int, continueWithExecutionErr bool) (int, error) {
|
||||
var err error
|
||||
var tss []prompb.TimeSeries
|
||||
for i := range replayRuleRetryAttempts {
|
||||
@@ -126,6 +128,21 @@ func replayRule(r Rule, start, end time.Time, rw remotewrite.RWClient, replayRul
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
// retry request if possible to tolerate temporary network or datasource unavailability issues
|
||||
var esc *httpserver.ErrorWithStatusCode
|
||||
if errors.As(err, &esc) {
|
||||
statusCode := esc.StatusCode
|
||||
// if the status code is 422, it means that the query was executed but failed due to an expression syntax error or a the resource limit being hit,
|
||||
// continue replaying but skip the problematic execution if continueWithExecutionErr is true, otherwise, return the error without retry.
|
||||
if statusCode == http.StatusUnprocessableEntity {
|
||||
if continueWithExecutionErr {
|
||||
logger.Errorf("rule %q: %s", r, err)
|
||||
return 0, nil
|
||||
} else {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.Errorf("attempt %d to execute rule %q failed: %s", i+1, r, err)
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
@@ -8,12 +8,14 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"github.com/golang/snappy"
|
||||
"github.com/prometheus/prometheus/config"
|
||||
"github.com/prometheus/prometheus/model/histogram"
|
||||
"github.com/prometheus/prometheus/prompb"
|
||||
"github.com/prometheus/prometheus/storage/remote"
|
||||
"github.com/prometheus/prometheus/tsdb/chunkenc"
|
||||
@@ -234,9 +236,29 @@ func processResponse(body io.ReadCloser, callback StreamCallback) error {
|
||||
// shouldn't be accounted as an error.
|
||||
for _, res := range readResp.Results {
|
||||
for _, ts := range res.Timeseries {
|
||||
vmTs := convertSamples(ts.Samples, ts.Labels)
|
||||
if err := callback(vmTs); err != nil {
|
||||
return err
|
||||
// A series contains either float samples or native histogram samples.
|
||||
// Both fields are processed independently, since a series may switch
|
||||
// from float to native histogram representation at some point in time,
|
||||
// so the requested time range may contain samples of both types.
|
||||
if len(ts.Samples) > 0 {
|
||||
vmTs := convertSamples(ts.Samples, ts.Labels)
|
||||
if err := callback(vmTs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(ts.Histograms) > 0 {
|
||||
hSamples := make([]histogramSample, 0, len(ts.Histograms))
|
||||
for _, h := range ts.Histograms {
|
||||
hSamples = append(hSamples, histogramSample{
|
||||
timestamp: h.Timestamp,
|
||||
fh: h.ToFloatHistogram(),
|
||||
})
|
||||
}
|
||||
for _, vmTs := range convertHistograms(hSamples, ts.Labels) {
|
||||
if err := callback(vmTs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -263,17 +285,45 @@ func processStreamResponse(body io.ReadCloser, callback StreamCallback) error {
|
||||
|
||||
for _, series := range res.ChunkedSeries {
|
||||
samples := make([]prompb.Sample, 0)
|
||||
var hSamples []histogramSample
|
||||
for _, chunk := range series.Chunks {
|
||||
s, err := parseSamples(chunk.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
switch chunk.Type {
|
||||
case prompb.Chunk_XOR, prompb.Chunk_UNKNOWN:
|
||||
// In proto3 the `type` field may be left unset (UNKNOWN) for XOR chunks.
|
||||
// Prometheus remote.proto: "REQUIREMENT: when using proto3, this field
|
||||
// MUST be set when using anything else than XOR". Senders before native
|
||||
// histograms support (Prometheus < 2.40) do not set this field at all,
|
||||
// so UNKNOWN chunks must be parsed as XOR ones.
|
||||
s, err := parseSamples(chunk.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
samples = append(samples, s...)
|
||||
case prompb.Chunk_HISTOGRAM, prompb.Chunk_FLOAT_HISTOGRAM:
|
||||
hs, err := parseHistograms(chunk.Type, chunk.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hSamples = append(hSamples, hs...)
|
||||
default:
|
||||
return fmt.Errorf("unsupported chunk encoding %q", chunk.Type)
|
||||
}
|
||||
samples = append(samples, s...)
|
||||
}
|
||||
|
||||
ts := convertSamples(samples, series.Labels)
|
||||
if err := callback(ts); err != nil {
|
||||
return err
|
||||
// A series contains either XOR chunks or native histogram chunks.
|
||||
// Both are processed independently, since a series may switch
|
||||
// from float to native histogram representation at some point in time,
|
||||
// so the requested time range may contain chunks of both types.
|
||||
if len(samples) > 0 {
|
||||
ts := convertSamples(samples, series.Labels)
|
||||
if err := callback(ts); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, ts := range convertHistograms(hSamples, series.Labels) {
|
||||
if err := callback(ts); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -312,6 +362,151 @@ func parseSamples(chunk []byte) ([]prompb.Sample, error) {
|
||||
return samples, it.Err()
|
||||
}
|
||||
|
||||
// histogramSample represents a single native histogram sample.
|
||||
type histogramSample struct {
|
||||
timestamp int64
|
||||
fh *histogram.FloatHistogram
|
||||
}
|
||||
|
||||
func parseHistograms(encoding prompb.Chunk_Encoding, chunk []byte) ([]histogramSample, error) {
|
||||
var enc chunkenc.Encoding
|
||||
switch encoding {
|
||||
case prompb.Chunk_HISTOGRAM:
|
||||
enc = chunkenc.EncHistogram
|
||||
case prompb.Chunk_FLOAT_HISTOGRAM:
|
||||
enc = chunkenc.EncFloatHistogram
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported histogram chunk encoding %q", encoding)
|
||||
}
|
||||
c, err := chunkenc.FromData(enc, chunk)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error read chunk: %w", err)
|
||||
}
|
||||
|
||||
var hSamples []histogramSample
|
||||
it := c.Iterator(nil)
|
||||
for {
|
||||
typ := it.Next()
|
||||
if typ == chunkenc.ValNone {
|
||||
break
|
||||
}
|
||||
switch typ {
|
||||
case chunkenc.ValHistogram:
|
||||
ts, h := it.AtHistogram(nil)
|
||||
hSamples = append(hSamples, histogramSample{
|
||||
timestamp: ts,
|
||||
fh: h.ToFloat(nil),
|
||||
})
|
||||
case chunkenc.ValFloatHistogram:
|
||||
ts, fh := it.AtFloatHistogram(nil)
|
||||
hSamples = append(hSamples, histogramSample{
|
||||
timestamp: ts,
|
||||
fh: fh,
|
||||
})
|
||||
default:
|
||||
// Skip unsupported values
|
||||
continue
|
||||
}
|
||||
}
|
||||
if err := it.Err(); err != nil {
|
||||
return nil, fmt.Errorf("error iterate over chunks: %w", err)
|
||||
}
|
||||
|
||||
return hSamples, nil
|
||||
}
|
||||
|
||||
// convertHistograms converts native histogram samples into VictoriaMetrics histogram
|
||||
// time series in the same way as VictoriaMetrics converts native histograms
|
||||
// received via Prometheus remote write protocol: every native histogram sample
|
||||
// is converted into `<name>_count` and `<name>_sum` series plus a set of
|
||||
// `<name>_bucket` series with `vmrange` labels containing non-cumulative bucket counts.
|
||||
// The only difference is that for native histograms with custom buckets (NHCB)
|
||||
// bucket bounds are taken from the custom values, while the remote write protocol
|
||||
// parser ignores custom values and estimates the bounds with the exponential formula.
|
||||
// See https://prometheus.io/docs/specs/native_histograms/#data-model
|
||||
func convertHistograms(hSamples []histogramSample, labels []prompb.Label) []*vm.TimeSeries {
|
||||
if len(hSamples) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
labelPairs := make([]vm.LabelPair, 0, len(labels))
|
||||
nameValue := ""
|
||||
for _, label := range labels {
|
||||
if label.Name == "__name__" {
|
||||
nameValue = label.Value
|
||||
continue
|
||||
}
|
||||
labelPairs = append(labelPairs, vm.LabelPair{Name: label.Name, Value: label.Value})
|
||||
}
|
||||
// the metric has no name, skip it in the same way as VictoriaMetrics does
|
||||
// when it receives a native histogram without the metric name via remote write protocol.
|
||||
if nameValue == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
countSeries := &vm.TimeSeries{
|
||||
Name: nameValue + "_count",
|
||||
LabelPairs: labelPairs,
|
||||
}
|
||||
sumSeries := &vm.TimeSeries{
|
||||
Name: nameValue + "_sum",
|
||||
LabelPairs: labelPairs,
|
||||
}
|
||||
bucketSeries := make(map[string]*vm.TimeSeries)
|
||||
// vmranges preserves the order of bucketSeries creation
|
||||
// in order to get deterministic results.
|
||||
var vmranges []string
|
||||
|
||||
for _, hs := range hSamples {
|
||||
fh := hs.fh
|
||||
countSeries.Timestamps = append(countSeries.Timestamps, hs.timestamp)
|
||||
countSeries.Values = append(countSeries.Values, fh.Count)
|
||||
sumSeries.Timestamps = append(sumSeries.Timestamps, hs.timestamp)
|
||||
sumSeries.Values = append(sumSeries.Values, fh.Sum)
|
||||
|
||||
it := fh.AllBucketIterator()
|
||||
for it.Next() {
|
||||
b := it.At()
|
||||
if b.Count <= 0 {
|
||||
continue
|
||||
}
|
||||
vmrange := formatVmrange(b.Lower, b.Upper)
|
||||
s := bucketSeries[vmrange]
|
||||
if s == nil {
|
||||
bucketLabelPairs := make([]vm.LabelPair, len(labelPairs), len(labelPairs)+1)
|
||||
copy(bucketLabelPairs, labelPairs)
|
||||
bucketLabelPairs = append(bucketLabelPairs, vm.LabelPair{Name: "vmrange", Value: vmrange})
|
||||
s = &vm.TimeSeries{
|
||||
Name: nameValue + "_bucket",
|
||||
LabelPairs: bucketLabelPairs,
|
||||
}
|
||||
bucketSeries[vmrange] = s
|
||||
vmranges = append(vmranges, vmrange)
|
||||
}
|
||||
s.Timestamps = append(s.Timestamps, hs.timestamp)
|
||||
s.Values = append(s.Values, b.Count)
|
||||
}
|
||||
}
|
||||
|
||||
tss := make([]*vm.TimeSeries, 0, 2+len(vmranges))
|
||||
tss = append(tss, countSeries, sumSeries)
|
||||
for _, vmrange := range vmranges {
|
||||
tss = append(tss, bucketSeries[vmrange])
|
||||
}
|
||||
return tss
|
||||
}
|
||||
|
||||
// formatVmrange formats the given bucket bounds into `vmrange` label value
|
||||
// in the same way as VictoriaMetrics does for native histograms
|
||||
// received via Prometheus remote write protocol.
|
||||
func formatVmrange(lower, upper float64) string {
|
||||
b := make([]byte, 0, 24)
|
||||
b = strconv.AppendFloat(b, lower, 'e', 3, 64)
|
||||
b = append(b, "..."...)
|
||||
b = strconv.AppendFloat(b, upper, 'e', 3, 64)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
type keyValue struct {
|
||||
key string
|
||||
value string
|
||||
|
||||
334
app/vmctl/remoteread/remoteread_test.go
Normal file
@@ -0,0 +1,334 @@
|
||||
package remoteread
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"github.com/golang/snappy"
|
||||
"github.com/prometheus/prometheus/model/histogram"
|
||||
"github.com/prometheus/prometheus/prompb"
|
||||
"github.com/prometheus/prometheus/storage/remote"
|
||||
"github.com/prometheus/prometheus/tsdb/chunkenc"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/vm"
|
||||
)
|
||||
|
||||
func testHistogram(mul int64) *histogram.Histogram {
|
||||
return &histogram.Histogram{
|
||||
Schema: 0,
|
||||
Count: uint64(10 * mul),
|
||||
Sum: 25.5 * float64(mul),
|
||||
ZeroThreshold: 0.001,
|
||||
ZeroCount: uint64(2 * mul),
|
||||
PositiveSpans: []histogram.Span{{Offset: 0, Length: 2}},
|
||||
PositiveBuckets: []int64{1 * mul, 2 * mul},
|
||||
NegativeSpans: []histogram.Span{{Offset: 0, Length: 1}},
|
||||
NegativeBuckets: []int64{4 * mul},
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertHistograms(t *testing.T) {
|
||||
f := func(hSamples []histogramSample, labels []prompb.Label, expected []*vm.TimeSeries) {
|
||||
t.Helper()
|
||||
|
||||
tss := convertHistograms(hSamples, labels)
|
||||
if !reflect.DeepEqual(tss, expected) {
|
||||
t.Fatalf("unexpected result\ngot:\n%v\nwant:\n%v", tss, expected)
|
||||
}
|
||||
}
|
||||
|
||||
// series without samples
|
||||
f(nil, []prompb.Label{{Name: "__name__", Value: "foo"}}, nil)
|
||||
|
||||
// series without the metric name must be skipped
|
||||
f([]histogramSample{
|
||||
{timestamp: 1000, fh: testHistogram(1).ToFloat(nil)},
|
||||
}, []prompb.Label{{Name: "job", Value: "bar"}}, nil)
|
||||
|
||||
// native histogram must be converted to _count, _sum and _bucket series
|
||||
// in the same way as VictoriaMetrics does for Prometheus remote write protocol
|
||||
labels := []prompb.Label{
|
||||
{Name: "__name__", Value: "request_duration_seconds"},
|
||||
{Name: "job", Value: "bar"},
|
||||
}
|
||||
jobLabel := []vm.LabelPair{{Name: "job", Value: "bar"}}
|
||||
bucketLabels := func(vmrange string) []vm.LabelPair {
|
||||
return []vm.LabelPair{
|
||||
{Name: "job", Value: "bar"},
|
||||
{Name: "vmrange", Value: vmrange},
|
||||
}
|
||||
}
|
||||
f([]histogramSample{
|
||||
{timestamp: 1000, fh: testHistogram(1).ToFloat(nil)},
|
||||
{timestamp: 2000, fh: testHistogram(2).ToFloat(nil)},
|
||||
}, labels, []*vm.TimeSeries{
|
||||
{
|
||||
Name: "request_duration_seconds_count",
|
||||
LabelPairs: jobLabel,
|
||||
Timestamps: []int64{1000, 2000},
|
||||
Values: []float64{10, 20},
|
||||
},
|
||||
{
|
||||
Name: "request_duration_seconds_sum",
|
||||
LabelPairs: jobLabel,
|
||||
Timestamps: []int64{1000, 2000},
|
||||
Values: []float64{25.5, 51},
|
||||
},
|
||||
{
|
||||
Name: "request_duration_seconds_bucket",
|
||||
LabelPairs: bucketLabels("-1.000e+00...-5.000e-01"),
|
||||
Timestamps: []int64{1000, 2000},
|
||||
Values: []float64{4, 8},
|
||||
},
|
||||
{
|
||||
Name: "request_duration_seconds_bucket",
|
||||
LabelPairs: bucketLabels("-1.000e-03...1.000e-03"),
|
||||
Timestamps: []int64{1000, 2000},
|
||||
Values: []float64{2, 4},
|
||||
},
|
||||
{
|
||||
Name: "request_duration_seconds_bucket",
|
||||
LabelPairs: bucketLabels("5.000e-01...1.000e+00"),
|
||||
Timestamps: []int64{1000, 2000},
|
||||
Values: []float64{1, 2},
|
||||
},
|
||||
{
|
||||
Name: "request_duration_seconds_bucket",
|
||||
LabelPairs: bucketLabels("1.000e+00...2.000e+00"),
|
||||
Timestamps: []int64{1000, 2000},
|
||||
Values: []float64{3, 6},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseHistograms(t *testing.T) {
|
||||
c := chunkenc.NewHistogramChunk()
|
||||
app, err := c.Appender()
|
||||
if err != nil {
|
||||
t.Fatalf("cannot create chunk appender: %s", err)
|
||||
}
|
||||
if _, _, _, err := app.AppendHistogram(nil, 0, 1000, testHistogram(1), true); err != nil {
|
||||
t.Fatalf("cannot append histogram: %s", err)
|
||||
}
|
||||
if _, _, _, err := app.AppendHistogram(nil, 0, 2000, testHistogram(2), true); err != nil {
|
||||
t.Fatalf("cannot append histogram: %s", err)
|
||||
}
|
||||
|
||||
hSamples, err := parseHistograms(prompb.Chunk_HISTOGRAM, c.Bytes())
|
||||
if err != nil {
|
||||
t.Fatalf("cannot parse histogram chunk: %s", err)
|
||||
}
|
||||
if len(hSamples) != 2 {
|
||||
t.Fatalf("unexpected number of histogram samples; got %d; want 2", len(hSamples))
|
||||
}
|
||||
for i, expected := range []struct {
|
||||
timestamp int64
|
||||
count float64
|
||||
sum float64
|
||||
}{
|
||||
{timestamp: 1000, count: 10, sum: 25.5},
|
||||
{timestamp: 2000, count: 20, sum: 51},
|
||||
} {
|
||||
if hSamples[i].timestamp != expected.timestamp {
|
||||
t.Fatalf("unexpected timestamp; got %d; want %d", hSamples[i].timestamp, expected.timestamp)
|
||||
}
|
||||
if hSamples[i].fh.Count != expected.count {
|
||||
t.Fatalf("unexpected count; got %f; want %f", hSamples[i].fh.Count, expected.count)
|
||||
}
|
||||
if hSamples[i].fh.Sum != expected.sum {
|
||||
t.Fatalf("unexpected sum; got %f; want %f", hSamples[i].fh.Sum, expected.sum)
|
||||
}
|
||||
}
|
||||
|
||||
// unsupported chunk encoding must return error
|
||||
if _, err := parseHistograms(prompb.Chunk_XOR, c.Bytes()); err == nil {
|
||||
t.Fatalf("expecting non-nil error for unsupported chunk encoding")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessResponse(t *testing.T) {
|
||||
readResp := &prompb.ReadResponse{
|
||||
Results: []*prompb.QueryResult{
|
||||
{
|
||||
Timeseries: []*prompb.TimeSeries{
|
||||
{
|
||||
Labels: []prompb.Label{
|
||||
{Name: "__name__", Value: "cpu_usage"},
|
||||
{Name: "job", Value: "bar"},
|
||||
},
|
||||
Samples: []prompb.Sample{
|
||||
{Timestamp: 1000, Value: 1.5},
|
||||
},
|
||||
},
|
||||
{
|
||||
Labels: []prompb.Label{
|
||||
{Name: "__name__", Value: "request_duration_seconds"},
|
||||
{Name: "job", Value: "bar"},
|
||||
},
|
||||
Histograms: []prompb.Histogram{
|
||||
prompb.FromIntHistogram(1000, testHistogram(1)),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
data, err := proto.Marshal(readResp)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot marshal ReadResponse: %s", err)
|
||||
}
|
||||
compressed := snappy.Encode(nil, data)
|
||||
|
||||
var tss []*vm.TimeSeries
|
||||
err = processResponse(io.NopCloser(bytes.NewReader(compressed)), func(ts *vm.TimeSeries) error {
|
||||
tss = append(tss, ts)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("cannot process response: %s", err)
|
||||
}
|
||||
|
||||
// 1 float series + _count + _sum + 4 buckets
|
||||
if len(tss) != 7 {
|
||||
t.Fatalf("unexpected number of time series; got %d; want 7", len(tss))
|
||||
}
|
||||
if tss[0].Name != "cpu_usage" || !reflect.DeepEqual(tss[0].Values, []float64{1.5}) {
|
||||
t.Fatalf("unexpected float series: %v", tss[0])
|
||||
}
|
||||
if tss[1].Name != "request_duration_seconds_count" || !reflect.DeepEqual(tss[1].Values, []float64{10}) {
|
||||
t.Fatalf("unexpected _count series: %v", tss[1])
|
||||
}
|
||||
if tss[2].Name != "request_duration_seconds_sum" || !reflect.DeepEqual(tss[2].Values, []float64{25.5}) {
|
||||
t.Fatalf("unexpected _sum series: %v", tss[2])
|
||||
}
|
||||
for _, ts := range tss[3:] {
|
||||
if ts.Name != "request_duration_seconds_bucket" {
|
||||
t.Fatalf("unexpected bucket series name %q", ts.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type nopFlusher struct{}
|
||||
|
||||
func (nopFlusher) Flush() {}
|
||||
|
||||
func TestProcessStreamResponse(t *testing.T) {
|
||||
// build a histogram chunk
|
||||
hc := chunkenc.NewHistogramChunk()
|
||||
hApp, err := hc.Appender()
|
||||
if err != nil {
|
||||
t.Fatalf("cannot create histogram chunk appender: %s", err)
|
||||
}
|
||||
if _, _, _, err := hApp.AppendHistogram(nil, 0, 1000, testHistogram(1), true); err != nil {
|
||||
t.Fatalf("cannot append histogram: %s", err)
|
||||
}
|
||||
|
||||
// build a float chunk
|
||||
xc := chunkenc.NewXORChunk()
|
||||
xApp, err := xc.Appender()
|
||||
if err != nil {
|
||||
t.Fatalf("cannot create xor chunk appender: %s", err)
|
||||
}
|
||||
xApp.Append(0, 1000, 1.5)
|
||||
|
||||
res := &prompb.ChunkedReadResponse{
|
||||
ChunkedSeries: []*prompb.ChunkedSeries{
|
||||
{
|
||||
Labels: []prompb.Label{
|
||||
{Name: "__name__", Value: "request_duration_seconds"},
|
||||
{Name: "job", Value: "bar"},
|
||||
},
|
||||
Chunks: []prompb.Chunk{
|
||||
{Type: prompb.Chunk_HISTOGRAM, Data: hc.Bytes()},
|
||||
},
|
||||
},
|
||||
{
|
||||
Labels: []prompb.Label{
|
||||
{Name: "__name__", Value: "cpu_usage"},
|
||||
},
|
||||
Chunks: []prompb.Chunk{
|
||||
{Type: prompb.Chunk_XOR, Data: xc.Bytes()},
|
||||
},
|
||||
},
|
||||
{
|
||||
Labels: []prompb.Label{
|
||||
{Name: "__name__", Value: "memory_usage"},
|
||||
},
|
||||
Chunks: []prompb.Chunk{
|
||||
// the `type` field may be unset for XOR chunks,
|
||||
// such chunks must be parsed as XOR ones
|
||||
{Type: prompb.Chunk_UNKNOWN, Data: xc.Bytes()},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
data, err := proto.Marshal(res)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot marshal ChunkedReadResponse: %s", err)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
cw := remote.NewChunkedWriter(&buf, nopFlusher{})
|
||||
if _, err := cw.Write(data); err != nil {
|
||||
t.Fatalf("cannot write chunked response: %s", err)
|
||||
}
|
||||
|
||||
var tss []*vm.TimeSeries
|
||||
err = processStreamResponse(io.NopCloser(&buf), func(ts *vm.TimeSeries) error {
|
||||
tss = append(tss, ts)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("cannot process stream response: %s", err)
|
||||
}
|
||||
|
||||
// _count + _sum + 4 buckets + 1 float series + 1 float series from UNKNOWN chunk
|
||||
if len(tss) != 8 {
|
||||
t.Fatalf("unexpected number of time series; got %d; want 8", len(tss))
|
||||
}
|
||||
if tss[0].Name != "request_duration_seconds_count" || !reflect.DeepEqual(tss[0].Values, []float64{10}) {
|
||||
t.Fatalf("unexpected _count series: %v", tss[0])
|
||||
}
|
||||
if tss[1].Name != "request_duration_seconds_sum" || !reflect.DeepEqual(tss[1].Values, []float64{25.5}) {
|
||||
t.Fatalf("unexpected _sum series: %v", tss[1])
|
||||
}
|
||||
for _, ts := range tss[2:6] {
|
||||
if ts.Name != "request_duration_seconds_bucket" {
|
||||
t.Fatalf("unexpected bucket series name %q", ts.Name)
|
||||
}
|
||||
}
|
||||
if tss[6].Name != "cpu_usage" || !reflect.DeepEqual(tss[6].Values, []float64{1.5}) {
|
||||
t.Fatalf("unexpected float series: %v", tss[6])
|
||||
}
|
||||
if tss[7].Name != "memory_usage" || !reflect.DeepEqual(tss[7].Values, []float64{1.5}) {
|
||||
t.Fatalf("unexpected float series from UNKNOWN chunk: %v", tss[7])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFloatHistograms(t *testing.T) {
|
||||
c := chunkenc.NewFloatHistogramChunk()
|
||||
app, err := c.Appender()
|
||||
if err != nil {
|
||||
t.Fatalf("cannot create chunk appender: %s", err)
|
||||
}
|
||||
fh := testHistogram(1).ToFloat(nil)
|
||||
if _, _, _, err := app.AppendFloatHistogram(nil, 0, 1000, fh, true); err != nil {
|
||||
t.Fatalf("cannot append float histogram: %s", err)
|
||||
}
|
||||
|
||||
hSamples, err := parseHistograms(prompb.Chunk_FLOAT_HISTOGRAM, c.Bytes())
|
||||
if err != nil {
|
||||
t.Fatalf("cannot parse float histogram chunk: %s", err)
|
||||
}
|
||||
if len(hSamples) != 1 {
|
||||
t.Fatalf("unexpected number of histogram samples; got %d; want 1", len(hSamples))
|
||||
}
|
||||
if hSamples[0].timestamp != 1000 {
|
||||
t.Fatalf("unexpected timestamp; got %d; want 1000", hSamples[0].timestamp)
|
||||
}
|
||||
if hSamples[0].fh.Count != 10 {
|
||||
t.Fatalf("unexpected count; got %f; want 10", hSamples[0].fh.Count)
|
||||
}
|
||||
}
|
||||
@@ -369,6 +369,10 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
}
|
||||
return true
|
||||
case "/tags/delSeries":
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, fmt.Sprintf("Only POST method is allowed. Got %s.", r.Method), http.StatusMethodNotAllowed)
|
||||
return true
|
||||
}
|
||||
if !httpserver.CheckAuthFlag(w, r, deleteAuthKey) {
|
||||
return true
|
||||
}
|
||||
@@ -388,6 +392,10 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
}
|
||||
return true
|
||||
case "/api/v1/admin/tsdb/delete_series":
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, fmt.Sprintf("Only POST method is allowed. Got %s.", r.Method), http.StatusMethodNotAllowed)
|
||||
return true
|
||||
}
|
||||
if !httpserver.CheckAuthFlag(w, r, deleteAuthKey) {
|
||||
return true
|
||||
}
|
||||
|
||||
197
app/vmselect/vmui/assets/index-B1dXK3k7.js
Normal file
@@ -37,7 +37,7 @@
|
||||
<meta property="og:title" content="UI for VictoriaMetrics">
|
||||
<meta property="og:url" content="https://victoriametrics.com/">
|
||||
<meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data">
|
||||
<script type="module" crossorigin src="./assets/index-D5egN2id.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index-B1dXK3k7.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/rolldown-runtime-CNC7AqOf.js">
|
||||
<link rel="modulepreload" crossorigin href="./assets/vendor-DwJYpOdw.js">
|
||||
<link rel="stylesheet" crossorigin href="./assets/vendor-CnsZ1jie.css">
|
||||
|
||||
5
app/vmui/packages/vmui/assets/favicon.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="48" height="48" fill="#020202" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24.5475 0C10.3246.0265251 1.11379 3.06365 4.40623 6.10077c0 0 12.32997 11.23333 16.58217 14.84083.8131.6896 2.1728 1.1936 3.5191 1.2201h.1199c1.3463-.0265 2.706-.5305 3.5191-1.2201 4.2522-3.5942 16.5422-14.84083 16.5422-14.84083C48.0478 3.06365 38.8636.0265251 24.6674 0"/>
|
||||
<path d="M28.1579 27.0159c-.8131.6896-2.1728 1.1936-3.5191 1.2201h-.12c-1.3463-.0265-2.7059-.5305-3.519-1.2201-2.9725-2.5067-13.35639-11.87-17.26201-15.3979v5.4112c0 .5968.22661 1.3793.6265 1.7506C7.00358 21.1936 17.2675 30.5437 20.9731 33.6737c.8132.6896 2.1728 1.1936 3.5191 1.2201h.12c1.3463-.0265 2.7059-.5305 3.519-1.2201 3.679-3.13 13.9429-12.4536 16.6089-14.8939.4132-.3713.6265-1.1538.6265-1.7506V11.618c-3.9323 3.5411-14.3162 12.931-17.2354 15.3979h.0267Z"/>
|
||||
<path d="M28.1579 39.748c-.8131.6897-2.1728 1.1937-3.5191 1.2202h-.12c-1.3463-.0265-2.7059-.5305-3.519-1.2202-2.9725-2.4933-13.35639-11.8567-17.26201-15.3978v5.4111c0 .5969.22661 1.3793.6265 1.7507C7.00358 33.9258 17.2675 43.2759 20.9731 46.4058c.8132.6897 2.1728 1.1937 3.5191 1.2202h.12c1.3463-.0265 2.7059-.5305 3.519-1.2202 3.679-3.1299 13.9429-12.4535 16.6089-14.8938.4132-.3714.6265-1.1538.6265-1.7507v-5.4111c-3.9323 3.5411-14.3162 12.931-17.2354 15.3978h.0267Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -2,9 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<link rel="icon" href="/favicon.svg"/>
|
||||
<link rel="apple-touch-icon" href="/favicon.svg"/>
|
||||
<link rel="mask-icon" href="/favicon.svg" color="#000000">
|
||||
<link id="favicon" rel="icon" href="/assets/favicon.svg" />
|
||||
<link rel="apple-touch-icon" href="/assets/favicon.svg" />
|
||||
<link id="mask-icon" rel="mask-icon" href="/assets/favicon.svg?no-inline" color="#000000">
|
||||
|
||||
<meta name="robots" content="noindex">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5"/>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg width="48" height="48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M24.5475 0C10.3246.0265251 1.11379 3.06365 4.40623 6.10077c0 0 12.32997 11.23333 16.58217 14.84083.8131.6896 2.1728 1.1936 3.5191 1.2201h.1199c1.3463-.0265 2.706-.5305 3.5191-1.2201 4.2522-3.5942 16.5422-14.84083 16.5422-14.84083C48.0478 3.06365 38.8636.0265251 24.6674 0" fill="#020202"/><path d="M28.1579 27.0159c-.8131.6896-2.1728 1.1936-3.5191 1.2201h-.12c-1.3463-.0265-2.7059-.5305-3.519-1.2201-2.9725-2.5067-13.35639-11.87-17.26201-15.3979v5.4112c0 .5968.22661 1.3793.6265 1.7506C7.00358 21.1936 17.2675 30.5437 20.9731 33.6737c.8132.6896 2.1728 1.1936 3.5191 1.2201h.12c1.3463-.0265 2.7059-.5305 3.519-1.2201 3.679-3.13 13.9429-12.4536 16.6089-14.8939.4132-.3713.6265-1.1538.6265-1.7506V11.618c-3.9323 3.5411-14.3162 12.931-17.2354 15.3979h.0267Z" fill="#020202"/><path d="M28.1579 39.748c-.8131.6897-2.1728 1.1937-3.5191 1.2202h-.12c-1.3463-.0265-2.7059-.5305-3.519-1.2202-2.9725-2.4933-13.35639-11.8567-17.26201-15.3978v5.4111c0 .5969.22661 1.3793.6265 1.7507C7.00358 33.9258 17.2675 43.2759 20.9731 46.4058c.8132.6897 2.1728 1.1937 3.5191 1.2202h.12c1.3463-.0265 2.7059-.5305 3.519-1.2202 3.679-3.1299 13.9429-12.4535 16.6089-14.8938.4132-.3714.6265-1.1538.6265-1.7507v-5.4111c-3.9323 3.5411-14.3162 12.931-17.2354 15.3978h.0267Z" fill="#020202"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -3,7 +3,7 @@
|
||||
"name": "vmui",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.svg",
|
||||
"src": "./assets/favicon.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { FC, useMemo } from "preact/compat";
|
||||
import "./style.scss";
|
||||
import { createFaviconUrl } from "../../../../utils/favicon";
|
||||
import classNames from "classnames";
|
||||
import { useBrowserTabSync } from "./hooks/useBrowserTabSync";
|
||||
import { CloseIcon } from "../../../Main/Icons";
|
||||
import { faviconColors } from "../../../../constants/faviconColors";
|
||||
|
||||
const BrowserTabController: FC = () => {
|
||||
const { faviconColor, changeFaviconColor } = useBrowserTabSync();
|
||||
|
||||
const faviconUrl = useMemo(() => {
|
||||
return createFaviconUrl(faviconColor);
|
||||
}, [faviconColor]);
|
||||
|
||||
const createHandlerClick = (color?: string) => () => {
|
||||
changeFaviconColor(color);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="vm-browser-tab-controller">
|
||||
<p className="vm-server-configurator__title">Favicon color</p>
|
||||
|
||||
<div className="vm-browser-tab-controller-palette">
|
||||
<div className="vm-browser-tab-controller-palette-list">
|
||||
<button
|
||||
className="vm-browser-tab-controller-palette-list__item vm-browser-tab-controller-palette-list__item_reset"
|
||||
type="button"
|
||||
onClick={createHandlerClick()}
|
||||
aria-label="Reset favicon color"
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
|
||||
{faviconColors.map(color => (
|
||||
<button
|
||||
className={classNames({
|
||||
"vm-browser-tab-controller-palette-list__item": true,
|
||||
"vm-browser-tab-controller-palette-list__item_selected": faviconColor === color
|
||||
})}
|
||||
key={color}
|
||||
type="button"
|
||||
style={{ color }}
|
||||
onClick={createHandlerClick(color)}
|
||||
aria-label={`Set favicon color to ${color}`}
|
||||
aria-pressed={faviconColor === color}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<img
|
||||
className="vm-browser-tab-controller-palette__preview"
|
||||
src={faviconUrl}
|
||||
alt="Favicon preview"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrowserTabController;
|
||||
@@ -0,0 +1,41 @@
|
||||
import useEventListener from "../../../../../hooks/useEventListener";
|
||||
import { useEffect, useState } from "preact/compat";
|
||||
import { getFromStorage, removeFromStorage, saveToStorage } from "../../../../../utils/storage";
|
||||
import { getFaviconStorageKey, updateFaviconColor } from "../../../../../utils/favicon";
|
||||
|
||||
const storageKey = `FAVICON_COLOR:${getFaviconStorageKey()}` as const;
|
||||
|
||||
const getColorFromStorage = () => {
|
||||
return getFromStorage(storageKey) as string | undefined;
|
||||
};
|
||||
|
||||
export const useBrowserTabSync = () => {
|
||||
const [faviconColor, setFaviconColor] = useState(getColorFromStorage);
|
||||
|
||||
const handleUpdateColor = () => {
|
||||
setFaviconColor(getColorFromStorage());
|
||||
};
|
||||
|
||||
const changeFaviconColor = (color?: string) => {
|
||||
if (color) {
|
||||
saveToStorage(storageKey, color);
|
||||
} else {
|
||||
removeFromStorage([storageKey]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handleUpdateColor();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
updateFaviconColor(faviconColor);
|
||||
}, [faviconColor]);
|
||||
|
||||
useEventListener("storage", handleUpdateColor);
|
||||
|
||||
return {
|
||||
faviconColor,
|
||||
changeFaviconColor,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
$color-item-size: 28px;
|
||||
$outline-width: 2px;
|
||||
$outline-offset: 2px;
|
||||
$outline-space: $outline-width + $outline-offset;
|
||||
|
||||
.vm-browser-tab-controller {
|
||||
.vm-server-configurator__title {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
&-palette {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: calc($padding-large * 2);
|
||||
|
||||
&-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: $padding-small;
|
||||
|
||||
&__item {
|
||||
width: $color-item-size;
|
||||
height: $color-item-size;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 50%;
|
||||
background-color: currentColor;
|
||||
cursor: pointer;
|
||||
transition-property: transform;
|
||||
transition-duration: 0.15s;
|
||||
transition-timing-function: linear;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&_reset {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: $border-divider;
|
||||
color: $color-text-disabled;
|
||||
padding: calc($padding-small / 2);
|
||||
}
|
||||
|
||||
&_selected {
|
||||
width: $color-item-size - 2 * $outline-space;
|
||||
height: $color-item-size - 2 * $outline-space;
|
||||
margin: $outline-space;
|
||||
outline: $outline-width solid currentColor;
|
||||
outline-offset: $outline-offset;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__preview {
|
||||
width: $color-item-size;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,12 @@ import Tooltip from "../../Main/Tooltip/Tooltip";
|
||||
import LimitsConfigurator from "./LimitsConfigurator/LimitsConfigurator";
|
||||
import { getAppModeEnable } from "../../../utils/app-mode";
|
||||
import classNames from "classnames";
|
||||
import Timezones from "./Timezones/Timezones";
|
||||
import TimezonesPicker from "./Timezones/TimezonesPicker";
|
||||
import ThemeControl from "../ThemeControl/ThemeControl";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import useBoolean from "../../../hooks/useBoolean";
|
||||
import BrowserTabController from "./BrowserTabController/BrowserTabController";
|
||||
import LegendCollapseController from "./LegendCollapseController/LegendCollapseController";
|
||||
|
||||
const title = "Settings";
|
||||
|
||||
@@ -26,7 +28,6 @@ const GlobalSettings: FC = () => {
|
||||
|
||||
const serverSettingRef = useRef<ChildComponentHandle>(null);
|
||||
const limitsSettingRef = useRef<ChildComponentHandle>(null);
|
||||
const timezoneSettingRef = useRef<ChildComponentHandle>(null);
|
||||
|
||||
const {
|
||||
value: open,
|
||||
@@ -37,7 +38,6 @@ const GlobalSettings: FC = () => {
|
||||
const handleApply = () => {
|
||||
serverSettingRef.current && serverSettingRef.current.handleApply();
|
||||
limitsSettingRef.current && limitsSettingRef.current.handleApply();
|
||||
timezoneSettingRef.current && timezoneSettingRef.current.handleApply();
|
||||
handleClose();
|
||||
};
|
||||
|
||||
@@ -49,6 +49,10 @@ const GlobalSettings: FC = () => {
|
||||
onClose={handleClose}
|
||||
/>
|
||||
},
|
||||
{
|
||||
show: true,
|
||||
component: <TimezonesPicker/>
|
||||
},
|
||||
{
|
||||
show: true,
|
||||
component: <LimitsConfigurator
|
||||
@@ -58,12 +62,16 @@ const GlobalSettings: FC = () => {
|
||||
},
|
||||
{
|
||||
show: true,
|
||||
component: <Timezones ref={timezoneSettingRef}/>
|
||||
component: <LegendCollapseController/>
|
||||
},
|
||||
{
|
||||
show: !appModeEnable,
|
||||
component: <ThemeControl/>
|
||||
}
|
||||
},
|
||||
{
|
||||
show: true,
|
||||
component: <BrowserTabController/>
|
||||
},
|
||||
].filter(control => control.show);
|
||||
|
||||
return <>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { FC, useEffect, useState } from "preact/compat";
|
||||
import { getFromStorage, saveToStorage } from "../../../../utils/storage";
|
||||
import Switch from "../../../Main/Switch/Switch";
|
||||
import { LEGEND_COLLAPSE_SERIES_LIMIT } from "../../../../constants/graph";
|
||||
import "./style.scss";
|
||||
|
||||
const LegendCollapseController: FC = () => {
|
||||
const storageCollapse = getFromStorage("LEGEND_AUTO_COLLAPSE");
|
||||
const [legendCollapse, setLegendCollapse] = useState(storageCollapse ? storageCollapse === "true" : true);
|
||||
|
||||
useEffect(() => {
|
||||
saveToStorage("LEGEND_AUTO_COLLAPSE", `${legendCollapse}`);
|
||||
}, [legendCollapse]);
|
||||
|
||||
return (
|
||||
<div className="vm-legend-collapse-controller">
|
||||
<Switch
|
||||
fullWidth
|
||||
color="neutral"
|
||||
value={legendCollapse}
|
||||
onChange={setLegendCollapse}
|
||||
label={<span className="vm-server-configurator__title">Auto-collapse legend</span>}
|
||||
/>
|
||||
<span className="vm-legend-collapse-controller__description">
|
||||
Collapses the legend when series count exceeds {LEGEND_COLLAPSE_SERIES_LIMIT} to reduce UI load.
|
||||
</span>
|
||||
</div>);
|
||||
};
|
||||
|
||||
export default LegendCollapseController;
|
||||
@@ -0,0 +1,19 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-legend-collapse-controller {
|
||||
background-color: $color-hover-black;
|
||||
border-radius: $border-radius-medium;
|
||||
padding: $padding-large;
|
||||
border: $border-divider;
|
||||
|
||||
.vm-graph-settings-row__label {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__description {
|
||||
padding-top: $padding-global;
|
||||
font-size: $font-size-small;
|
||||
line-height: 1.3;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,21 @@
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "preact/compat";
|
||||
import { forwardRef, useCallback, useImperativeHandle, useState } from "preact/compat";
|
||||
import { DisplayType, ErrorTypes } from "../../../../types";
|
||||
import TextField from "../../../Main/TextField/TextField";
|
||||
import Tooltip from "../../../Main/Tooltip/Tooltip";
|
||||
import { InfoIcon, RestartIcon } from "../../../Main/Icons";
|
||||
import Button from "../../../Main/Button/Button";
|
||||
import { DEFAULT_MAX_SERIES, LEGEND_COLLAPSE_SERIES_LIMIT } from "../../../../constants/graph";
|
||||
import { DEFAULT_MAX_SERIES } from "../../../../constants/graph";
|
||||
import "./style.scss";
|
||||
import classNames from "classnames";
|
||||
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
|
||||
import { ChildComponentHandle } from "../GlobalSettings";
|
||||
import { useCustomPanelDispatch, useCustomPanelState } from "../../../../state/customPanel/CustomPanelStateContext";
|
||||
import Switch from "../../../Main/Switch/Switch";
|
||||
import { getFromStorage, saveToStorage } from "../../../../utils/storage";
|
||||
|
||||
interface ServerConfiguratorProps {
|
||||
onClose: () => void
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const fields: {label: string, type: DisplayType}[] = [
|
||||
const fields: { label: string, type: DisplayType }[] = [
|
||||
{ label: "Graph", type: DisplayType.chart },
|
||||
{ label: "JSON", type: DisplayType.code },
|
||||
{ label: "Table", type: DisplayType.table }
|
||||
@@ -29,8 +27,7 @@ const LimitsConfigurator = forwardRef<ChildComponentHandle, ServerConfiguratorPr
|
||||
const { seriesLimits } = useCustomPanelState();
|
||||
const customPanelDispatch = useCustomPanelDispatch();
|
||||
|
||||
const storageCollapse = getFromStorage("LEGEND_AUTO_COLLAPSE");
|
||||
const [legendCollapse, setLegendCollapse] = useState(storageCollapse ? storageCollapse === "true" : true);
|
||||
|
||||
|
||||
const [limits, setLimits] = useState(seriesLimits);
|
||||
const [error, setError] = useState({
|
||||
@@ -43,7 +40,7 @@ const LimitsConfigurator = forwardRef<ChildComponentHandle, ServerConfiguratorPr
|
||||
setLimits(DEFAULT_MAX_SERIES);
|
||||
};
|
||||
|
||||
const createChangeHandler = (type: DisplayType) => (val: string) => {
|
||||
const createChangeHandler = (type: DisplayType) => (val: string) => {
|
||||
const value = val || "";
|
||||
setError(prev => ({ ...prev, [type]: +value < 0 ? ErrorTypes.positiveNumber : "" }));
|
||||
setLimits({
|
||||
@@ -57,10 +54,6 @@ const LimitsConfigurator = forwardRef<ChildComponentHandle, ServerConfiguratorPr
|
||||
onClose();
|
||||
}, [limits]);
|
||||
|
||||
useEffect(() => {
|
||||
saveToStorage("LEGEND_AUTO_COLLAPSE", `${legendCollapse}`);
|
||||
}, [legendCollapse]);
|
||||
|
||||
useImperativeHandle(ref, () => ({ handleApply }), [handleApply]);
|
||||
|
||||
return (
|
||||
@@ -106,19 +99,6 @@ const LimitsConfigurator = forwardRef<ChildComponentHandle, ServerConfiguratorPr
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="vm-graph-settings-row">
|
||||
<span className="vm-graph-settings-row__label">Auto-collapse legend</span>
|
||||
<Switch
|
||||
value={legendCollapse}
|
||||
onChange={setLegendCollapse}
|
||||
label={legendCollapse ? "Enabled" : "Disabled"}
|
||||
fullWidth={isMobile}
|
||||
/>
|
||||
<span className="vm-legend-configs-item__info">
|
||||
Collapses the legend when series count exceeds {LEGEND_COLLAPSE_SERIES_LIMIT} to reduce UI load.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
import { FC, forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from "preact/compat";
|
||||
import { getBrowserTimezone, getTimezoneList, getUTCByTimezone } from "../../../../utils/time";
|
||||
import { ArrowDropDownIcon } from "../../../Main/Icons";
|
||||
import classNames from "classnames";
|
||||
import Popper from "../../../Main/Popper/Popper";
|
||||
import Accordion from "../../../Main/Accordion/Accordion";
|
||||
import TextField from "../../../Main/TextField/TextField";
|
||||
import { Timezone } from "../../../../types";
|
||||
import "./style.scss";
|
||||
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
|
||||
import useBoolean from "../../../../hooks/useBoolean";
|
||||
import WarningTimezone from "./WarningTimezone";
|
||||
import { useTimeDispatch, useTimeState } from "../../../../state/time/TimeStateContext";
|
||||
|
||||
interface PinnedTimezone extends Timezone {
|
||||
title: string;
|
||||
isInvalid?: boolean;
|
||||
}
|
||||
|
||||
const browserTimezone = getBrowserTimezone();
|
||||
|
||||
const Timezones: FC = forwardRef((props, ref) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
const timezones = getTimezoneList();
|
||||
|
||||
const { timezone: stateTimezone, defaultTimezone } = useTimeState();
|
||||
const timeDispatch = useTimeDispatch();
|
||||
|
||||
const [timezone, setTimezone] = useState(stateTimezone);
|
||||
const [search, setSearch] = useState("");
|
||||
const targetRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const {
|
||||
value: openList,
|
||||
toggle: toggleOpenList,
|
||||
setFalse: handleCloseList,
|
||||
} = useBoolean(false);
|
||||
|
||||
const pinnedTimezones = useMemo(() => [
|
||||
{
|
||||
title: `Default time (${defaultTimezone})`,
|
||||
region: defaultTimezone,
|
||||
utc: defaultTimezone ? getUTCByTimezone(defaultTimezone) : "UTC"
|
||||
},
|
||||
{
|
||||
title: browserTimezone.title,
|
||||
region: browserTimezone.region,
|
||||
utc: getUTCByTimezone(browserTimezone.region),
|
||||
isInvalid: !browserTimezone.isValid
|
||||
},
|
||||
{
|
||||
title: "UTC (Coordinated Universal Time)",
|
||||
region: "UTC",
|
||||
utc: "UTC"
|
||||
},
|
||||
].filter(t => t.region) as PinnedTimezone[], [defaultTimezone]);
|
||||
|
||||
const searchTimezones = useMemo(() => {
|
||||
if (!search) return timezones;
|
||||
try {
|
||||
return getTimezoneList(search);
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}, [search, timezones]);
|
||||
|
||||
const timezonesGroups = useMemo(() => Object.keys(searchTimezones), [searchTimezones]);
|
||||
|
||||
const activeTimezone = useMemo(() => ({
|
||||
region: timezone,
|
||||
utc: getUTCByTimezone(timezone)
|
||||
}), [timezone]);
|
||||
|
||||
const handleChangeSearch = (val: string) => {
|
||||
setSearch(val);
|
||||
};
|
||||
|
||||
const handleSetTimezone = (val: Timezone) => {
|
||||
setTimezone(val.region);
|
||||
setSearch("");
|
||||
handleCloseList();
|
||||
};
|
||||
|
||||
const createHandlerSetTimezone = (val: Timezone) => () => {
|
||||
handleSetTimezone(val);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTimezone(stateTimezone);
|
||||
}, [stateTimezone]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleApply: () => {
|
||||
timeDispatch({ type: "SET_TIMEZONE", payload: timezone });
|
||||
}
|
||||
}), [timezone]);
|
||||
|
||||
return (
|
||||
<div className="vm-timezones">
|
||||
<div className="vm-server-configurator__title">
|
||||
Time zone
|
||||
</div>
|
||||
<div
|
||||
className="vm-timezones-item vm-timezones-item_selected"
|
||||
onClick={toggleOpenList}
|
||||
ref={targetRef}
|
||||
>
|
||||
<div className="vm-timezones-item__title">{activeTimezone.region}</div>
|
||||
<div className="vm-timezones-item__utc">{activeTimezone.utc}</div>
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-timezones-item__icon": true,
|
||||
"vm-timezones-item__icon_open": openList
|
||||
})}
|
||||
>
|
||||
<ArrowDropDownIcon/>
|
||||
</div>
|
||||
</div>
|
||||
<Popper
|
||||
open={openList}
|
||||
buttonRef={targetRef}
|
||||
placement="bottom-left"
|
||||
onClose={handleCloseList}
|
||||
fullWidth
|
||||
title={isMobile ? "Time zone" : undefined}
|
||||
>
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-timezones-list": true,
|
||||
"vm-timezones-list_mobile": isMobile,
|
||||
})}
|
||||
>
|
||||
<div className="vm-timezones-list-header">
|
||||
<div className="vm-timezones-list-header__search">
|
||||
<TextField
|
||||
autofocus
|
||||
label="Search"
|
||||
value={search}
|
||||
onChange={handleChangeSearch}
|
||||
/>
|
||||
</div>
|
||||
{pinnedTimezones.map((t, i) => t && (
|
||||
<div
|
||||
key={`${i}_${t.region}`}
|
||||
className="vm-timezones-item vm-timezones-list-group-options__item"
|
||||
onClick={createHandlerSetTimezone(t)}
|
||||
>
|
||||
<div className="vm-timezones-item__title">{t.title}{t.isInvalid && <WarningTimezone/>}</div>
|
||||
<div className="vm-timezones-item__utc">{t.utc}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{timezonesGroups.map(t => (
|
||||
<div
|
||||
className="vm-timezones-list-group"
|
||||
key={t}
|
||||
>
|
||||
<Accordion
|
||||
defaultExpanded={true}
|
||||
title={<div className="vm-timezones-list-group__title">{t}</div>}
|
||||
>
|
||||
<div className="vm-timezones-list-group-options">
|
||||
{searchTimezones[t] && searchTimezones[t].map(item => (
|
||||
<div
|
||||
className="vm-timezones-item vm-timezones-list-group-options__item"
|
||||
onClick={createHandlerSetTimezone(item)}
|
||||
key={item.search}
|
||||
>
|
||||
<div className="vm-timezones-item__title">{item.region}</div>
|
||||
<div className="vm-timezones-item__utc">{item.utc}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Accordion>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Popper>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default Timezones;
|
||||
@@ -0,0 +1,129 @@
|
||||
import { FC, useMemo, useState } from "preact/compat";
|
||||
import { getBrowserTimezone, getTimezoneList, getUTCByTimezone } from "../../../../utils/time";
|
||||
import classNames from "classnames";
|
||||
import Accordion from "../../../Main/Accordion/Accordion";
|
||||
import TextField from "../../../Main/TextField/TextField";
|
||||
import { Timezone } from "../../../../types";
|
||||
import "./style.scss";
|
||||
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
|
||||
import WarningTimezone from "./WarningTimezone";
|
||||
import { useTimeState } from "../../../../state/time/TimeStateContext";
|
||||
|
||||
interface PinnedTimezone extends Timezone {
|
||||
title: string;
|
||||
isInvalid?: boolean;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
onChange: (tz: Timezone) => void;
|
||||
}
|
||||
|
||||
const browserTimezone = getBrowserTimezone();
|
||||
|
||||
const TimezonesList: FC<Props> = ({ onChange }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
const { defaultTimezone } = useTimeState();
|
||||
|
||||
const timezones = useMemo(() => getTimezoneList(), []);
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const pinnedTimezones = useMemo(() => [
|
||||
{
|
||||
title: `Default time (${defaultTimezone})`,
|
||||
region: defaultTimezone,
|
||||
utc: defaultTimezone ? getUTCByTimezone(defaultTimezone) : "UTC"
|
||||
},
|
||||
{
|
||||
title: browserTimezone.title,
|
||||
region: browserTimezone.region,
|
||||
utc: getUTCByTimezone(browserTimezone.region),
|
||||
isInvalid: !browserTimezone.isValid
|
||||
},
|
||||
{
|
||||
title: "UTC (Coordinated Universal Time)",
|
||||
region: "UTC",
|
||||
utc: "UTC"
|
||||
},
|
||||
].filter(t => t.region) as PinnedTimezone[], [defaultTimezone]);
|
||||
|
||||
const searchTimezones = useMemo(() => {
|
||||
if (!search) return timezones;
|
||||
try {
|
||||
return getTimezoneList(search);
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}, [search, timezones]);
|
||||
|
||||
const timezonesGroups = useMemo(() => Object.keys(searchTimezones), [searchTimezones]);
|
||||
|
||||
const handleChangeSearch = (val: string) => {
|
||||
setSearch(val);
|
||||
};
|
||||
|
||||
const handleSetTimezone = (tz: Timezone) => {
|
||||
onChange(tz);
|
||||
setSearch("");
|
||||
};
|
||||
|
||||
const createHandlerSetTimezone = (val: Timezone) => () => {
|
||||
handleSetTimezone(val);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-list": true,
|
||||
"vm-timezones-list": true,
|
||||
"vm-timezones-list_mobile": isMobile,
|
||||
})}
|
||||
>
|
||||
<div className="vm-timezones-list-header">
|
||||
<div className="vm-timezones-list-header__search">
|
||||
<TextField
|
||||
label="Search"
|
||||
value={search}
|
||||
onChange={handleChangeSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{pinnedTimezones.map((t, i) => t && (
|
||||
<div
|
||||
key={`${i}_${t.region}`}
|
||||
className="vm-list-item vm-timezones-item vm-timezones-list-group-options__item"
|
||||
onClick={createHandlerSetTimezone(t)}
|
||||
>
|
||||
<div className="vm-timezones-item__title">{t.title}{t.isInvalid && <WarningTimezone/>}</div>
|
||||
<div className="vm-timezones-item__utc">{t.utc}</div>
|
||||
</div>
|
||||
))}
|
||||
{timezonesGroups.map(t => (
|
||||
<div
|
||||
className="vm-timezones-list-group"
|
||||
key={t}
|
||||
>
|
||||
<Accordion
|
||||
defaultExpanded={true}
|
||||
title={<div className="vm-timezones-list-group__title">{t}</div>}
|
||||
>
|
||||
<div className="vm-timezones-list-group-options">
|
||||
{searchTimezones[t] && searchTimezones[t].map(item => (
|
||||
<div
|
||||
className="vm-list-item vm-timezones-item vm-timezones-list-group-options__item"
|
||||
onClick={createHandlerSetTimezone(item)}
|
||||
key={item.search}
|
||||
>
|
||||
<div className="vm-timezones-item__title">{item.region}</div>
|
||||
<div className="vm-timezones-item__utc">{item.utc}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Accordion>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimezonesList;
|
||||
@@ -0,0 +1,71 @@
|
||||
import { FC, useMemo, useRef } from "preact/compat";
|
||||
import { getUTCByTimezone } from "../../../../utils/time";
|
||||
import { ArrowDropDownIcon } from "../../../Main/Icons";
|
||||
import classNames from "classnames";
|
||||
import { Timezone } from "../../../../types";
|
||||
import "./style.scss";
|
||||
import useBoolean from "../../../../hooks/useBoolean";
|
||||
import { useTimeDispatch, useTimeState } from "../../../../state/time/TimeStateContext";
|
||||
import TimezonesList from "./TimezonesList";
|
||||
import Popper from "../../../Main/Popper/Popper";
|
||||
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
|
||||
|
||||
const TimezonesPicker: FC = () => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
const { timezone: stateTimezone } = useTimeState();
|
||||
const timeDispatch = useTimeDispatch();
|
||||
|
||||
const triggerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const {
|
||||
value: isOpenList,
|
||||
toggle: toggleOpenList,
|
||||
setFalse: handleCloseList,
|
||||
} = useBoolean(false);
|
||||
|
||||
const activeTimezone = useMemo(() => ({
|
||||
region: stateTimezone,
|
||||
utc: getUTCByTimezone(stateTimezone)
|
||||
}), [stateTimezone]);
|
||||
|
||||
const handleSetTimezone = (tz: Timezone) => {
|
||||
timeDispatch({ type: "SET_TIMEZONE", payload: tz.region });
|
||||
handleCloseList();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="vm-timezones">
|
||||
<div className="vm-server-configurator__title">
|
||||
Time zone
|
||||
</div>
|
||||
<div
|
||||
className="vm-timezones-item vm-timezones-item_selected"
|
||||
onClick={toggleOpenList}
|
||||
ref={triggerRef}
|
||||
>
|
||||
<div className="vm-timezones-item__title">{activeTimezone.region}</div>
|
||||
<div className="vm-timezones-item__utc">{activeTimezone.utc}</div>
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-timezones-item__icon": true,
|
||||
"vm-timezones-item__icon_open": isOpenList
|
||||
})}
|
||||
>
|
||||
<ArrowDropDownIcon/>
|
||||
</div>
|
||||
</div>
|
||||
<Popper
|
||||
open={isOpenList}
|
||||
buttonRef={triggerRef}
|
||||
placement="bottom-left"
|
||||
onClose={handleCloseList}
|
||||
fullWidth
|
||||
title={isMobile ? "Time zone" : undefined}
|
||||
>
|
||||
<TimezonesList onChange={handleSetTimezone}/>
|
||||
</Popper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimezonesPicker;
|
||||
@@ -16,6 +16,7 @@
|
||||
}
|
||||
|
||||
&__title {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $padding-small;
|
||||
@@ -34,6 +35,7 @@
|
||||
background-color: $color-hover-black;
|
||||
padding: calc($padding-small/2);
|
||||
border-radius: $border-radius-small;
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
@@ -54,9 +56,11 @@
|
||||
}
|
||||
|
||||
&-list {
|
||||
padding-top: 0;
|
||||
max-height: 300px;
|
||||
background-color: $color-background-block;
|
||||
border-radius: $border-radius-medium;
|
||||
font-size: $font-size-small;
|
||||
overflow: auto;
|
||||
|
||||
&_mobile {
|
||||
@@ -72,10 +76,9 @@
|
||||
top: 0;
|
||||
background-color: $color-background-block;
|
||||
z-index: 2;
|
||||
border-bottom: $border-divider;
|
||||
|
||||
&__search {
|
||||
padding: $padding-small;
|
||||
padding: $padding-small $padding-small calc($padding-small / 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +94,7 @@
|
||||
font-weight: bold;
|
||||
color: $color-text-secondary;
|
||||
padding: $padding-small $padding-global;
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
|
||||
&-options {
|
||||
@@ -98,7 +102,7 @@
|
||||
align-items: flex-start;
|
||||
|
||||
&__item {
|
||||
padding: $padding-small $padding-global;
|
||||
padding: calc($padding-small / 2) $padding-global;
|
||||
transition: background-color 200ms ease;
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $padding-large;
|
||||
gap: calc($padding-global * 2);
|
||||
width: 600px;
|
||||
padding-bottom: $padding-medium;
|
||||
padding-inline: $padding-large;
|
||||
|
||||
&_mobile {
|
||||
grid-auto-rows: min-content;
|
||||
@@ -62,6 +62,7 @@
|
||||
justify-content: flex-end;
|
||||
gap: $padding-small;
|
||||
width: 100%;
|
||||
padding-block: $padding-global;
|
||||
}
|
||||
|
||||
&_mobile &-footer {
|
||||
|
||||
@@ -22,12 +22,10 @@ const StepConfigurator: FC = () => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const { customStep: value, isHistogram } = useGraphState();
|
||||
const { period: { step, end, start } } = useTimeState();
|
||||
const { period: { end, start } } = useTimeState();
|
||||
const graphDispatch = useGraphDispatch();
|
||||
const { displayType } = useCustomPanelState();
|
||||
|
||||
const prevDuration = usePrevious(end - start);
|
||||
|
||||
const defaultStep = useMemo(() => {
|
||||
return getStepFromDuration(end - start, isHistogram, displayType);
|
||||
}, [end, start, isHistogram, displayType]);
|
||||
@@ -106,16 +104,14 @@ const StepConfigurator: FC = () => {
|
||||
}, [defaultStep]);
|
||||
|
||||
useEffect(() => {
|
||||
const dur = end - start;
|
||||
if (dur === prevDuration || !prevDuration || value !== prevDefaultStep) return;
|
||||
if (defaultStep) {
|
||||
handleApply(defaultStep);
|
||||
}
|
||||
}, [prevDuration, defaultStep]);
|
||||
if (!prevDefaultStep) return;
|
||||
if (value !== prevDefaultStep) return;
|
||||
if (value === defaultStep) return;
|
||||
|
||||
useEffect(() => {
|
||||
if (step === value || step === defaultStep) handleApply(defaultStep);
|
||||
}, [isHistogram, displayType]);
|
||||
graphDispatch({ type: "SET_CUSTOM_STEP", payload: defaultStep });
|
||||
setCustomStep(defaultStep);
|
||||
setError("");
|
||||
}, [defaultStep, prevDefaultStep, value, graphDispatch]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -5,8 +5,20 @@ import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import classNames from "classnames";
|
||||
import { FC } from "preact/compat";
|
||||
import { useAppDispatch, useAppState } from "../../../state/common/StateContext";
|
||||
import { DarkIcon, LightIcon, SystemIcon } from "../../Main/Icons";
|
||||
|
||||
const themeIcons = {
|
||||
[Theme.system]: <SystemIcon/>,
|
||||
[Theme.light]: <LightIcon/>,
|
||||
[Theme.dark]: <DarkIcon/>,
|
||||
};
|
||||
|
||||
const options = Object.values(Theme).map(value => ({
|
||||
title: value,
|
||||
value,
|
||||
icon: themeIcons[value],
|
||||
}));
|
||||
|
||||
const options = Object.values(Theme).map(value => ({ title: value, value }));
|
||||
const ThemeControl: FC = () => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -25,13 +37,14 @@ const ThemeControl: FC = () => {
|
||||
})}
|
||||
>
|
||||
<div className="vm-server-configurator__title">
|
||||
Theme preferences
|
||||
Theme
|
||||
</div>
|
||||
<div
|
||||
className="vm-theme-control__toggle"
|
||||
key={`${isMobile}`}
|
||||
>
|
||||
<Toggle
|
||||
size="large"
|
||||
options={options}
|
||||
value={theme}
|
||||
onChange={handleClickItem}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
&__toggle {
|
||||
display: inline-flex;
|
||||
min-width: 300px;
|
||||
width: 100%;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
|
||||
@@ -633,3 +633,60 @@ export const DebugIcon = () => (
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SystemIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M18 4C18.7957 4 19.5595 4.3163 20.1221 4.87891C20.6845 5.44148 21 6.20452 21 7V15.5264L21.0069 15.6426C21.0203 15.7579 21.0542 15.8702 21.1065 15.9746L22.1729 18.0996C22.3271 18.4056 22.4009 18.7466 22.3858 19.0889C22.3705 19.431 22.2675 19.7637 22.0869 20.0547C21.9063 20.3457 21.6534 20.5855 21.3535 20.751C21.0555 20.9154 20.7202 21.0003 20.3799 20.999L3.62013 21C3.28002 21.0012 2.94535 20.9153 2.64748 20.751C2.34748 20.5855 2.09477 20.3458 1.91408 20.0547C1.73343 19.7636 1.63047 19.4311 1.61525 19.0889C1.60006 18.7466 1.67297 18.4056 1.82716 18.0996L2.89455 15.9746L2.94045 15.8682C2.9801 15.7589 3.00007 15.6432 3.00002 15.5264V7C3.00002 6.20442 3.3164 5.4415 3.87892 4.87891C4.44146 4.31636 5.20447 4.00007 6.00002 4H18ZM4.62404 16.9873L3.61427 18.999L3.6133 19H20.3877L20.3867 18.999L19.376 16.9873H4.62404ZM6.00002 6C5.7349 6.00007 5.48045 6.1055 5.29298 6.29297C5.10554 6.48049 5.00002 6.73485 5.00002 7V14.9873H19V7C19 6.73478 18.8946 6.48051 18.707 6.29297C18.5195 6.10552 18.2652 6 18 6H6.00002Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const LightIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M12 19C12.5523 19 13 19.4477 13 20V22C13 22.5523 12.5523 23 12 23C11.4477 23 11 22.5523 11 22V20C11 19.4477 11.4477 19 12 19Z"
|
||||
/>
|
||||
<path
|
||||
d="M5.63281 16.9531C6.02334 16.5627 6.65638 16.5626 7.04688 16.9531C7.43717 17.3436 7.43725 17.9767 7.04688 18.3672L5.63672 19.7773C5.24625 20.1676 4.61313 20.1676 4.22266 19.7773C3.8322 19.3869 3.83233 18.7538 4.22266 18.3633L5.63281 16.9531Z"
|
||||
/>
|
||||
<path
|
||||
d="M16.9531 16.9531C17.3436 16.5626 17.9767 16.5626 18.3672 16.9531L19.7773 18.3633C20.1676 18.7538 20.1678 19.3869 19.7773 19.7773C19.3869 20.1677 18.7538 20.1675 18.3633 19.7773L16.9531 18.3672C16.5626 17.9767 16.5627 17.3437 16.9531 16.9531Z"
|
||||
/>
|
||||
<path
|
||||
d="M12 7C14.7614 7 17 9.23858 17 12C17 14.7614 14.7614 17 12 17C9.23858 17 7 14.7614 7 12C7 9.23858 9.23858 7 12 7ZM12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9Z"
|
||||
/>
|
||||
<path
|
||||
d="M4 11C4.55228 11 5 11.4477 5 12C5 12.5523 4.55228 13 4 13H2C1.44772 13 1 12.5523 1 12C1 11.4477 1.44772 11 2 11H4Z"
|
||||
/>
|
||||
<path
|
||||
d="M22 11C22.5523 11 23 11.4477 23 12C23 12.5523 22.5523 13 22 13H20C19.4477 13 19 12.5523 19 12C19 11.4477 19.4477 11 20 11H22Z"
|
||||
/>
|
||||
<path
|
||||
d="M4.22266 4.22266C4.61315 3.83229 5.24623 3.83229 5.63672 4.22266L7.04688 5.63281C7.4372 6.02331 7.43723 6.65639 7.04688 7.04688C6.6564 7.43735 6.02335 7.43724 5.63281 7.04688L4.22266 5.63672C3.83225 5.24618 3.83217 4.61314 4.22266 4.22266Z"
|
||||
/>
|
||||
<path
|
||||
d="M18.3633 4.22266C18.7538 3.83237 19.3869 3.83232 19.7773 4.22266C20.1677 4.61312 20.1676 5.2462 19.7773 5.63672L18.3672 7.04688C17.9767 7.4373 17.3436 7.4373 16.9531 7.04688C16.5627 6.65637 16.5627 6.0233 16.9531 5.63281L18.3633 4.22266Z"
|
||||
/>
|
||||
<path
|
||||
d="M12 1C12.5523 1 13 1.44772 13 2V4C13 4.55228 12.5523 5 12 5C11.4477 5 11 4.55228 11 4V2C11 1.44772 11.4477 1 12 1Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const DarkIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M11.5809 2.01318C12.1851 2.02907 12.6373 2.40742 12.8475 2.84717C13.0627 3.29758 13.0619 3.86893 12.7615 4.34814L12.7606 4.34717C12.1616 5.30581 11.9061 6.43987 12.034 7.56299C12.162 8.68628 12.6672 9.73328 13.4666 10.5327C14.2661 11.332 15.3131 11.8364 16.4363 11.9644C17.5596 12.0922 18.6934 11.836 19.6522 11.2368L19.8348 11.1382C20.2701 10.9405 20.7563 10.9617 21.1512 11.1499C21.6217 11.3742 22.0194 11.8752 21.9832 12.5405C21.8789 14.4693 21.2181 16.3268 20.0809 17.8882C18.9435 19.4496 17.378 20.6484 15.574 21.3394C13.7701 22.0302 11.8042 22.184 9.91485 21.7817C8.02549 21.3794 6.29356 20.4376 4.92754 19.0718C3.56149 17.7059 2.62012 15.9739 2.21758 14.0845C1.81507 12.195 1.96826 10.2294 2.65899 8.42529C3.34975 6.62115 4.5487 5.05596 6.11016 3.91846C7.6716 2.781 9.52882 2.11947 11.4578 2.01514L11.5809 2.01318ZM10.6209 4.12061C9.42038 4.33038 8.27873 4.81214 7.28692 5.53467C6.03798 6.44459 5.07973 7.69705 4.52715 9.14014C3.97456 10.5834 3.85165 12.156 4.17364 13.6675C4.49565 15.179 5.24872 16.5649 6.34161 17.6577C7.43448 18.7505 8.82026 19.5038 10.3318 19.8257C11.8434 20.1475 13.416 20.0239 14.8592 19.4712C16.3024 18.9184 17.5548 17.9597 18.4647 16.7104C19.1869 15.7188 19.6671 14.5776 19.8768 13.3774C18.7333 13.8927 17.4674 14.0949 16.2098 13.9517C14.637 13.7725 13.1709 13.0661 12.0516 11.9468C10.9324 10.8275 10.2258 9.36126 10.0467 7.78857C9.90352 6.53075 10.1054 5.26417 10.6209 4.12061Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { ReactNode } from "react";
|
||||
import { FC, ReactNode } from "preact/compat";
|
||||
import classNames from "classnames";
|
||||
import "./style.scss";
|
||||
import { FC } from "preact/compat";
|
||||
|
||||
interface SwitchProps {
|
||||
value: boolean
|
||||
color?: "primary" | "secondary" | "error"
|
||||
color?: "primary" | "secondary" | "error" | "neutral"
|
||||
disabled?: boolean
|
||||
label?: string | ReactNode
|
||||
fullWidth?: boolean
|
||||
|
||||
@@ -29,6 +29,10 @@ $switch-border-radius: $switch-handle-size + ($switch-padding * 2);
|
||||
background-color: $color-secondary;
|
||||
}
|
||||
|
||||
&_neutral_active &-track {
|
||||
background-color: $color-text;
|
||||
}
|
||||
|
||||
&_primary_active &-track {
|
||||
background-color: $color-primary;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import { FC, useEffect, useRef, useState } from "preact/compat";
|
||||
import { FC, useEffect, useRef, useState, ReactNode } from "preact/compat";
|
||||
import classNames from "classnames";
|
||||
import { ReactNode } from "react";
|
||||
import "./style.scss";
|
||||
|
||||
interface ToggleProps {
|
||||
options: {value: string, title?: string, icon?: ReactNode}[]
|
||||
value: string
|
||||
onChange: (val: string) => void
|
||||
label?: string
|
||||
options: { value: string, title?: string, icon?: ReactNode }[];
|
||||
value: string;
|
||||
onChange: (val: string) => void;
|
||||
label?: string;
|
||||
size?: "medium" | "large";
|
||||
}
|
||||
|
||||
const Toggle: FC<ToggleProps> = ({ options, value, label, onChange }) => {
|
||||
const Toggle: FC<ToggleProps> = ({ options, value, label, size = "medium", onChange }) => {
|
||||
|
||||
const activeRef = useRef<HTMLDivElement>(null);
|
||||
const [position, setPosition] = useState({
|
||||
width: "0px",
|
||||
left: "0px",
|
||||
borderRadius: "0px"
|
||||
});
|
||||
|
||||
const createHandlerChange = (value: string) => () => {
|
||||
@@ -28,35 +27,25 @@ const Toggle: FC<ToggleProps> = ({ options, value, label, onChange }) => {
|
||||
setPosition({
|
||||
width: "0px",
|
||||
left: "0px",
|
||||
borderRadius: "0px"
|
||||
});
|
||||
return;
|
||||
}
|
||||
const index = options.findIndex(o => o.value === value);
|
||||
const { width: widthRect } = activeRef.current.getBoundingClientRect();
|
||||
|
||||
let width = widthRect;
|
||||
let left = index * width;
|
||||
let borderRadius = "0";
|
||||
if (index === 0) borderRadius = "16px 0 0 16px";
|
||||
const width = widthRect;
|
||||
const left = index * width;
|
||||
|
||||
if (index === options.length - 1) {
|
||||
borderRadius = "10px";
|
||||
left -= 1;
|
||||
borderRadius = "0 16px 16px 0";
|
||||
}
|
||||
|
||||
if (index !== 0 && (index !== options.length - 1)) {
|
||||
width += 1;
|
||||
left -= 1;
|
||||
}
|
||||
|
||||
|
||||
setPosition({ width: `${width}px`, left: `${left}px`, borderRadius });
|
||||
setPosition({ width: `${width}px`, left: `${left}px` });
|
||||
}, [activeRef, value, options]);
|
||||
|
||||
return (
|
||||
<div className="vm-toggles">
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-toggles": true,
|
||||
[`vm-toggles_${size}`]: size,
|
||||
})}
|
||||
>
|
||||
{label && (
|
||||
<label className="vm-toggles__label">
|
||||
{label}
|
||||
@@ -66,15 +55,14 @@ const Toggle: FC<ToggleProps> = ({ options, value, label, onChange }) => {
|
||||
className="vm-toggles-group"
|
||||
style={{ gridTemplateColumns: `repeat(${options.length}, 1fr)` }}
|
||||
>
|
||||
{position.borderRadius && <div
|
||||
<div
|
||||
className="vm-toggles-group__highlight"
|
||||
style={position}
|
||||
/>}
|
||||
{options.map((option, i) => (
|
||||
/>
|
||||
{options.map((option) => (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-toggles-group-item": true,
|
||||
"vm-toggles-group-item_first": i === 0,
|
||||
"vm-toggles-group-item_active": option.value === value,
|
||||
"vm-toggles-group-item_icon": option.icon && option.title
|
||||
})}
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
border-radius: $border-radius-small;
|
||||
background: $color-hover-black;
|
||||
|
||||
&-item {
|
||||
position: relative;
|
||||
@@ -27,55 +29,68 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $padding-small;
|
||||
border-right: $border-divider;
|
||||
border-top: $border-divider;
|
||||
border-bottom: $border-divider;
|
||||
font-size: $font-size-small;
|
||||
color: $color-text-secondary;
|
||||
font-weight: bold;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: color 150ms ease-in;
|
||||
transition: opacity 150ms ease-in, color 150ms ease-in;
|
||||
z-index: 2;
|
||||
user-select: none;
|
||||
|
||||
&_first {
|
||||
border-radius: 16px 0 0 16px;
|
||||
border-left: $border-divider
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 16px 16px 0;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
&_icon {
|
||||
grid-template-columns: 14px auto;
|
||||
gap: 4px;
|
||||
gap: calc($padding-small / 2);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $color-primary;
|
||||
&:hover:not(&_active) {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&_active {
|
||||
color: $color-primary;
|
||||
border-color: transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
color: $color-text;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&__highlight {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3px;
|
||||
height: 100%;
|
||||
background-color: rgba($color-primary, 0.08);
|
||||
border: 1px solid $color-primary;
|
||||
transition: left 200ms cubic-bezier(0.280, 0.840, 0.420, 1), border-radius 200ms linear;
|
||||
z-index: 1;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: $color-background-block;
|
||||
border-radius: $border-radius-small;
|
||||
box-shadow: $box-shadow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&_large &-group {
|
||||
border-radius: $border-radius-medium;
|
||||
|
||||
&-item {
|
||||
padding: $padding-global $padding-small;
|
||||
|
||||
&_icon {
|
||||
grid-template-columns: 16px auto;
|
||||
gap: $padding-small;
|
||||
}
|
||||
}
|
||||
|
||||
&__highlight {
|
||||
padding: 4px;
|
||||
border-radius: $border-radius-medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
14
app/vmui/packages/vmui/src/constants/faviconColors.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export const faviconColors = [
|
||||
"#A1A1AA",
|
||||
"#71717A",
|
||||
"#020202",
|
||||
"#E94600",
|
||||
"#FF7A00",
|
||||
"#F2B705",
|
||||
"#84CC16",
|
||||
"#16B86A",
|
||||
"#00AFAF",
|
||||
"#2979FF",
|
||||
"#8B5CF6",
|
||||
"#E83E9A",
|
||||
] as const;
|
||||
@@ -14,6 +14,9 @@ import useFetchDefaultTimezone from "../../hooks/useFetchDefaultTimezone";
|
||||
import useFetchAppConfig from "../../hooks/useFetchAppConfig";
|
||||
import WebStorageCheck from "../../components/WebStorageCheck/WebStorageCheck";
|
||||
import { migrateStorageToPrefixedKeys } from "../../utils/storage";
|
||||
import {
|
||||
useBrowserTabSync
|
||||
} from "../../components/Configurators/GlobalSettings/BrowserTabController/hooks/useBrowserTabSync";
|
||||
|
||||
const MainLayout: FC = () => {
|
||||
const appModeEnable = getAppModeEnable();
|
||||
@@ -21,6 +24,7 @@ const MainLayout: FC = () => {
|
||||
const { pathname } = useLocation();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
useBrowserTabSync();
|
||||
useFetchDashboards();
|
||||
useFetchDefaultTimezone();
|
||||
useFetchAppConfig();
|
||||
|
||||
29
app/vmui/packages/vmui/src/utils/favicon.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import faviconRaw from "../../assets/favicon.svg?raw";
|
||||
|
||||
export const createFaviconUrl = (color = "#020202"): string => {
|
||||
const svgDocument = new DOMParser().parseFromString(faviconRaw, "image/svg+xml");
|
||||
const svg = svgDocument.documentElement;
|
||||
|
||||
if (svg.localName !== "svg") {
|
||||
throw new Error("Invalid favicon SVG");
|
||||
}
|
||||
|
||||
svg.setAttribute("fill", color);
|
||||
|
||||
const serializedSvg = new XMLSerializer().serializeToString(svg);
|
||||
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(serializedSvg)}`;
|
||||
};
|
||||
|
||||
export const updateFaviconColor = (color = "#020202"): void => {
|
||||
const favicon = document.querySelector<HTMLLinkElement>("#favicon");
|
||||
if (favicon) {
|
||||
favicon.href = createFaviconUrl(color);
|
||||
}
|
||||
|
||||
const maskIcon = document.querySelector<HTMLLinkElement>("#mask-icon");
|
||||
if (maskIcon) {
|
||||
maskIcon.setAttribute("color", color);
|
||||
}
|
||||
};
|
||||
|
||||
export const getFaviconStorageKey = () => window.location.pathname.replace(/\/+$/, "") || "/";
|
||||
@@ -17,7 +17,11 @@ export const ALL_STORAGE_KEYS = [
|
||||
"POINTS_SHOW_ALL",
|
||||
] as const;
|
||||
|
||||
export type StorageKeys = (typeof ALL_STORAGE_KEYS)[number];
|
||||
export type FaviconStorageKey = `FAVICON_COLOR:${string}`;
|
||||
|
||||
export type StorageKeys =
|
||||
| (typeof ALL_STORAGE_KEYS)[number]
|
||||
| FaviconStorageKey;
|
||||
|
||||
type PrefixedStorageKeys = `${typeof STORAGE_PREFIX}${StorageKeys}`;
|
||||
|
||||
@@ -58,7 +62,10 @@ export const getFromStorage = (key: StorageKeys, withPrefix = true): undefined |
|
||||
|
||||
export const removeFromStorage = (keys: StorageKeys[], withPrefix = true): void => {
|
||||
const storageKeys = withPrefix ? keys.map(toPrefixedKey) : keys;
|
||||
storageKeys.forEach(k => window.localStorage.removeItem(k));
|
||||
storageKeys.forEach(k => {
|
||||
window.localStorage.removeItem(k);
|
||||
window.dispatchEvent(new StorageEvent("storage", { key: k }));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -205,19 +205,21 @@ export const getUTCByTimezone = (timezone: string) => {
|
||||
};
|
||||
|
||||
export const getTimezoneList = (search = "") => {
|
||||
const regexp = new RegExp(search, "i");
|
||||
const normalizedSearch = search.toLowerCase();
|
||||
|
||||
return supportedTimezones.reduce((acc: {[key: string]: Timezone[]}, region) => {
|
||||
return supportedTimezones.reduce((acc: { [key: string]: Timezone[] }, region) => {
|
||||
const zone = (region.match(/^(.*?)\//) || [])[1] || "unknown";
|
||||
const utc = getUTCByTimezone(region);
|
||||
const utcForSearch = utc.replace(/UTC|0/, "");
|
||||
const utcForSearch = utc.replace(/^UTC/, "");
|
||||
const regionForSearch = region.replace(/[/_]/g, " ");
|
||||
|
||||
const item = {
|
||||
region,
|
||||
utc,
|
||||
search: `${region} ${utc} ${regionForSearch} ${utcForSearch}`
|
||||
};
|
||||
const includeZone = !search || (search && regexp.test(item.search));
|
||||
|
||||
const includeZone = !normalizedSearch || item.search.toLowerCase().includes(normalizedSearch);
|
||||
|
||||
if (includeZone && acc[zone]) {
|
||||
acc[zone].push(item);
|
||||
|
||||
@@ -50,6 +50,13 @@ export default defineConfig(() => {
|
||||
return "vendor";
|
||||
}
|
||||
},
|
||||
assetFileNames: (assetInfo) => {
|
||||
if (assetInfo.names.includes("favicon.svg")) {
|
||||
return "assets/favicon.svg";
|
||||
}
|
||||
|
||||
return "assets/[name]-[hash][extname]";
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -26,33 +26,20 @@ func TestClusterSearchWithDisabledPerDayIndex(t *testing.T) {
|
||||
defer tc.Stop()
|
||||
|
||||
testSearchWithDisabledPerDayIndex(tc, func(name string, disablePerDayIndex bool) apptest.PrometheusWriteQuerier {
|
||||
// Using static ports for vmstorage because random ports may cause
|
||||
// changes in how data is sharded.
|
||||
vmstorage1 := tc.MustStartVmstorage("vmstorage1-"+name, []string{
|
||||
"-storageDataPath=" + tc.Dir() + "/vmstorage1",
|
||||
vmstorage := tc.MustStartVmstorage("vmstorage-"+name, []string{
|
||||
"-storageDataPath=" + tc.Dir() + "/vmstorage",
|
||||
"-retentionPeriod=100y",
|
||||
"-httpListenAddr=127.0.0.1:61001",
|
||||
"-vminsertAddr=127.0.0.1:61002",
|
||||
"-vmselectAddr=127.0.0.1:61003",
|
||||
fmt.Sprintf("-disablePerDayIndex=%t", disablePerDayIndex),
|
||||
})
|
||||
vmstorage2 := tc.MustStartVmstorage("vmstorage2-"+name, []string{
|
||||
"-storageDataPath=" + tc.Dir() + "/vmstorage2",
|
||||
"-retentionPeriod=100y",
|
||||
"-httpListenAddr=127.0.0.1:62001",
|
||||
"-vminsertAddr=127.0.0.1:62002",
|
||||
"-vmselectAddr=127.0.0.1:62003",
|
||||
fmt.Sprintf("-disablePerDayIndex=%t", disablePerDayIndex),
|
||||
})
|
||||
vminsert := tc.MustStartVminsert("vminsert-"+name, []string{
|
||||
"-storageNode=" + vmstorage1.VminsertAddr() + "," + vmstorage2.VminsertAddr(),
|
||||
"-storageNode=" + vmstorage.VminsertAddr(),
|
||||
})
|
||||
vmselect := tc.MustStartVmselect("vmselect"+name, []string{
|
||||
"-storageNode=" + vmstorage1.VmselectAddr() + "," + vmstorage2.VmselectAddr(),
|
||||
"-storageNode=" + vmstorage.VmselectAddr(),
|
||||
"-search.maxStalenessInterval=1m",
|
||||
})
|
||||
return &apptest.Vmcluster{
|
||||
Vmstorages: []*apptest.Vmstorage{vmstorage1, vmstorage2},
|
||||
Vmstorages: []*apptest.Vmstorage{vmstorage},
|
||||
Vminsert: vminsert,
|
||||
Vmselect: vmselect,
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ func (ms *PrometheusMockStorage) Read(_ context.Context, query *prompb.Query, so
|
||||
}
|
||||
|
||||
if !notMatch {
|
||||
q.Timeseries = append(q.Timeseries, &prompb.TimeSeries{Labels: s.Labels, Samples: s.Samples})
|
||||
q.Timeseries = append(q.Timeseries, &prompb.TimeSeries{Labels: s.Labels, Samples: s.Samples, Histograms: s.Histograms})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"github.com/golang/snappy"
|
||||
"github.com/prometheus/prometheus/model/histogram"
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
"github.com/prometheus/prometheus/prompb"
|
||||
"github.com/prometheus/prometheus/storage/remote"
|
||||
@@ -86,10 +87,17 @@ func (rrs *RemoteReadServer) getReadHandler(t *testing.T) http.Handler {
|
||||
samples = append(samples, sample)
|
||||
}
|
||||
}
|
||||
var histograms []prompb.Histogram
|
||||
for _, h := range s.Histograms {
|
||||
if h.Timestamp >= startTs && h.Timestamp < endTs {
|
||||
histograms = append(histograms, h)
|
||||
}
|
||||
}
|
||||
var series prompb.TimeSeries
|
||||
if len(samples) > 0 {
|
||||
if len(samples) > 0 || len(histograms) > 0 {
|
||||
series.Labels = s.Labels
|
||||
series.Samples = samples
|
||||
series.Histograms = histograms
|
||||
}
|
||||
ts[i] = &series
|
||||
}
|
||||
@@ -317,6 +325,37 @@ func generateRemoteReadSamples(idx int, startTime, endTime, numOfSamples int64)
|
||||
return samples
|
||||
}
|
||||
|
||||
// GenerateRemoteReadHistogramSeries generates a remote read series
|
||||
// with native histogram samples within the given time range.
|
||||
func GenerateRemoteReadHistogramSeries(start, end, numOfSamples int64) []*prompb.TimeSeries {
|
||||
timeSeries := &prompb.TimeSeries{
|
||||
Labels: []prompb.Label{
|
||||
{Name: labels.MetricName, Value: "vm_histogram_metric"},
|
||||
{Name: "job", Value: "0"},
|
||||
},
|
||||
}
|
||||
|
||||
delta := (end - start) / numOfSamples
|
||||
mul := int64(0)
|
||||
for t := start; t != end; t += delta {
|
||||
mul++
|
||||
h := &histogram.Histogram{
|
||||
Schema: 0,
|
||||
Count: uint64(10 * mul),
|
||||
Sum: 25.5 * float64(mul),
|
||||
ZeroThreshold: 0.001,
|
||||
ZeroCount: uint64(2 * mul),
|
||||
PositiveSpans: []histogram.Span{{Offset: 0, Length: 2}},
|
||||
PositiveBuckets: []int64{mul, 2 * mul},
|
||||
NegativeSpans: []histogram.Span{{Offset: 0, Length: 1}},
|
||||
NegativeBuckets: []int64{4 * mul},
|
||||
}
|
||||
timeSeries.Histograms = append(timeSeries.Histograms, prompb.FromIntHistogram(t*1000, h))
|
||||
}
|
||||
|
||||
return []*prompb.TimeSeries{timeSeries}
|
||||
}
|
||||
|
||||
func labelsToLabelsProto(ls labels.Labels) []prompb.Label {
|
||||
result := make([]prompb.Label, 0, ls.Len())
|
||||
ls.Range(func(l labels.Label) {
|
||||
|
||||
@@ -75,6 +75,118 @@ func TestClusterVmctlRemoteReadProtocol(t *testing.T) {
|
||||
testRemoteReadProtocol(tc, clusterDst, newRemoteReadServer, vmctlFlags)
|
||||
}
|
||||
|
||||
func TestSingleVmctlRemoteReadNativeHistograms(t *testing.T) {
|
||||
fs.MustRemoveDir(t.Name())
|
||||
|
||||
tc := apptest.NewTestCase(t)
|
||||
defer tc.Stop()
|
||||
|
||||
vmsingleDst := tc.MustStartDefaultVmsingle()
|
||||
vmAddr := fmt.Sprintf("http://%s/", vmsingleDst.HTTPAddr())
|
||||
vmctlFlags := []string{
|
||||
`remote-read`,
|
||||
`--remote-read-filter-time-start=2025-06-11T15:31:10Z`,
|
||||
`--remote-read-filter-time-end=2025-06-11T15:31:20Z`,
|
||||
`--remote-read-step-interval=minute`,
|
||||
`--vm-addr=` + vmAddr,
|
||||
`--disable-progress-bar=true`,
|
||||
}
|
||||
|
||||
testRemoteReadNativeHistograms(tc, vmsingleDst, NewRemoteReadServer, vmctlFlags)
|
||||
}
|
||||
|
||||
func TestSingleVmctlRemoteReadStreamNativeHistograms(t *testing.T) {
|
||||
fs.MustRemoveDir(t.Name())
|
||||
|
||||
tc := apptest.NewTestCase(t)
|
||||
defer tc.Stop()
|
||||
|
||||
vmsingleDst := tc.MustStartDefaultVmsingle()
|
||||
vmAddr := fmt.Sprintf("http://%s/", vmsingleDst.HTTPAddr())
|
||||
vmctlFlags := []string{
|
||||
`remote-read`,
|
||||
`--remote-read-filter-time-start=2025-06-11T15:31:10Z`,
|
||||
`--remote-read-filter-time-end=2025-06-11T15:31:20Z`,
|
||||
`--remote-read-step-interval=minute`,
|
||||
`--vm-addr=` + vmAddr,
|
||||
`--remote-read-use-stream=true`,
|
||||
`--disable-progress-bar=true`,
|
||||
}
|
||||
|
||||
testRemoteReadNativeHistograms(tc, vmsingleDst, NewRemoteReadStreamServer, vmctlFlags)
|
||||
}
|
||||
|
||||
// testRemoteReadNativeHistograms verifies that native histograms are migrated
|
||||
// as _count, _sum and _bucket series with vmrange labels in the same way
|
||||
// as VictoriaMetrics converts native histograms received via Prometheus remote write protocol.
|
||||
func testRemoteReadNativeHistograms(tc *apptest.TestCase, sut apptest.PrometheusWriteQuerier, newRemoteReadServer func(t *testing.T, series []*prompb.TimeSeries) *RemoteReadServer, vmctlFlags []string) {
|
||||
t := tc.T()
|
||||
t.Helper()
|
||||
|
||||
series := GenerateRemoteReadHistogramSeries(1749655870, 1749655880, 2)
|
||||
|
||||
rrs := newRemoteReadServer(t, series)
|
||||
defer rrs.Close()
|
||||
|
||||
vmctlFlags = append(vmctlFlags, `--remote-read-src-addr=`+rrs.HTTPAddr())
|
||||
tc.MustStartVmctl("vmctl", vmctlFlags)
|
||||
|
||||
sut.ForceFlush(t)
|
||||
|
||||
tc.Assert(&apptest.AssertOptions{
|
||||
Retries: 300,
|
||||
Msg: `unexpected native histogram metrics stored on vmsingle via the prometheus protocol`,
|
||||
Got: func() any {
|
||||
got := sut.PrometheusAPIV1Export(t, `{__name__=~".*"}`, apptest.QueryOpts{
|
||||
Start: "2025-06-11T15:31:10Z",
|
||||
End: "2025-06-11T15:32:20Z",
|
||||
})
|
||||
got.Sort()
|
||||
return got.Data.Result
|
||||
},
|
||||
Want: expectedNativeHistogramQueryResult(),
|
||||
CmpOpts: []cmp.Option{
|
||||
cmpopts.IgnoreFields(apptest.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType"),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// expectedNativeHistogramQueryResult returns the series expected to be stored in VictoriaMetrics
|
||||
// after migrating the series generated by GenerateRemoteReadHistogramSeries(1749655870, 1749655880, 2).
|
||||
func expectedNativeHistogramQueryResult() []*apptest.QueryResult {
|
||||
metric := func(name, vmrange string) map[string]string {
|
||||
m := map[string]string{
|
||||
"__name__": name,
|
||||
"job": "0",
|
||||
}
|
||||
if vmrange != "" {
|
||||
m["vmrange"] = vmrange
|
||||
}
|
||||
return m
|
||||
}
|
||||
samples := func(v1, v2 float64) []*apptest.Sample {
|
||||
return []*apptest.Sample{
|
||||
{Timestamp: 1749655870000, Value: v1},
|
||||
{Timestamp: 1749655875000, Value: v2},
|
||||
}
|
||||
}
|
||||
resp := &apptest.PrometheusAPIV1QueryResponse{
|
||||
Data: &apptest.QueryData{
|
||||
Result: []*apptest.QueryResult{
|
||||
{Metric: metric("vm_histogram_metric_count", ""), Samples: samples(10, 20)},
|
||||
{Metric: metric("vm_histogram_metric_sum", ""), Samples: samples(25.5, 51)},
|
||||
{Metric: metric("vm_histogram_metric_bucket", "-1.000e+00...-5.000e-01"), Samples: samples(4, 8)},
|
||||
{Metric: metric("vm_histogram_metric_bucket", "-1.000e-03...1.000e-03"), Samples: samples(2, 4)},
|
||||
{Metric: metric("vm_histogram_metric_bucket", "5.000e-01...1.000e+00"), Samples: samples(1, 2)},
|
||||
{Metric: metric("vm_histogram_metric_bucket", "1.000e+00...2.000e+00"), Samples: samples(3, 6)},
|
||||
},
|
||||
},
|
||||
}
|
||||
// sort in the same way as the exported result
|
||||
resp.Sort()
|
||||
return resp.Data.Result
|
||||
}
|
||||
|
||||
func testRemoteReadProtocol(tc *apptest.TestCase, sut apptest.PrometheusWriteQuerier, newRemoteReadServer func(t *testing.T) *RemoteReadServer, vmctlFlags []string) {
|
||||
t := tc.T()
|
||||
t.Helper()
|
||||
|
||||
@@ -3,7 +3,7 @@ services:
|
||||
# It scrapes targets defined in --promscrape.config
|
||||
# And forward them to --remoteWrite.url
|
||||
vmagent:
|
||||
image: victoriametrics/vmagent:v1.148.0
|
||||
image: victoriametrics/vmagent:v1.149.0
|
||||
depends_on:
|
||||
- "vmauth"
|
||||
ports:
|
||||
@@ -42,14 +42,14 @@ services:
|
||||
# vmstorage shards. Each shard receives 1/N of all metrics sent to vminserts,
|
||||
# where N is number of vmstorages (2 in this case).
|
||||
vmstorage-1:
|
||||
image: victoriametrics/vmstorage:v1.148.0-cluster
|
||||
image: victoriametrics/vmstorage:v1.149.0-cluster
|
||||
volumes:
|
||||
- strgdata-1:/storage
|
||||
command:
|
||||
- "--storageDataPath=/storage"
|
||||
restart: always
|
||||
vmstorage-2:
|
||||
image: victoriametrics/vmstorage:v1.148.0-cluster
|
||||
image: victoriametrics/vmstorage:v1.149.0-cluster
|
||||
volumes:
|
||||
- strgdata-2:/storage
|
||||
command:
|
||||
@@ -59,7 +59,7 @@ services:
|
||||
# vminsert is ingestion frontend. It receives metrics pushed by vmagent,
|
||||
# pre-process them and distributes across configured vmstorage shards.
|
||||
vminsert-1:
|
||||
image: victoriametrics/vminsert:v1.148.0-cluster
|
||||
image: victoriametrics/vminsert:v1.149.0-cluster
|
||||
depends_on:
|
||||
- "vmstorage-1"
|
||||
- "vmstorage-2"
|
||||
@@ -68,7 +68,7 @@ services:
|
||||
- "--storageNode=vmstorage-2:8400"
|
||||
restart: always
|
||||
vminsert-2:
|
||||
image: victoriametrics/vminsert:v1.148.0-cluster
|
||||
image: victoriametrics/vminsert:v1.149.0-cluster
|
||||
depends_on:
|
||||
- "vmstorage-1"
|
||||
- "vmstorage-2"
|
||||
@@ -80,7 +80,7 @@ services:
|
||||
# vmselect is a query fronted. It serves read queries in MetricsQL or PromQL.
|
||||
# vmselect collects results from configured `--storageNode` shards.
|
||||
vmselect-1:
|
||||
image: victoriametrics/vmselect:v1.148.0-cluster
|
||||
image: victoriametrics/vmselect:v1.149.0-cluster
|
||||
depends_on:
|
||||
- "vmstorage-1"
|
||||
- "vmstorage-2"
|
||||
@@ -90,7 +90,7 @@ services:
|
||||
- "--vmalert.proxyURL=http://vmalert:8880"
|
||||
restart: always
|
||||
vmselect-2:
|
||||
image: victoriametrics/vmselect:v1.148.0-cluster
|
||||
image: victoriametrics/vmselect:v1.149.0-cluster
|
||||
depends_on:
|
||||
- "vmstorage-1"
|
||||
- "vmstorage-2"
|
||||
@@ -105,7 +105,7 @@ services:
|
||||
# read requests from Grafana, vmui, vmalert among vmselects.
|
||||
# It can be used as an authentication proxy.
|
||||
vmauth:
|
||||
image: victoriametrics/vmauth:v1.148.0
|
||||
image: victoriametrics/vmauth:v1.149.0
|
||||
depends_on:
|
||||
- "vmselect-1"
|
||||
- "vmselect-2"
|
||||
@@ -119,7 +119,7 @@ services:
|
||||
|
||||
# vmalert executes alerting and recording rules
|
||||
vmalert:
|
||||
image: victoriametrics/vmalert:v1.148.0
|
||||
image: victoriametrics/vmalert:v1.149.0
|
||||
depends_on:
|
||||
- "vmauth"
|
||||
ports:
|
||||
|
||||
@@ -3,7 +3,7 @@ services:
|
||||
# It scrapes targets defined in --promscrape.config
|
||||
# And forward them to --remoteWrite.url
|
||||
vmagent:
|
||||
image: victoriametrics/vmagent:v1.148.0
|
||||
image: victoriametrics/vmagent:v1.149.0
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
@@ -18,7 +18,7 @@ services:
|
||||
# VictoriaMetrics instance, a single process responsible for
|
||||
# storing metrics and serve read requests.
|
||||
victoriametrics:
|
||||
image: victoriametrics/victoria-metrics:v1.148.0
|
||||
image: victoriametrics/victoria-metrics:v1.149.0
|
||||
ports:
|
||||
- 8428:8428
|
||||
- 8089:8089
|
||||
@@ -59,7 +59,7 @@ services:
|
||||
|
||||
# vmalert executes alerting and recording rules
|
||||
vmalert:
|
||||
image: victoriametrics/vmalert:v1.148.0
|
||||
image: victoriametrics/vmalert:v1.149.0
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
- "alertmanager"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
vmagent:
|
||||
image: victoriametrics/vmagent:v1.148.0
|
||||
image: victoriametrics/vmagent:v1.149.0
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
@@ -14,7 +14,7 @@ services:
|
||||
restart: always
|
||||
|
||||
victoriametrics:
|
||||
image: victoriametrics/victoria-metrics:v1.148.0
|
||||
image: victoriametrics/victoria-metrics:v1.149.0
|
||||
ports:
|
||||
- 8428:8428
|
||||
volumes:
|
||||
@@ -40,7 +40,7 @@ services:
|
||||
restart: always
|
||||
|
||||
vmalert:
|
||||
image: victoriametrics/vmalert:v1.148.0
|
||||
image: victoriametrics/vmalert:v1.149.0
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
@@ -59,7 +59,7 @@ services:
|
||||
- '--external.alert.source=explore?orgId=1&left=["now-1h","now","VictoriaMetrics",{"expr": },{"mode":"Metrics"},{"ui":[true,true,true,"none"]}]'
|
||||
restart: always
|
||||
vmanomaly:
|
||||
image: victoriametrics/vmanomaly:v1.30.0
|
||||
image: victoriametrics/vmanomaly:v1.30.1
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
build:
|
||||
list: never
|
||||
publishResources: false
|
||||
render: never
|
||||
sitemap:
|
||||
disable: true
|
||||
---
|
||||
VictoriaMetrics Observability Stack integrates with AI assistants through [MCP servers](https://docs.victoriametrics.com/ai-tools/#mcp-servers)
|
||||
and [agent skills](https://docs.victoriametrics.com/ai-tools/#agent-skills).
|
||||
The integrations allow AI agents and automation tools to query Metrics, Logs, and Traces, analyze telemetry data,
|
||||
|
||||
@@ -16,6 +16,27 @@ Please find the changelog for VictoriaMetrics Anomaly Detection below.
|
||||
|
||||
{{% collapse name="2026" open=true %}}
|
||||
|
||||
## v1.30.1
|
||||
Released: 2026-08-06
|
||||
|
||||
- UI: Updated [vmanomaly UI](https://docs.victoriametrics.com/anomaly-detection/ui/) from [v1.8.0](https://docs.victoriametrics.com/anomaly-detection/ui/#v180) to [v1.8.1](https://docs.victoriametrics.com/anomaly-detection/ui/#v181). The update improves UX validation and fixes regressions introduced by new design.
|
||||
|
||||
- IMPROVEMENT: Reduced fit and inference latency for the Z-score, MAD, standard deviation, Seasonal Quantile, and Rolling Quantile online models. Representative service-stage gains range from 1.5-2.6x for fit and 1.7-2.3x for inference, depending on model, storage mode, and data size.
|
||||
|
||||
- IMPROVEMENT: Removed forwarded datasource credentials from in-memory state for completed, failed, canceled, and shutting-down [analysis and autotune tasks](https://docs.victoriametrics.com/anomaly-detection/components/server/#time-series-analysis-and-autotune-api).
|
||||
|
||||
- BUGFIX: Stabilized [Temporal Envelope](https://docs.victoriametrics.com/anomaly-detection/components/models/#temporal-envelope) after fitting across a late level shift. Its level, trend, residual, and supported calendar state now initialize coherently from the recent regime, avoiding stale fitted magnitudes and false seasonal oscillations when periodic inference starts.
|
||||
|
||||
- BUGFIX: Corrected `/api/v1/timeseries/characteristics` seasonality detection for time series whose timestamps are offset from whole sampling intervals. Trend interpolation now preserves the original observation grid, allowing daily and weekly patterns to be detected on shifted grids.
|
||||
|
||||
- BUGFIX: Restored backward-compatible `inference_only` [backtesting](https://docs.victoriametrics.com/anomaly-detection/components/scheduler/#backtesting-scheduler) for configurations that omit `infer_every`. The scheduler derives its inference grid from the query step or reader sampling period and preserves valid single-timestamp range queries.
|
||||
|
||||
- BUGFIX: Aligned periodic inference for exact-capable online models with exact backtesting (used in [UI](https://docs.victoriametrics.com/anomaly-detection/ui/) experiments) by applying the configured `infer_every` as the causal update cadence.
|
||||
|
||||
- BUGFIX: Corrected [self-monitoring](https://docs.victoriametrics.com/anomaly-detection/components/monitoring/#writer-behaviour-metrics) accounting so failed VictoriaMetrics write attempts contribute to `vmanomaly_writer_request_duration_seconds`, including connection retries, and inference counts only unseen *valid* rows in `vmanomaly_model_datapoints_accepted`.
|
||||
|
||||
- BUGFIX: Fixed service-level [`settings.anomaly_score_outside_data_range`](https://docs.victoriametrics.com/anomaly-detection/components/settings/#anomaly-score-outside-data-range) propagation so its configured score applies to every model unless the model defines its own override.
|
||||
|
||||
## v1.30.0
|
||||
Released: 2026-07-23
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ The decision to set the changepoint at `1.0` is made to ensure consistency acros
|
||||
> `anomaly_score` is a metric itself, which preserves all labels found in input data and (optionally) appends [custom labels, specified in writer](https://docs.victoriametrics.com/anomaly-detection/components/writer/#metrics-formatting) - follow the link for detailed output example.
|
||||
|
||||
## How is anomaly score calculated?
|
||||
For most of the [univariate models](https://docs.victoriametrics.com/anomaly-detection/components/models/#univariate-models) that can generate `yhat`, `yhat_lower`, and `yhat_upper` time series in [their output](https://docs.victoriametrics.com/anomaly-detection/components/models/#vmanomaly-output) (such as [Prophet](https://docs.victoriametrics.com/anomaly-detection/components/models/#prophet) or [Z-score](https://docs.victoriametrics.com/anomaly-detection/components/models/#z-score)), the anomaly score is calculated as follows:
|
||||
For most of the [univariate models](https://docs.victoriametrics.com/anomaly-detection/components/models/#univariate-models) that can generate `yhat`, `yhat_lower`, and `yhat_upper` time series in [their output](https://docs.victoriametrics.com/anomaly-detection/components/models/#vmanomaly-output) (such as [Temporal Envelope](https://docs.victoriametrics.com/anomaly-detection/components/models/#temporal-envelope) or [Online Z-score](https://docs.victoriametrics.com/anomaly-detection/components/models/#online-z-score)), the anomaly score is calculated as follows:
|
||||
- If `yhat` (expected series behavior) equals `y` (actual value observed), then the anomaly score is 0.
|
||||
- If `y` (actual value observed) falls within the `[yhat_lower, yhat_upper]` confidence interval, the anomaly score will gradually approach 1, the closer `y` is to the boundary.
|
||||
- If `y` (actual value observed) strictly exceeds the `[yhat_lower, yhat_upper]` interval, the anomaly score will be greater than 1, increasing as the margin between the actual value and the expected range grows.
|
||||
@@ -82,7 +82,7 @@ reader:
|
||||
|
||||
`vmanomaly` supports timezone-aware anomaly detection {{% available_from "v1.18.0" anomaly %}} through a `tz` argument, available both at the [reader level](https://docs.victoriametrics.com/anomaly-detection/components/reader/#vm-reader) and at the [query level](https://docs.victoriametrics.com/anomaly-detection/components/reader/#per-query-parameters).
|
||||
|
||||
For models that depend on seasonality, such as [`ProphetModel`](https://docs.victoriametrics.com/anomaly-detection/components/models/#prophet) and [`OnlineQuantileModel`](https://docs.victoriametrics.com/anomaly-detection/components/models/#online-seasonal-quantile), handling timezone shifts is crucial. Changes like Daylight Saving Time (DST) can disrupt seasonality patterns learned by models, resulting in inaccurate anomaly predictions as the periodic patterns shift with time. Proper timezone configuration ensures that seasonal cycles align with expected intervals, even as DST changes occur.
|
||||
For models that depend on seasonality, such as [`TemporalEnvelopeModel`](https://docs.victoriametrics.com/anomaly-detection/components/models/#temporal-envelope) and [`OnlineQuantileModel`](https://docs.victoriametrics.com/anomaly-detection/components/models/#online-seasonal-quantile), handling timezone shifts is crucial. Changes like Daylight Saving Time (DST) can disrupt seasonality patterns learned by models, resulting in inaccurate anomaly predictions as the periodic patterns shift with time. Proper timezone configuration ensures that seasonal cycles align with expected intervals, even as DST changes occur.
|
||||
|
||||
To enable timezone handling:
|
||||
1. **Reader-level**: Set `tz` in the [`reader`](https://docs.victoriametrics.com/anomaly-detection/components/reader/#vm-reader) section to a specific timezone (e.g., `Europe/Berlin`) to apply this setting to all queries.
|
||||
@@ -100,9 +100,9 @@ reader:
|
||||
tz: 'Europe/London' # per-query override
|
||||
models:
|
||||
seasonal_model:
|
||||
class: 'prophet'
|
||||
class: 'temporal_envelope'
|
||||
queries: ['your_query']
|
||||
# other model params ...
|
||||
seasonalities: ['hod_smooth', 'dow_smooth']
|
||||
```
|
||||
|
||||
## Output produced by vmanomaly
|
||||
@@ -118,36 +118,14 @@ To visualize and interact with both [self-monitoring metrics](https://docs.victo
|
||||
- {{% available_from "v1.26.0" anomaly %}} For rapid exploration of how different models, their configurations and included domain knowledge impacts the results of anomaly detection, use the built-in [vmanomaly UI](https://docs.victoriametrics.com/anomaly-detection/ui/).
|
||||

|
||||
|
||||
## Is vmanomaly stateful?
|
||||
By default, `vmanomaly` is **stateless**, meaning it does not retain any state between service restarts. However, it can be configured {{% available_from "v1.24.0" anomaly %}} to be **stateful** by enabling the `restore_state` setting in the [settings section](https://docs.victoriametrics.com/anomaly-detection/components/settings/). This allows the service to restore its state from a previous run (training data, trained models), ensuring that models continue to produce [anomaly scores](#what-is-anomaly-score) right after restart and without requiring a full retraining process or re-querying training data from VictoriaMetrics. This is particularly useful for long-running services that need to maintain continuity in anomaly detection without losing previously learned patterns, especially when using [online models](https://docs.victoriametrics.com/anomaly-detection/components/models/#online-models) that continuously adapt to new data and update their internal state. Also, [hot-reloading](https://docs.victoriametrics.com/anomaly-detection/components/#hot-reload) works well with state restoration, allowing for on-the-fly configuration changes without losing the current state of the models and reusing unchanged models/data/scheduler combinations.
|
||||
|
||||
Please refer to the [state restoration section](https://docs.victoriametrics.com/anomaly-detection/components/settings/#state-restoration) for more details on how it works and how to configure it.
|
||||
|
||||
## Config hot-reloading
|
||||
|
||||
`vmanomaly` supports [hot reload](https://docs.victoriametrics.com/anomaly-detection/components/#hot-reload) {{% available_from "v1.25.0" anomaly %}} to apply configuration-file changes automatically. Enable it with the `--watch` [CLI argument](https://docs.victoriametrics.com/anomaly-detection/quickstart/#command-line-arguments) to update the service without an explicit restart.
|
||||
|
||||
## Environment variables
|
||||
|
||||
`vmanomaly` supports {{% available_from "v1.25.0" anomaly %}} an option to reference environment variables in [configuration files](https://docs.victoriametrics.com/anomaly-detection/components/) using scalar string placeholders `%{ENV_NAME}`. This feature is particularly useful for managing sensitive information like API keys or database credentials while still making it accessible to the service. Please refer to the [environment variables section](https://docs.victoriametrics.com/anomaly-detection/components/#environment-variables) for more details and examples.
|
||||
|
||||
## Deploying vmanomaly
|
||||
|
||||
`vmanomaly` can be deployed in various environments, including Docker, Kubernetes, and VM Operator. For detailed deployment instructions, refer to the [QuickStart section](https://docs.victoriametrics.com/anomaly-detection/quickstart/#how-to-install-and-run-vmanomaly).
|
||||
|
||||
## Migration
|
||||
|
||||
For information on migrating between different versions of `vmanomaly`, please refer to the [Migration section](https://docs.victoriametrics.com/anomaly-detection/migration/) for compatibility considerations and steps for a smooth transition.
|
||||
|
||||
## Choosing the right model for vmanomaly
|
||||
|
||||
Selecting the best model for `vmanomaly` depends on the data's nature and the [types of anomalies](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-2/#categories-of-anomalies) to detect:
|
||||
|
||||
- Use [Online MAD](https://docs.victoriametrics.com/anomaly-detection/components/models/#online-mad) for simple, mostly stationary data with no-to-slow trend, when robustness to outliers is important.
|
||||
- Use [Online Z-score](https://docs.victoriametrics.com/anomaly-detection/components/models/#online-z-score) for simple, light-tailed data where standard-deviation units are meaningful.
|
||||
- Use [Temporal Envelope](https://docs.victoriametrics.com/anomaly-detection/components/models/#temporal-envelope) {{% available_from "v1.30.0" anomaly %}} for complex data with trends, calendar patterns, holidays, or persistent shifts. It is the preferred *online* alternative to Prophet (which will be deprecated in the future releases).
|
||||
- Use [Temporal Envelope](https://docs.victoriametrics.com/anomaly-detection/components/models/#temporal-envelope) {{% available_from "v1.30.0" anomaly %}} for complex data with trends, calendar patterns, holidays, or persistent shifts. It is the preferred online migration target for existing Prophet configurations.
|
||||
- Use multivariate Temporal Envelope when normal relationships between aligned metrics matter. This should replace [Isolation Forest](https://docs.victoriametrics.com/anomaly-detection/components/models/#isolation-forest-multivariate) used in previous versions of `vmanomaly`, which will be deprecated in future releases.
|
||||
- Use [Prophet](https://docs.victoriametrics.com/anomaly-detection/components/models/#prophet) when Prophet-specific decomposition outputs, or offline batch behavior are required. Consider using Temporal Envelope instead, as it is more efficient and provides better results in most cases.
|
||||
|
||||
There is also an option to auto-tune the most important parameters of a selected model class {{% available_from "v1.12.0" anomaly %}}. {{% available_from "v1.30.0" anomaly %}} The asynchronous autotune API can first profile a bounded sample through `/api/v1/timeseries/characteristics`, then tune a shared concrete configuration through `/api/v1/autotune/tasks`. See the [autotune workflow](https://docs.victoriametrics.com/anomaly-detection/components/models/#shared-asynchronous-autotune-workflow).
|
||||
|
||||
@@ -155,16 +133,6 @@ Please refer to [respective blogpost on anomaly types and alerting heuristics](h
|
||||
|
||||
Still not 100% sure what to use? We are [here to help](https://docs.victoriametrics.com/anomaly-detection/#get-in-touch).
|
||||
|
||||
## Can AI help configure vmanomaly?
|
||||
|
||||
Yes. The available tools serve different workflows:
|
||||
|
||||
- [UI Copilot](https://docs.victoriametrics.com/anomaly-detection/ui/#ai-assistance) provides interactive guidance and can apply query, model, and alerting changes in the UI.
|
||||
- The [vmanomaly MCP server](https://docs.victoriametrics.com/ai-tools/#vmanomaly-mcp-server) gives compatible AI clients access to live schemas, documentation, time-series characteristics, configuration validation, and autotune tasks.
|
||||
- [Agent skills](https://docs.victoriametrics.com/ai-tools/#agent-skills) provide repeatable workflows for investigating data and generating or reviewing `vmanomaly` and `vmalert` configurations.
|
||||
|
||||
Treat AI-generated configuration as a proposal. Review it and validate it through the UI or with [`--dryRun`](https://docs.victoriametrics.com/anomaly-detection/quickstart/#command-line-arguments) before deployment.
|
||||
|
||||
## Incorporating domain knowledge
|
||||
|
||||
Anomaly detection models can significantly improve when incorporating business-specific assumptions about the data and what constitutes an anomaly. `vmanomaly` supports various [business-side configuration parameters](https://docs.victoriametrics.com/anomaly-detection/components/models/#common-args) across all built-in models to **reduce [false positives](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-1/#false-positive)** and **align model behavior with business needs**, for example:
|
||||
@@ -222,39 +190,15 @@ models:
|
||||
provide_series: ['anomaly_score', 'y', 'yhat', 'yhat_lower', 'yhat_upper']
|
||||
```
|
||||
|
||||
## Alert generation in vmanomaly
|
||||
While `vmanomaly` detects anomalies and produces scores, it *does not directly generate alerts*. The anomaly scores are written back to VictoriaMetrics, where respective alerting tool, like [`vmalert`](https://docs.victoriametrics.com/victoriametrics/vmalert/), can be used to create alerts based on these scores for integrating it with your alerting management system. See an example diagram of how `vmanomaly` integrates into observability pipeline for anomaly detection on `node_exporter` metrics:
|
||||
## Can AI help configure vmanomaly?
|
||||
|
||||
<img src="https://docs.victoriametrics.com/anomaly-detection/guides/guide-vmanomaly-vmalert/guide-vmanomaly-vmalert_overview.webp" alt="node_exporter_example_diagram" style="width:60%"/>
|
||||
Yes. The available tools serve different workflows:
|
||||
|
||||
Once anomaly scores are written back to VictoriaMetrics, you can use [MetricsQL](https://docs.victoriametrics.com/victoriametrics/metricsql/) expressions in `vmalert` to define alerting rules based on these scores. Reasonable defaults are based around default threshold of `anomaly_score > 1`:
|
||||
- [UI Copilot](https://docs.victoriametrics.com/anomaly-detection/ui/#ai-assistance) provides interactive guidance and can apply query, model, and alerting changes in the UI.
|
||||
- The [vmanomaly MCP server](https://docs.victoriametrics.com/ai-tools/#vmanomaly-mcp-server) gives compatible AI clients access to live schemas, documentation, time-series characteristics, configuration validation, and autotune tasks.
|
||||
- [Agent skills](https://docs.victoriametrics.com/ai-tools/#agent-skills) provide repeatable workflows for investigating data and generating or reviewing `vmanomaly` and `vmalert` configurations.
|
||||
|
||||
```yaml
|
||||
groups:
|
||||
- name: VMAnomalyAlerts
|
||||
interval: 60s
|
||||
rules:
|
||||
- alert: HighAnomalyScore
|
||||
expr: min(anomaly_score) without (model_alias, scheduler_alias) >= 1
|
||||
for: 5m # adjust to your needs based on data frequency and alerting policies
|
||||
labels:
|
||||
severity: warning
|
||||
query_alias: explore
|
||||
model_alias: default
|
||||
scheduler_alias: periodic
|
||||
preset: ui
|
||||
annotations:
|
||||
summary: High anomaly score detected.
|
||||
description: Anomaly score exceeded threshold ({{ $value }}) for more than
|
||||
{{ $for }} for query {{ $labels.for }}.
|
||||
```
|
||||
|
||||
> {{% available_from "v1.27.0" anomaly %}} You can also use the [vmanomaly UI](https://docs.victoriametrics.com/anomaly-detection/ui/) to generate alerting rules automatically based on your model configurations and selected thresholds.
|
||||
|
||||
> {{% available_from "v1.28.3" anomaly %}} Check out our [MCP Server](https://github.com/VictoriaMetrics/mcp-vmanomaly) to get AI-assisted recommendations on setting up alerting rules based on produced anomaly scores. See [installation guide](https://github.com/VictoriaMetrics/mcp-vmanomaly#installation) for more details.
|
||||
|
||||
## Preventing alert fatigue
|
||||
Produced anomaly scores are designed in such a way that values from 0.0 to 1.0 indicate non-anomalous data, while a value greater than 1.0 is generally classified as an anomaly. However, there are no perfect models for anomaly detection, that's why reasonable defaults expressions like `anomaly_score > 1` may not work 100% of the time. However, anomaly scores, produced by `vmanomaly` are written back as metrics to VictoriaMetrics, where tools like [`vmalert`](https://docs.victoriametrics.com/victoriametrics/vmalert/) can use [MetricsQL](https://docs.victoriametrics.com/victoriametrics/metricsql/) expressions to fine-tune alerting thresholds and conditions, balancing between avoiding [false negatives](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-1/#false-negative) and reducing [false positives](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-1/#false-positive).
|
||||
Treat AI-generated configuration as a proposal. Review it and validate it through the UI or with [`--dryRun`](https://docs.victoriametrics.com/anomaly-detection/quickstart/#command-line-arguments) before deployment.
|
||||
|
||||
## How to backtest particular configuration on historical data?
|
||||
|
||||
@@ -309,7 +253,7 @@ Configuration above will produce N intervals of full length (`fit_window`=14d +
|
||||
|
||||
## Forecasting
|
||||
|
||||
`vmanomaly` can generate future forecasts using [Temporal Envelope](https://docs.victoriametrics.com/anomaly-detection/components/models/#temporal-envelope) {{% available_from "v1.30.0" anomaly %}} or [ProphetModel](https://docs.victoriametrics.com/anomaly-detection/components/models/#prophet) {{% available_from "v1.25.3" anomaly %}}. This is helpful for capacity planning, resource allocation, or trend analysis when the underlying data is complex and exceeds what inline MetricsQL queries, including [predict_linear](https://docs.victoriametrics.com/victoriametrics/metricsql/#predict_linear), can handle.
|
||||
`vmanomaly` can generate future forecasts with [Temporal Envelope](https://docs.victoriametrics.com/anomaly-detection/components/models/#temporal-envelope) {{% available_from "v1.30.0" anomaly %}}, the preferred online forecasting model. [ProphetModel](https://docs.victoriametrics.com/anomaly-detection/components/models/#prophet) {{% available_from "v1.25.3" anomaly %}} also supports forecasting for existing offline configurations. Forecasts help with capacity planning, resource allocation, or trend analysis when the underlying data is complex and exceeds what inline MetricsQL queries, including [predict_linear](https://docs.victoriametrics.com/victoriametrics/metricsql/#predict_linear), can handle.
|
||||
|
||||
> However, please note that this mode should be used with care, as the model will produce `yhat_{h}` (and probably `yhat_lower_{h}`, and `yhat_upper_{h}`) time series **for each timeseries returned by input queries and for each forecasting horizon specified in `forecast_at` argument, which can lead to a significant increase in the number of active timeseries in VictoriaMetrics TSDB**.
|
||||
|
||||
@@ -417,6 +361,61 @@ groups:
|
||||
description: "Disk usage is forecasted to exceed 95% in the next 3 days for instance {{ $labels.instance }}. Forecasted value: {{ $value }}."
|
||||
```
|
||||
|
||||
## Alert generation in vmanomaly
|
||||
While `vmanomaly` detects anomalies and produces scores, it *does not directly generate alerts*. The anomaly scores are written back to VictoriaMetrics, where respective alerting tool, like [`vmalert`](https://docs.victoriametrics.com/victoriametrics/vmalert/), can be used to create alerts based on these scores for integrating it with your alerting management system. See an example diagram of how `vmanomaly` integrates into observability pipeline for anomaly detection on `node_exporter` metrics:
|
||||
|
||||
<img src="/anomaly-detection/guides/guide-vmanomaly-vmalert/guide-vmanomaly-vmalert_overview.svg" alt="Typical vmanomaly observability pipeline using node-exporter, vmagent, VictoriaMetrics, Grafana, vmalert, and Alertmanager" style="width:60%"/>
|
||||
|
||||
Once anomaly scores are written back to VictoriaMetrics, you can use [MetricsQL](https://docs.victoriametrics.com/victoriametrics/metricsql/) expressions in `vmalert` to define alerting rules based on these scores. Reasonable defaults are based around default threshold of `anomaly_score > 1`:
|
||||
|
||||
```yaml
|
||||
groups:
|
||||
- name: VMAnomalyAlerts
|
||||
interval: 60s
|
||||
rules:
|
||||
- alert: HighAnomalyScore
|
||||
expr: min(anomaly_score) without (model_alias, scheduler_alias) >= 1
|
||||
for: 5m # adjust to your needs based on data frequency and alerting policies
|
||||
labels:
|
||||
severity: warning
|
||||
query_alias: explore
|
||||
model_alias: default
|
||||
scheduler_alias: periodic
|
||||
preset: ui
|
||||
annotations:
|
||||
summary: High anomaly score detected.
|
||||
description: Anomaly score exceeded threshold ({{ $value }}) for more than
|
||||
{{ $for }} for query {{ $labels.for }}.
|
||||
```
|
||||
|
||||
> {{% available_from "v1.27.0" anomaly %}} You can also use the [vmanomaly UI](https://docs.victoriametrics.com/anomaly-detection/ui/) to generate alerting rules automatically based on your model configurations and selected thresholds.
|
||||
|
||||
> {{% available_from "v1.28.3" anomaly %}} Check out our [MCP Server](https://github.com/VictoriaMetrics/mcp-vmanomaly) to get AI-assisted recommendations on setting up alerting rules based on produced anomaly scores. See [installation guide](https://github.com/VictoriaMetrics/mcp-vmanomaly#installation) for more details.
|
||||
|
||||
## Preventing alert fatigue
|
||||
Produced anomaly scores are designed in such a way that values from 0.0 to 1.0 indicate non-anomalous data, while a value greater than 1.0 is generally classified as an anomaly. However, there are no perfect models for anomaly detection, that's why reasonable defaults expressions like `anomaly_score > 1` may not work 100% of the time. However, anomaly scores, produced by `vmanomaly` are written back as metrics to VictoriaMetrics, where tools like [`vmalert`](https://docs.victoriametrics.com/victoriametrics/vmalert/) can use [MetricsQL](https://docs.victoriametrics.com/victoriametrics/metricsql/) expressions to fine-tune alerting thresholds and conditions, balancing between avoiding [false negatives](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-1/#false-negative) and reducing [false positives](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-1/#false-positive).
|
||||
|
||||
## Deploying vmanomaly
|
||||
|
||||
`vmanomaly` can be deployed in various environments, including Docker, Kubernetes, and VM Operator. For detailed deployment instructions, refer to the [QuickStart section](https://docs.victoriametrics.com/anomaly-detection/quickstart/#how-to-install-and-run-vmanomaly).
|
||||
|
||||
## Environment variables
|
||||
|
||||
`vmanomaly` supports {{% available_from "v1.25.0" anomaly %}} an option to reference environment variables in [configuration files](https://docs.victoriametrics.com/anomaly-detection/components/) using scalar string placeholders `%{ENV_NAME}`. This feature is particularly useful for managing sensitive information like API keys or database credentials while still making it accessible to the service. Please refer to the [environment variables section](https://docs.victoriametrics.com/anomaly-detection/components/#environment-variables) for more details and examples.
|
||||
|
||||
## Is vmanomaly stateful?
|
||||
By default, `vmanomaly` is **stateless**, meaning it does not retain any state between service restarts. However, it can be configured {{% available_from "v1.24.0" anomaly %}} to be **stateful** by enabling the `restore_state` setting in the [settings section](https://docs.victoriametrics.com/anomaly-detection/components/settings/). This allows the service to restore its state from a previous run (training data, trained models), ensuring that models continue to produce [anomaly scores](#what-is-anomaly-score) right after restart and without requiring a full retraining process or re-querying training data from VictoriaMetrics. This is particularly useful for long-running services that need to maintain continuity in anomaly detection without losing previously learned patterns, especially when using [online models](https://docs.victoriametrics.com/anomaly-detection/components/models/#online-models) that continuously adapt to new data and update their internal state. Also, [hot-reloading](https://docs.victoriametrics.com/anomaly-detection/components/#hot-reload) works well with state restoration, allowing for on-the-fly configuration changes without losing the current state of the models and reusing unchanged models/data/scheduler combinations.
|
||||
|
||||
Please refer to the [state restoration section](https://docs.victoriametrics.com/anomaly-detection/components/settings/#state-restoration) for more details on how it works and how to configure it.
|
||||
|
||||
## Config hot-reloading
|
||||
|
||||
`vmanomaly` supports [hot reload](https://docs.victoriametrics.com/anomaly-detection/components/#hot-reload) {{% available_from "v1.25.0" anomaly %}} to apply configuration-file changes automatically. Enable it with the `--watch` [CLI argument](https://docs.victoriametrics.com/anomaly-detection/quickstart/#command-line-arguments) to update the service without an explicit restart.
|
||||
|
||||
## Migration
|
||||
|
||||
For information on migrating between different versions of `vmanomaly`, please refer to the [Migration section](https://docs.victoriametrics.com/anomaly-detection/migration/) for compatibility considerations and steps for a smooth transition.
|
||||
|
||||
## Resource consumption of vmanomaly
|
||||
`vmanomaly` itself is a lightweight service, resource usage is primarily dependent on [scheduling](https://docs.victoriametrics.com/anomaly-detection/components/scheduler/) (how often and on what data to fit/infer your models), [# and size of timeseries returned by your queries](https://docs.victoriametrics.com/anomaly-detection/components/reader/#vm-reader), and the complexity of the employed [models](https://docs.victoriametrics.com/anomaly-detection/components/models/). Its resource usage is directly related to these factors, making it adaptable to various operational scales. Various optimizations are available to balance between RAM usage, processing speed, and model capacity. These options are described in the sections below.
|
||||
|
||||
@@ -432,7 +431,7 @@ services:
|
||||
# ...
|
||||
vmanomaly:
|
||||
container_name: vmanomaly
|
||||
image: victoriametrics/vmanomaly:v1.30.0
|
||||
image: victoriametrics/vmanomaly:v1.30.1
|
||||
# ...
|
||||
restart: always
|
||||
volumes:
|
||||
@@ -554,7 +553,8 @@ reader:
|
||||
expr: 'sum(ALERTS{alertstate=~'(pending|firing)'}) by (alertstate)'
|
||||
max_points_per_query: 5000 # query-level override
|
||||
models:
|
||||
prophet:
|
||||
temporal_envelope:
|
||||
class: temporal_envelope
|
||||
# other model args
|
||||
queries: [
|
||||
'sum_alerts',
|
||||
@@ -575,7 +575,8 @@ reader:
|
||||
sum_alerts:
|
||||
expr: 'sum(ALERTS{alertstate=~'(pending|firing)'}) by (alertstate)'
|
||||
models:
|
||||
prophet:
|
||||
temporal_envelope:
|
||||
class: temporal_envelope
|
||||
# other model args
|
||||
queries: [
|
||||
'sum_alerts',
|
||||
@@ -594,7 +595,8 @@ reader:
|
||||
sum_alerts_firing:
|
||||
expr: 'sum(ALERTS{alertstate='firing'}) by ()'
|
||||
models:
|
||||
prophet:
|
||||
temporal_envelope:
|
||||
class: temporal_envelope
|
||||
# other model args
|
||||
queries: [
|
||||
'sum_alerts_pending',
|
||||
@@ -652,7 +654,7 @@ options:
|
||||
Here’s an example of using the config splitter to divide configurations based on the `extra_filters` argument from the reader section:
|
||||
|
||||
```sh
|
||||
docker pull victoriametrics/vmanomaly:v1.30.0 && docker image tag victoriametrics/vmanomaly:v1.30.0 vmanomaly
|
||||
docker pull victoriametrics/vmanomaly:v1.30.1 && docker image tag victoriametrics/vmanomaly:v1.30.1 vmanomaly
|
||||
```
|
||||
|
||||
```sh
|
||||
|
||||
@@ -45,7 +45,7 @@ There are 2 types of compatibility to consider when migrating in stateful mode:
|
||||
|
||||
| Group start | Group end | Compatibility | Notes |
|
||||
|---------|--------- |------------|-------|
|
||||
| [v1.29.1](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1291) | [v1.30.0](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1300) | Fully Compatible | v1.30.0 adds new [Temporal Envelope](https://docs.victoriametrics.com/anomaly-detection/components/models/#temporal-envelope) model state without changing the compatibility of existing model and data artifacts. |
|
||||
| [v1.29.1](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1291) | [v1.30.1](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1301) | Fully Compatible | v1.30.0 adds new [Temporal Envelope](https://docs.victoriametrics.com/anomaly-detection/components/models/#temporal-envelope) model state without changing the compatibility of existing model and data artifacts. v1.30.1 remains compatible with v1.30.0 state and its compatible predecessors. |
|
||||
| [v1.28.7](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1287) | [v1.29.0](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1290) | Partially compatible* | Dumped models of class [prophet](https://docs.victoriametrics.com/anomaly-detection/components/models/#prophet) and [seasonal quantile](https://docs.victoriametrics.com/anomaly-detection/components/models/#online-seasonal-quantile) have problems with loading to [v1.29.0](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1290) due to dropped `pytz` library. **Upgrading directly from v1.28.7 to [v1.29.1](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1291) with a fix is suggested** |
|
||||
| [v1.26.0](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1262) | [v1.28.7](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1287) | Fully Compatible | [v1.28.0](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1280) introduced [rolling](https://docs.victoriametrics.com/anomaly-detection/components/models/#rolling-models) model class drop in favor of [online](https://docs.victoriametrics.com/anomaly-detection/components/models/#online-models) models (`rolling_quantile` and `std` models), however, it does not impact compatibility, as artifacts were not produced by default for rolling models. Also, offline `mad` and `zscore` models are redirecting to their respective online counterparts since [v1.28.4](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1284). |
|
||||
| [v1.25.3](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1253) | [v1.26.0](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1270) | Partially Compatible* | [v1.25.3](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1253) introduced `forecast_at` argument for base [univariate](https://docs.victoriametrics.com/anomaly-detection/components/models/#univariate-models) and `Prophet` [models](https://docs.victoriametrics.com/anomaly-detection/components/models/#prophet), however, itself remains backward-reversible from newer states like [v1.26.2](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1262), [v1.27.0](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1270). (All models except `isolation_forest_multivariate` class will be dropped) |
|
||||
@@ -71,7 +71,7 @@ In stateless mode, the migration process is almost straightforward as there are
|
||||
|
||||
**Breaking Changes**
|
||||
|
||||
- [v1.12.0](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1120) **ARIMA** model is removed from [built-in models](https://docs.victoriametrics.com/anomaly-detection/components/models/#built-in-models); Action: replace ARIMA by [Prophet](https://docs.victoriametrics.com/anomaly-detection/components/models/#prophet) or alternative seasonal models in `model(s)` section of your configuration files.
|
||||
- [v1.12.0](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1120) **ARIMA** model is removed from [built-in models](https://docs.victoriametrics.com/anomaly-detection/components/models/#built-in-models). Action: for vmanomaly v1.30.0 and newer, replace ARIMA with [Temporal Envelope](https://docs.victoriametrics.com/anomaly-detection/components/models/#temporal-envelope) {{% available_from "v1.30.0" anomaly %}}; for older releases, use another supported seasonal model in the `models` section of the configuration.
|
||||
|
||||
- [v1.9.0](https://docs.victoriametrics.com/anomaly-detection/changelog/#v190) The `sampling_period` parameter is now mandatory in `VmReader`. This change aims to clarify and standardize the frequency of input/output in `vmanomaly`, thereby reducing uncertainty and aligning with user expectations; Action: Add the `sampling_period` parameter to your `VmReader` configuration, e.g.:
|
||||
|
||||
|
||||
@@ -126,13 +126,18 @@ groups:
|
||||
> docker pull quay.io/victoriametrics/vmanomaly:vX.Y.Z
|
||||
> ```
|
||||
|
||||
> [!NOTE] ARM64 startup on affected Apple Silicon virtualization
|
||||
> On some `linux/arm64` environments running through virtualization on Apple M4/M5 hosts, `vmanomaly` may exit with `SIGILL` (exit code `132`) before startup. This is caused by the virtualized host advertising an SVE2 capability that traps when used by OpenSSL 4.x; it does not affect all ARM64 systems.
|
||||
>
|
||||
> On affected hosts, add `-e OPENSSL_armcap=0` to `docker run`, or add `- OPENSSL_armcap=0` under the service's Docker Compose `environment`, matching the list syntax used below. This disables ARM cryptographic acceleration, so apply it only as a temporary workaround on affected hosts.
|
||||
|
||||
|
||||
Below are the steps to get `vmanomaly` up and running inside a Docker container:
|
||||
|
||||
1. Pull Docker image:
|
||||
|
||||
```sh
|
||||
docker pull victoriametrics/vmanomaly:v1.30.0
|
||||
docker pull victoriametrics/vmanomaly:v1.30.1
|
||||
```
|
||||
|
||||
2. Create the license file with your license key.
|
||||
@@ -152,7 +157,7 @@ docker run -it \
|
||||
-v ./license:/license \
|
||||
-v ./config.yaml:/config.yaml \
|
||||
-p 8490:8490 \
|
||||
victoriametrics/vmanomaly:v1.30.0 \
|
||||
victoriametrics/vmanomaly:v1.30.1 \
|
||||
/config.yaml \
|
||||
--licenseFile=/license \
|
||||
--loggerLevel=INFO \
|
||||
@@ -169,7 +174,7 @@ docker run -it \
|
||||
-e VMANOMALY_DATA_DUMPS_DIR=/tmp/vmanomaly/data \
|
||||
-e VMANOMALY_MODEL_DUMPS_DIR=/tmp/vmanomaly/models \
|
||||
-p 8490:8490 \
|
||||
victoriametrics/vmanomaly:v1.30.0 \
|
||||
victoriametrics/vmanomaly:v1.30.1 \
|
||||
/config.yaml \
|
||||
--licenseFile=/license \
|
||||
--loggerLevel=INFO \
|
||||
@@ -182,7 +187,7 @@ services:
|
||||
# ...
|
||||
vmanomaly:
|
||||
container_name: vmanomaly
|
||||
image: victoriametrics/vmanomaly:v1.30.0
|
||||
image: victoriametrics/vmanomaly:v1.30.1
|
||||
# ...
|
||||
restart: always
|
||||
volumes:
|
||||
|
||||
@@ -27,7 +27,7 @@ Key functions:
|
||||
|
||||
The diagram below illustrates how `vmanomaly` fits into an observability setup, such as detecting anomalies in metrics collected by `node_exporter`:
|
||||
|
||||
<img src="https://docs.victoriametrics.com/anomaly-detection/guides/guide-vmanomaly-vmalert/guide-vmanomaly-vmalert_overview.webp" alt="node_exporter_example_diagram" style="width:60%"/>
|
||||
<img src="/anomaly-detection/guides/guide-vmanomaly-vmalert/guide-vmanomaly-vmalert_overview.svg" alt="Typical vmanomaly observability pipeline using node-exporter, vmagent, VictoriaMetrics, Grafana, vmalert, and Alertmanager" style="width:60%"/>
|
||||
|
||||
## How does it work?
|
||||
|
||||
@@ -40,7 +40,7 @@ VictoriaMetrics Anomaly Detection **continuously re-fit and apply machine learni
|
||||
- **Confidence intervals** (`[yhat_lower, yhat_upper]`)
|
||||
These outputs integrate seamlessly into downstream applications, making it easier to **visually inspect anomalies**, e.g. in respective [Grafana dashboards](https://docs.victoriametrics.com/anomaly-detection/presets/#grafana-dashboard).
|
||||
|
||||
<img src="https://docs.victoriametrics.com/anomaly-detection/components/vmanomaly-components.webp" alt="node_exporter_example_diagram" style="width:80%"/>
|
||||
{{% content "components/vmanomaly-components-diagram.md" %}}
|
||||
|
||||
## Key benefits
|
||||
|
||||
|
||||
@@ -78,9 +78,7 @@ These [sub-configurations](#sub-configuration) can be assigned to a specific sha
|
||||
|
||||
Additionally, a replication factor `R ≥ 1` ensures [high availability](#high-availability) by enforcing redundancy across shards.
|
||||
|
||||
<p></p>
|
||||
|
||||

|
||||
{{% content "vmanomaly-sharding-ha-diagram.md" %}}
|
||||
|
||||
> Please [refer to deployment options section](#deployment-options) for the examples (Docker, Docker Compose, Helm). To avoid duplicate metrics being reported from each vmanomaly service used in sharded mode, make sure that [deduplication](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#deduplication) is configured on vmsingle or vmselect and vmstorage for the VictoriaMetrics instance used in the [writer section of the configuration](https://docs.victoriametrics.com/anomaly-detection/components/writer/).
|
||||
|
||||
@@ -130,9 +128,7 @@ Similar to other VictoriaMetrics ecosystem components, like [VMAgent](https://do
|
||||
|
||||
When `VMANOMALY_REPLICATION_FACTOR` > 1, each [sub-config](#sub-configuration) `n` from `{0, N-1}` is assigned to exactly `R` nodes. This ensures redundancy, preventing single-node failures from causing data loss.
|
||||
|
||||
<p></p>
|
||||
|
||||

|
||||
{{% content "vmanomaly-sharding-ha-diagram.md" %}}
|
||||
|
||||
> Please [refer to deployment options section](#deployment-options) for the examples (Docker, Docker Compose, Helm). To avoid duplicate metrics being reported from each vmanomaly service used in sharded mode, make sure that [deduplication](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#deduplication) is configured on vmsingle or vmselect and vmstorage for the VictoriaMetrics instance used in the [writer section of the configuration](https://docs.victoriametrics.com/anomaly-detection/components/writer/).
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ The best applications of this mode are:
|
||||
|
||||
### What you can do with Copilot
|
||||
|
||||
- **Ask questions** about any model (e.g. [Temporal Envelope](https://docs.victoriametrics.com/anomaly-detection/components/models/#temporal-envelope), [Prophet](https://docs.victoriametrics.com/anomaly-detection/components/models/#prophet), or [Z-score](https://docs.victoriametrics.com/anomaly-detection/components/models/#online-z-score) - parameters, trade-offs, when to use each)
|
||||
- **Ask questions** about any model (e.g. [Temporal Envelope](https://docs.victoriametrics.com/anomaly-detection/components/models/#temporal-envelope), [Online Seasonal Quantile](https://docs.victoriametrics.com/anomaly-detection/components/models/#online-seasonal-quantile), or [Online Z-score](https://docs.victoriametrics.com/anomaly-detection/components/models/#online-z-score) - parameters, trade-offs, when to use each)
|
||||
- **Improve detection quality** - describe what's wrong ("too many false positives", "missing spikes") and Copilot reads the config, searches the docs, and proposes a validated configuration change to fix the issue.
|
||||
- **Get config suggestions inline** - suggestions appear as interactive cards with an explanation and a YAML diff; click **Apply** to write the change directly to your current settings, or **Decline** to keep the conversation going.
|
||||
- {{% available_from "v1.30.0" anomaly %}} **Profile and tune the real query** - with [mcp-vmanomaly](#mcp-tools-server) connected, Copilot can inspect bounded time-series characteristics, recommend an online model, start an asynchronous autotune task, and apply its validated query and model suggestions.
|
||||
@@ -316,7 +316,7 @@ docker run -it --rm \
|
||||
-e VMANOMALY_MCP_SERVER_URL=http://mcp-vmanomaly:8081/mcp \
|
||||
-p 8080:8080 \
|
||||
-p 8490:8490 \
|
||||
victoriametrics/vmanomaly:v1.30.0 \
|
||||
victoriametrics/vmanomaly:v1.30.1 \
|
||||
vmanomaly_config.yaml
|
||||
```
|
||||
|
||||
@@ -569,7 +569,7 @@ Set up the time range and resolution (step) for data visualization and anomaly d
|
||||
|
||||

|
||||
|
||||
Pay attention to trends, seasonality, noise, outliers, and other patterns in the data, which can influence the choice of anomaly detection model and its hyperparameters (e.g. use seasonal models for seasonal data - like `Prophet`, robust models for noisy de-seasonalized data - like `MAD`, etc.).
|
||||
Pay attention to trends, seasonality, noise, outliers, and other patterns in the data, which can influence the choice of anomaly detection model and its hyperparameters. Use [Temporal Envelope](https://docs.victoriametrics.com/anomaly-detection/components/models/#temporal-envelope) for complex data with trend or calendar patterns, and [Online MAD](https://docs.victoriametrics.com/anomaly-detection/components/models/#online-mad) for simple, mostly stationary data where robustness to outliers matters.
|
||||
|
||||

|
||||
|
||||
@@ -645,6 +645,19 @@ If the **results** look good and the **model configuration should be deployed in
|
||||
|
||||
{{% collapse name="Release history" %}}
|
||||
|
||||
### v1.8.1
|
||||
Released: 2026-08-06
|
||||
|
||||
vmanomaly version: [v1.30.1](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1301)
|
||||
|
||||
- IMPROVEMENT: Model settings are validated and normalized when applied. Invalid drafts remain open with actionable feedback, and advanced-setting summaries open the corresponding editor directly.
|
||||
|
||||
- BUGFIX: Server-query counts load when the query drawer opens, and numeric model fields preserve valid scalar and range values while reporting parsing errors on blur.
|
||||
|
||||
- BUGFIX: The anomaly visualization empty state now follows the active theme instead of using light-theme colors in dark mode.
|
||||
|
||||
- BUGFIX: Tenant selection now follows the datasource URL resolved by the server, avoiding an incorrect switch to tenant `0` when it is unavailable.
|
||||
|
||||
### v1.8.0
|
||||
Released: 2026-07-23
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ Below, you will find an example illustrating how the components of `vmanomaly` i
|
||||
|
||||
> [Reader](https://docs.victoriametrics.com/anomaly-detection/components/reader/#vm-reader) and [Writer](https://docs.victoriametrics.com/anomaly-detection/components/writer/#vm-writer) also support [multitenancy](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#multitenancy), so you can read/write from/to different locations - see `tenant_id` param description.
|
||||
|
||||

|
||||
{{% content "vmanomaly-components-diagram.md" %}}
|
||||
|
||||
## Example config
|
||||
|
||||
@@ -54,11 +54,11 @@ schedulers:
|
||||
fit_window: "3d" # how much historical data to use for fit stage
|
||||
start_from: "00:00" # align the annual fit schedule to midnight in the configured timezone
|
||||
tz: "Europe/Kyiv" # timezone to use for start_from
|
||||
periodic_offline_1w:
|
||||
periodic_online_weekly:
|
||||
class: 'periodic'
|
||||
infer_every: "15m"
|
||||
scatter_infer_jobs: true
|
||||
fit_every: "24h"
|
||||
fit_every: "365d" # online state continues adapting between infrequent full re-fits
|
||||
fit_window: "14d"
|
||||
# if no start_from is specified, jobs will start immediately after service starts
|
||||
|
||||
@@ -75,18 +75,16 @@ models:
|
||||
min_dev_from_expected: 0.0 # turned off. if |y - yhat| < min_dev_from_expected, anomaly score will be 0
|
||||
detection_direction: 'above_expected' # detect anomalies only when y > yhat, "peaks"
|
||||
clip_predictions: True # clip predictions to expected data range, i.e. [0, inf] for this query `host_network_receive_errors
|
||||
prophet_weekly: # we can set up alias for model
|
||||
class: 'prophet'
|
||||
envelope_weekly: # we can set up alias for model
|
||||
class: 'temporal_envelope'
|
||||
provide_series: ['anomaly_score', 'y', 'yhat', 'yhat_lower', 'yhat_upper']
|
||||
queries: ['cpu_seconds_total']
|
||||
schedulers: ['periodic_offline_1w'] # will be attached to 1-week scheduler, re-fit every 24h and infer every 15m
|
||||
schedulers: ['periodic_online_weekly'] # fit on two weekly cycles, then update online every 15m
|
||||
min_dev_from_expected: [0.01, 0.01] # minimum deviation from expected value to be even considered as anomaly
|
||||
anomaly_score_outside_data_range: 1.5 # override default anomaly score outside expected data range
|
||||
detection_direction: 'above_expected'
|
||||
clip_predictions: True # clip predictions to expected data range, i.e. [0, inf] for this query `cpu_seconds_total`
|
||||
args: # model-specific arguments
|
||||
interval_width: 0.98
|
||||
yearly_seasonality: False # disable yearly seasonality, since we have only 7 days of data
|
||||
seasonalities: ['hod_smooth', 'dow_smooth']
|
||||
|
||||
# where to read data from
|
||||
# https://docs.victoriametrics.com/anomaly-detection/components/reader/#vm-reader
|
||||
@@ -113,7 +111,7 @@ reader:
|
||||
# https://docs.victoriametrics.com/anomaly-detection/components/writer/
|
||||
writer:
|
||||
datasource_url: "http://victoriametrics:8428/"
|
||||
# tenant_id: "0:0" # for VictoriaMetrics cluster, can support "multitenant"
|
||||
tenant_id: "0:0" # for VictoriaMetrics cluster, can support "multitenant"
|
||||
# https://docs.victoriametrics.com/anomaly-detection/components/writer/#metrics-formatting
|
||||
metric_format:
|
||||
__name__: $VAR
|
||||
@@ -204,6 +202,7 @@ models:
|
||||
|
||||
writer:
|
||||
datasource_url: "http://victoriametrics:8428/"
|
||||
tenant_id: "0:0"
|
||||
|
||||
monitoring:
|
||||
push:
|
||||
|
||||
68
docs/anomaly-detection/components/autotune.svg
Normal file
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 968 420" role="img" aria-labelledby="title description">
|
||||
<title id="title">AutoTunedModel tuning and inference lifecycle</title>
|
||||
<desc id="description">The tuning process tests and scores model candidates across n time-series splits until the trial count or timeout is reached. Each fold fits a candidate on training data and predicts anomalies on its validation segment. The best model is then used on inference data until the next fit call.</desc>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0 0 10 5 0 10Z" fill="#202124"/>
|
||||
</marker>
|
||||
<style>
|
||||
text { font-family: Arial, Helvetica, sans-serif; fill: #202124; font-size: 18px; }
|
||||
.line { fill: none; stroke: #202124; stroke-width: 2.5; }
|
||||
.arrow { fill: none; stroke: #202124; stroke-width: 2.5; marker-end: url(#arrow); }
|
||||
.dash { fill: none; stroke: #202124; stroke-width: 2.5; stroke-dasharray: 8 9; }
|
||||
.box { fill: #fff; stroke: #202124; stroke-width: 1.5; }
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<rect width="968" height="420" fill="#fff"/>
|
||||
<rect x="112" y="10" width="242" height="83" class="box"/>
|
||||
<text x="233" y="38" text-anchor="middle">
|
||||
<tspan x="233" dy="0">test and score candidates</tspan>
|
||||
<tspan x="233" dy="21">until `n_trials` or `timeout`</tspan>
|
||||
<tspan x="233" dy="21">is reached</tspan>
|
||||
</text>
|
||||
<path d="M354 49 V67" class="arrow"/>
|
||||
<text x="355" y="90" text-anchor="middle">Tuning</text>
|
||||
<text x="355" y="112" text-anchor="middle">process</text>
|
||||
|
||||
<path d="M162 157 V136 H549 V157" class="line"/>
|
||||
<path d="M355 112 V136" class="line"/>
|
||||
<path d="M399 93 H653 V152" class="line"/>
|
||||
|
||||
<path d="M113 161 H75 V350 H113" class="line"/>
|
||||
<path d="M75 236 H39" class="line"/>
|
||||
<text x="28" y="250" transform="rotate(-90 28 250)" text-anchor="middle">n splits</text>
|
||||
|
||||
<text x="102" y="227">Fold 1</text>
|
||||
<text x="102" y="255">Fold 2</text>
|
||||
<text x="102" y="283">Fold 3</text>
|
||||
<text x="120" y="327">...</text>
|
||||
|
||||
<text x="260" y="179" text-anchor="middle">Fit</text>
|
||||
<text x="260" y="202" text-anchor="middle">candidate</text>
|
||||
<text x="413" y="179" text-anchor="middle">Predict</text>
|
||||
<text x="413" y="202" text-anchor="middle">anomalies</text>
|
||||
|
||||
<path d="M173 221 H354 V207" class="line"/>
|
||||
<path d="M232 249 H412 V235" class="line"/>
|
||||
<path d="M293 280 H472 V265" class="line"/>
|
||||
<path d="M354 221 H412" class="dash"/>
|
||||
<path d="M412 249 H474" class="dash"/>
|
||||
<path d="M472 280 H513" class="dash"/>
|
||||
|
||||
<path d="M548 144 V415" class="dash" style="stroke-width:1.5;stroke-dasharray:4 6"/>
|
||||
<rect x="577" y="152" width="136" height="61" rx="12" class="box"/>
|
||||
<text x="645" y="187" text-anchor="middle">best model</text>
|
||||
<path d="M653 213 V348" class="arrow"/>
|
||||
<text x="815" y="168" text-anchor="middle">
|
||||
<tspan x="815" dy="0">used to predict</tspan>
|
||||
<tspan x="815" dy="21">on inference data</tspan>
|
||||
<tspan x="815" dy="21">until the next `fit` call</tspan>
|
||||
</text>
|
||||
|
||||
<path d="M40 350 H895" class="arrow"/>
|
||||
<text x="455" y="402" text-anchor="middle">Training data</text>
|
||||
<text x="623" y="402" text-anchor="middle">Inference data</text>
|
||||
<text x="856" y="402">Time axis, t</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,139 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1920 1295" role="img" aria-labelledby="title description">
|
||||
<title id="title">Multivariate models lifecycle</title>
|
||||
<desc id="description">MetricsQL queries retrieve an aligned set of series from VictoriaMetrics. Reader data fits one shared multivariate model. The exact same series set produces one anomaly score series with the intersected label set; inference is skipped when the fit and inference series sets differ. Writer stores produced scores in VictoriaMetrics.</desc>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 12 12" refX="10.5" refY="6" markerWidth="16" markerHeight="16" markerUnits="userSpaceOnUse" orient="auto-start-reverse">
|
||||
<path d="M1.5 1.5 10.5 6 1.5 10.5" fill="none" stroke="#303038" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</marker>
|
||||
<marker id="arrow-blue" viewBox="0 0 12 12" refX="10.5" refY="6" markerWidth="16" markerHeight="16" markerUnits="userSpaceOnUse" orient="auto-start-reverse">
|
||||
<path d="M1.5 1.5 10.5 6 1.5 10.5" fill="none" stroke="#1478c9" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</marker>
|
||||
<style>
|
||||
text { font-family: Arial, Helvetica, sans-serif; fill: #303038; }
|
||||
.title { font-size: 50px; font-weight: 400; }
|
||||
.node { font-size: 34px; font-weight: 400; }
|
||||
.label { font-size: 29px; }
|
||||
.blue-label { font-size: 26px; }
|
||||
.small { font-size: 25px; }
|
||||
.box { fill: #fff; stroke: #303038; stroke-width: 3; }
|
||||
.group { fill: #fff; stroke: #303038; stroke-width: 3; }
|
||||
.model-group { fill: #fff; stroke: #303038; stroke-width: 3; stroke-dasharray: 14 12; }
|
||||
.line { fill: none; stroke: #303038; stroke-width: 3; marker-end: url(#arrow); }
|
||||
.blue-line { fill: none; stroke: #1478c9; stroke-width: 4; marker-end: url(#arrow-blue); }
|
||||
.blue { fill: #1478c9; }
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<rect width="1920" height="1295" fill="#fff"/>
|
||||
<text x="20" y="70" class="title">Multivariate Models Lifecycle</text>
|
||||
|
||||
<!-- VictoriaMetrics and query configuration -->
|
||||
<rect x="535" y="175" width="420" height="215" class="box"/>
|
||||
<text x="745" y="265" class="node" text-anchor="middle">VictoriaMetrics TSDB</text>
|
||||
<text x="745" y="310" class="node" text-anchor="middle">(Single-Node or Cluster)</text>
|
||||
<rect x="270" y="270" width="150" height="155" class="box"/>
|
||||
<text x="345" y="305" class="small" text-anchor="middle">Config.yml</text>
|
||||
<text x="285" y="360" class="label">MetricsQL</text>
|
||||
<text x="285" y="395" class="label">queries</text>
|
||||
<path d="M420 346 H535" class="line"/>
|
||||
|
||||
<!-- Reader and datasource exchange -->
|
||||
<rect x="580" y="500" width="310" height="100" class="box"/>
|
||||
<text x="735" y="562" class="node" text-anchor="middle">Reader</text>
|
||||
<path d="M580 545 H455 V365 H535" class="line"/>
|
||||
<text x="465" y="478" class="label">1. Request data</text>
|
||||
<path d="M955 270 H1040 V550 H890" class="line"/>
|
||||
<text x="810" y="478" class="label">2. Get metrics</text>
|
||||
|
||||
<!-- Historical fit data returned by the configured queries -->
|
||||
<g aria-label="Historical fit data">
|
||||
<rect x="1090" y="85" width="430" height="505" class="group"/>
|
||||
<rect x="1120" y="135" width="340" height="170" class="group"/>
|
||||
<text x="1140" y="180" class="node">Query 1</text>
|
||||
<rect x="1135" y="195" width="300" height="42" class="box"/>
|
||||
<text x="1150" y="228" class="label">Metric 1.1</text>
|
||||
<text x="1150" y="262" class="label">...</text>
|
||||
<rect x="1135" y="265" width="300" height="42" class="box"/>
|
||||
<text x="1150" y="298" class="label">Metric 1.M₁</text>
|
||||
<text x="1305" y="355" class="node" text-anchor="middle">...</text>
|
||||
<rect x="1120" y="390" width="340" height="170" class="group"/>
|
||||
<text x="1140" y="435" class="node">Query N</text>
|
||||
<rect x="1135" y="450" width="300" height="42" class="box"/>
|
||||
<text x="1150" y="483" class="label">Metric N.1</text>
|
||||
<text x="1150" y="517" class="label">...</text>
|
||||
<rect x="1135" y="520" width="300" height="42" class="box"/>
|
||||
<text x="1150" y="553" class="label">Metric N.Mₙ</text>
|
||||
</g>
|
||||
|
||||
<!-- One shared model is fitted on the complete aligned set -->
|
||||
<rect x="1215" y="795" width="310" height="115" class="box"/>
|
||||
<text x="1370" y="865" class="node" text-anchor="middle">Model</text>
|
||||
<rect x="1365" y="635" width="155" height="130" class="box"/>
|
||||
<text x="1443" y="675" class="small" text-anchor="middle">Config.yml</text>
|
||||
<text x="1382" y="723" class="label">Model</text>
|
||||
<text x="1382" y="757" class="label">config</text>
|
||||
<path d="M1305 590 V795" class="line"/>
|
||||
<text x="1055" y="672" class="label">
|
||||
<tspan x="1055" dy="0">3. Fit one model</tspan>
|
||||
<tspan x="1055" dy="38">on all historical series</tspan>
|
||||
</text>
|
||||
<path d="M1443 765 V795" class="line"/>
|
||||
|
||||
<!-- Inference data must contain the same set of series -->
|
||||
<g aria-label="Inference data">
|
||||
<rect x="25" y="615" width="430" height="500" class="group"/>
|
||||
<rect x="55" y="645" width="340" height="170" class="group"/>
|
||||
<text x="75" y="690" class="node">Query 1</text>
|
||||
<rect x="70" y="705" width="300" height="42" class="box"/>
|
||||
<text x="85" y="738" class="label">Metric 1.1</text>
|
||||
<text x="85" y="772" class="label">...</text>
|
||||
<rect x="70" y="775" width="300" height="42" class="box"/>
|
||||
<text x="85" y="808" class="label">Metric 1.M₁</text>
|
||||
<text x="225" y="870" class="node" text-anchor="middle">...</text>
|
||||
<rect x="55" y="900" width="340" height="175" class="group"/>
|
||||
<text x="75" y="945" class="node">Query N</text>
|
||||
<rect x="70" y="960" width="300" height="42" class="box"/>
|
||||
<text x="85" y="993" class="label">Metric N.1</text>
|
||||
<text x="85" y="1027" class="label">...</text>
|
||||
<rect x="70" y="1030" width="300" height="42" fill="#fff" stroke="#1478c9" stroke-width="4"/>
|
||||
<text x="85" y="1063" class="label blue">Metric N.Mₖ</text>
|
||||
</g>
|
||||
<path d="M580 575 H500 V650 H455" class="line"/>
|
||||
<text x="465" y="680" class="label">4. Provide inference data</text>
|
||||
|
||||
<!-- Single multivariate model registry -->
|
||||
<g aria-label="Multivariate model registry">
|
||||
<rect x="580" y="750" width="420" height="480" class="group"/>
|
||||
<text x="790" y="800" class="node" text-anchor="middle">Model registry</text>
|
||||
<rect x="610" y="835" width="360" height="220" class="model-group"/>
|
||||
<rect x="640" y="885" width="300" height="115" class="box"/>
|
||||
<text x="790" y="955" class="node" text-anchor="middle">Model (single)</text>
|
||||
</g>
|
||||
<path d="M455 1050 H580" class="line"/>
|
||||
<path d="M1215 852 H1000" class="line"/>
|
||||
|
||||
<!-- Joint output and mismatched-series skip path -->
|
||||
<rect x="1600" y="1015" width="285" height="105" class="box"/>
|
||||
<text x="1743" y="1080" class="node" text-anchor="middle">Writer</text>
|
||||
<rect x="1740" y="805" width="145" height="145" class="box"/>
|
||||
<text x="1813" y="845" class="small" text-anchor="middle">Config.yml</text>
|
||||
<text x="1758" y="900" class="label">Writer</text>
|
||||
<text x="1758" y="935" class="label">config</text>
|
||||
<path d="M1813 950 V1015" class="line"/>
|
||||
<path d="M1000 1040 H1600" class="line"/>
|
||||
<text x="1295" y="965" class="label" text-anchor="middle">
|
||||
<tspan x="1295" dy="0">5.a Produce one anomaly-score series</tspan>
|
||||
<tspan x="1295" dy="38">with the intersected label set</tspan>
|
||||
</text>
|
||||
<path d="M1000 1110 H1600" class="blue-line"/>
|
||||
<text x="1320" y="1145" class="blue-label blue" text-anchor="middle">
|
||||
<tspan x="1320" dy="0">5.b Skip inference when the fit and inference</tspan>
|
||||
<tspan x="1320" dy="32">series sets differ;</tspan>
|
||||
<tspan x="1320" dy="32">update the model_runs_skipped counter</tspan>
|
||||
</text>
|
||||
|
||||
<!-- Persist the joint anomaly score -->
|
||||
<path d="M1743 1015 V80 H745 V175" class="line"/>
|
||||
<text x="1170" y="55" class="label" text-anchor="middle">6. Write one anomaly-score series (label set = intersection)</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 66 KiB |
148
docs/anomaly-detection/components/model-lifecycle-univariate.svg
Normal file
@@ -0,0 +1,148 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1920 1295" role="img" aria-labelledby="title description">
|
||||
<title id="title">Univariate models lifecycle</title>
|
||||
<desc id="description">MetricsQL queries retrieve multiple series from VictoriaMetrics. Reader data fits one model per series in the model registry. Known series produce individual anomaly scores for Writer; an unseen series is skipped until a fitted model exists. Writer stores produced scores in VictoriaMetrics.</desc>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 12 12" refX="10.5" refY="6" markerWidth="16" markerHeight="16" markerUnits="userSpaceOnUse" orient="auto-start-reverse">
|
||||
<path d="M1.5 1.5 10.5 6 1.5 10.5" fill="none" stroke="#303038" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</marker>
|
||||
<marker id="arrow-blue" viewBox="0 0 12 12" refX="10.5" refY="6" markerWidth="16" markerHeight="16" markerUnits="userSpaceOnUse" orient="auto-start-reverse">
|
||||
<path d="M1.5 1.5 10.5 6 1.5 10.5" fill="none" stroke="#1478c9" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</marker>
|
||||
<style>
|
||||
text { font-family: Arial, Helvetica, sans-serif; fill: #303038; }
|
||||
.title { font-size: 50px; font-weight: 400; }
|
||||
.node { font-size: 34px; font-weight: 400; }
|
||||
.label { font-size: 29px; }
|
||||
.blue-label { font-size: 26px; }
|
||||
.small { font-size: 25px; }
|
||||
.box { fill: #fff; stroke: #303038; stroke-width: 3; }
|
||||
.group { fill: #fff; stroke: #303038; stroke-width: 3; }
|
||||
.model-group { fill: #fff; stroke: #303038; stroke-width: 3; stroke-dasharray: 14 12; }
|
||||
.line { fill: none; stroke: #303038; stroke-width: 3; marker-end: url(#arrow); }
|
||||
.blue-line { fill: none; stroke: #1478c9; stroke-width: 4; marker-end: url(#arrow-blue); }
|
||||
.blue { fill: #1478c9; }
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<rect width="1920" height="1295" fill="#fff"/>
|
||||
<text x="20" y="70" class="title">Univariate Models Lifecycle</text>
|
||||
|
||||
<!-- VictoriaMetrics and query configuration -->
|
||||
<rect x="535" y="175" width="420" height="215" class="box"/>
|
||||
<text x="745" y="265" class="node" text-anchor="middle">VictoriaMetrics TSDB</text>
|
||||
<text x="745" y="310" class="node" text-anchor="middle">(Single-Node or Cluster)</text>
|
||||
<rect x="270" y="270" width="150" height="155" class="box"/>
|
||||
<text x="345" y="305" class="small" text-anchor="middle">Config.yml</text>
|
||||
<text x="285" y="360" class="label">MetricsQL</text>
|
||||
<text x="285" y="395" class="label">queries</text>
|
||||
<path d="M420 346 H535" class="line"/>
|
||||
|
||||
<!-- Reader and datasource exchange -->
|
||||
<rect x="580" y="500" width="310" height="100" class="box"/>
|
||||
<text x="735" y="562" class="node" text-anchor="middle">Reader</text>
|
||||
<path d="M580 545 H455 V365 H535" class="line"/>
|
||||
<text x="465" y="478" class="label">1. Request data</text>
|
||||
<path d="M955 270 H1040 V550 H890" class="line"/>
|
||||
<text x="810" y="478" class="label">2. Get metrics</text>
|
||||
|
||||
<!-- Historical fit data returned by the configured queries -->
|
||||
<g aria-label="Historical fit data">
|
||||
<rect x="1090" y="85" width="430" height="505" class="group"/>
|
||||
<rect x="1120" y="135" width="340" height="170" class="group"/>
|
||||
<text x="1140" y="180" class="node">Query 1</text>
|
||||
<rect x="1135" y="195" width="300" height="42" class="box"/>
|
||||
<text x="1150" y="228" class="label">Metric 1.1</text>
|
||||
<text x="1150" y="262" class="label">...</text>
|
||||
<rect x="1135" y="265" width="300" height="42" class="box"/>
|
||||
<text x="1150" y="298" class="label">Metric 1.M₁</text>
|
||||
<text x="1305" y="355" class="node" text-anchor="middle">...</text>
|
||||
<rect x="1120" y="390" width="340" height="170" class="group"/>
|
||||
<text x="1140" y="435" class="node">Query N</text>
|
||||
<rect x="1135" y="450" width="300" height="42" class="box"/>
|
||||
<text x="1150" y="483" class="label">Metric N.1</text>
|
||||
<text x="1150" y="517" class="label">...</text>
|
||||
<rect x="1135" y="520" width="300" height="42" class="box"/>
|
||||
<text x="1150" y="553" class="label">Metric N.Mₙ</text>
|
||||
</g>
|
||||
|
||||
<!-- Model fitting -->
|
||||
<rect x="1215" y="795" width="310" height="115" class="box"/>
|
||||
<text x="1370" y="865" class="node" text-anchor="middle">Model</text>
|
||||
<rect x="1365" y="635" width="155" height="130" class="box"/>
|
||||
<text x="1443" y="675" class="small" text-anchor="middle">Config.yml</text>
|
||||
<text x="1382" y="723" class="label">Model</text>
|
||||
<text x="1382" y="757" class="label">config</text>
|
||||
<path d="M1305 590 V795" class="line"/>
|
||||
<text x="1055" y="672" class="label">
|
||||
<tspan x="1055" dy="0">3. Fit models</tspan>
|
||||
<tspan x="1055" dy="38">on historical data</tspan>
|
||||
</text>
|
||||
<path d="M1443 765 V795" class="line"/>
|
||||
|
||||
<!-- Inference data, including a new unseen series -->
|
||||
<g aria-label="Inference data">
|
||||
<rect x="25" y="615" width="430" height="500" class="group"/>
|
||||
<rect x="55" y="645" width="340" height="170" class="group"/>
|
||||
<text x="75" y="690" class="node">Query 1 (has fit model)</text>
|
||||
<rect x="70" y="705" width="300" height="42" class="box"/>
|
||||
<text x="85" y="738" class="label">Metric 1.1</text>
|
||||
<text x="85" y="772" class="label">...</text>
|
||||
<rect x="70" y="775" width="300" height="42" class="box"/>
|
||||
<text x="85" y="808" class="label">Metric 1.M₁</text>
|
||||
<text x="225" y="870" class="node" text-anchor="middle">...</text>
|
||||
<rect x="55" y="900" width="340" height="175" class="group"/>
|
||||
<text x="75" y="945" class="node">Query N</text>
|
||||
<rect x="70" y="960" width="300" height="42" class="box"/>
|
||||
<text x="85" y="993" class="label">Metric N.1</text>
|
||||
<text x="85" y="1027" class="label">...</text>
|
||||
<rect x="70" y="1030" width="300" height="42" fill="#fff" stroke="#1478c9" stroke-width="4"/>
|
||||
<text x="85" y="1063" class="label blue">Metric N.Mₖ</text>
|
||||
</g>
|
||||
<path d="M580 575 H500 V650 H455" class="line"/>
|
||||
<text x="465" y="680" class="label">4. Provide inference data</text>
|
||||
|
||||
<!-- One fitted model per known series -->
|
||||
<g aria-label="Univariate model registry">
|
||||
<rect x="580" y="750" width="420" height="480" class="group"/>
|
||||
<text x="790" y="800" class="node" text-anchor="middle">Model registry</text>
|
||||
<rect x="610" y="830" width="360" height="170" class="model-group"/>
|
||||
<rect x="630" y="865" width="300" height="42" class="box"/>
|
||||
<text x="645" y="898" class="label">Model 1.1</text>
|
||||
<text x="645" y="932" class="label">...</text>
|
||||
<rect x="630" y="935" width="300" height="42" class="box"/>
|
||||
<text x="645" y="968" class="label">Model 1.M₁</text>
|
||||
<text x="790" y="1045" class="node" text-anchor="middle">...</text>
|
||||
<rect x="610" y="1070" width="360" height="135" class="model-group"/>
|
||||
<rect x="630" y="1090" width="300" height="42" class="box"/>
|
||||
<text x="645" y="1123" class="label">Model N.1</text>
|
||||
<rect x="630" y="1145" width="300" height="42" class="box"/>
|
||||
<text x="645" y="1178" class="label">Model N.Mₙ</text>
|
||||
</g>
|
||||
<path d="M455 1050 H580" class="line"/>
|
||||
<path d="M1215 852 H1000" class="line"/>
|
||||
|
||||
<!-- Known-series output and unseen-series skip path -->
|
||||
<rect x="1600" y="1015" width="285" height="105" class="box"/>
|
||||
<text x="1743" y="1080" class="node" text-anchor="middle">Writer</text>
|
||||
<rect x="1740" y="805" width="145" height="145" class="box"/>
|
||||
<text x="1813" y="845" class="small" text-anchor="middle">Config.yml</text>
|
||||
<text x="1758" y="900" class="label">Writer</text>
|
||||
<text x="1758" y="935" class="label">config</text>
|
||||
<path d="M1813 950 V1015" class="line"/>
|
||||
<path d="M1000 1040 H1600" class="line"/>
|
||||
<text x="1275" y="975" class="label" text-anchor="middle">
|
||||
<tspan x="1275" dy="0">5.a Produce anomaly scores</tspan>
|
||||
<tspan x="1275" dy="38">for known series</tspan>
|
||||
</text>
|
||||
<path d="M1000 1110 H1600" class="blue-line"/>
|
||||
<text x="1320" y="1145" class="blue-label blue" text-anchor="middle">
|
||||
<tspan x="1320" dy="0">5.b Skip inference until a fitted model exists</tspan>
|
||||
<tspan x="1320" dy="32">for Metric N.Mₖ;</tspan>
|
||||
<tspan x="1320" dy="32">update the model_runs_skipped counter</tspan>
|
||||
</text>
|
||||
|
||||
<!-- Persist anomaly scores -->
|
||||
<path d="M1743 1015 V80 H745 V175" class="line"/>
|
||||
<text x="1130" y="55" class="label" text-anchor="middle">6. Write back produced anomaly scores</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 66 KiB |
@@ -27,16 +27,13 @@ This section covers the `Models` component of VictoriaMetrics Anomaly Detection
|
||||
```yaml
|
||||
models:
|
||||
model_univariate_1:
|
||||
class: 'zscore' # or 'model.zscore.ZscoreModel' until v1.13.0
|
||||
class: 'zscore_online'
|
||||
z_threshold: 2.5
|
||||
queries: ['query_alias2'] # referencing queries defined in `reader` section
|
||||
model_multivariate_1:
|
||||
class: 'isolation_forest_multivariate' # or model.isolation_forest.IsolationForestMultivariateModel until v1.13.0
|
||||
contamination: 'auto'
|
||||
args:
|
||||
n_estimators: 100
|
||||
# i.e. to assure reproducibility of produced results each time model is fit on the same input
|
||||
random_state: 42
|
||||
class: 'temporal_envelope_multivariate'
|
||||
seasonalities: ['hod_smooth', 'dow_smooth']
|
||||
provide_series: ['anomaly_score']
|
||||
# if there is no explicit `queries` arg, then the model will be run on ALL queries found in reader section
|
||||
# ...
|
||||
```
|
||||
@@ -68,6 +65,10 @@ models:
|
||||
|
||||
Common arguments supported by every model were introduced in [v1.10.0](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1100).
|
||||
|
||||
<div class="collapse-group">
|
||||
|
||||
{{% collapse name="Queries" %}}
|
||||
|
||||
### Queries
|
||||
|
||||
The `queries` argument selects the [reader queries](https://docs.victoriametrics.com/anomaly-detection/components/reader/#config-parameters) used to fit and run a particular model{{% available_from "v1.10.0" anomaly %}}. Every series returned by a selected query is passed to that model.
|
||||
@@ -93,6 +94,10 @@ models:
|
||||
queries: ['q1', 'q2', 'q3'] # i.e., if your `queries` in `reader` section has exactly q1, q2, q3 aliases
|
||||
```
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Schedulers" %}}
|
||||
|
||||
### Schedulers
|
||||
|
||||
The `schedulers` argument selects the [schedulers](https://docs.victoriametrics.com/anomaly-detection/components/scheduler/) that run a particular model{{% available_from "v1.11.0" anomaly %}}.
|
||||
@@ -118,6 +123,10 @@ models:
|
||||
schedulers: ['s1', 's2', 's3'] # i.e., if your `schedulers` section has exactly s1, s2, s3 aliases
|
||||
```
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Provide series" %}}
|
||||
|
||||
### Provide series
|
||||
|
||||
The `provide_series` argument{{% available_from "v1.12.0" anomaly %}} limits the [model output](#vmanomaly-output) sent to the writer. For example, a model may produce `['anomaly_score', 'yhat', 'yhat_lower', 'yhat_upper']` by default, while the following configuration writes only `anomaly_score` for each input series:
|
||||
@@ -131,6 +140,10 @@ models:
|
||||
|
||||
> If `provide_series` is not specified in model config, the model will produce its default [model-dependent output](#vmanomaly-output). The output can't be less than `['anomaly_score']`. Even if `timestamp` column is omitted, it will be implicitly added to `provide_series` list, as it's required for metrics to be properly written.
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Detection direction" %}}
|
||||
|
||||
### Detection direction
|
||||
The `detection_direction` argument{{% available_from "v1.13.0" anomaly %}} can reduce [false positives](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-1/#false-positive) when domain knowledge indicates that only values above or below the expected value are anomalous. Available values are `both`, `above_expected`, and `below_expected`.
|
||||
|
||||
@@ -190,6 +203,10 @@ reader:
|
||||
# other components like writer, schedule, monitoring
|
||||
```
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Minimal deviation from expected" %}}
|
||||
|
||||
### Minimal deviation from expected
|
||||
|
||||
`min_dev_from_expected`{{% available_from "v1.13.0" anomaly %}} argument is designed to **reduce [false positives](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-1/#false-positive)** in scenarios where deviations between the actual value (`y`) and the expected value (`yhat`) are **relatively** high. Such deviations can cause models to generate high [anomaly scores](https://docs.victoriametrics.com/anomaly-detection/faq/#what-is-anomaly-score). However, these deviations may not be significant enough in **absolute values** from a business perspective to be considered anomalies. This parameter ensures that anomaly scores for data points where `|y - yhat| < min_dev_from_expected` are explicitly set to 0. By default, if this parameter is not set, it is set to `0` to maintain backward compatibility.
|
||||
@@ -238,6 +255,10 @@ models:
|
||||
queries: ['normal_behavior'] # use the default where it's not needed
|
||||
```
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Minimal relative deviation from expected" %}}
|
||||
|
||||
### Minimal relative deviation from expected
|
||||
|
||||
{{% available_from "v1.29.1" anomaly %}} `min_rel_dev_from_expected` argument serves a similar purpose to `min_dev_from_expected` (see [section above](#minimal-deviation-from-expected)), but focuses on **relative deviations** rather than absolute ones. It is designed to reduce [false positives](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-1/#false-positive) in scenarios where the relative deviation between the actual value (`y`) and the expected value (`yhat`) is high, but the absolute deviation is not significant enough to be considered an anomaly from a business perspective. This parameter ensures that anomaly scores for data points where `|y - yhat| / |yhat| < min_rel_dev_from_expected` are explicitly set to 0. By default, if this parameter is not set, it is set to `0` to maintain backward compatibility.
|
||||
@@ -278,6 +299,10 @@ models:
|
||||
```
|
||||
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Group by" %}}
|
||||
|
||||
### Group by
|
||||
|
||||
> The `groupby` argument works only in combination with [multivariate models](#multivariate-models).
|
||||
@@ -306,9 +331,10 @@ reader:
|
||||
+ sum(rate(node_network_transmit_bytes_total[5m])) by (host)
|
||||
|
||||
models:
|
||||
iforest: # alias for the model
|
||||
class: isolation_forest_multivariate
|
||||
contamination: 0.01
|
||||
envelope: # alias for the model
|
||||
class: temporal_envelope_multivariate
|
||||
seasonalities: [hod_smooth, dow_smooth]
|
||||
provide_series: [anomaly_score]
|
||||
# the multivariate model can be trained on 2+ timeseries returned by 1+ queries
|
||||
queries: [cpu, ram, network]
|
||||
# train a distinct multivariate model for each unique value found in the `host` label
|
||||
@@ -316,6 +342,10 @@ models:
|
||||
groupby: [host]
|
||||
```
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Scale" %}}
|
||||
|
||||
### Scale
|
||||
|
||||
Previously available only to [ProphetModel](#prophet) and [OnlineQuantileModel](#online-seasonal-quantile), the `scale` {{% available_from "v1.20.0" anomaly %}} parameter is now applicable to all models that support generating predictions (`yhat`, `yhat_lower`, `yhat_upper`). Also, it is **two-sided** now, represented as a list of two positive float values, allowing separate scaling for the intervals `[yhat, yhat_upper]` and `[yhat_lower, yhat]`. The new margins are calculated as:
|
||||
@@ -346,6 +376,10 @@ models:
|
||||
scale: [1.2, 0.75]
|
||||
```
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Clip predictions" %}}
|
||||
|
||||
### Clip predictions
|
||||
|
||||
A post-processing step to **clip model predictions** (`yhat`, `yhat_lower`, and `yhat_upper` series) to the configured [`data_range` values](https://docs.victoriametrics.com/anomaly-detection/components/reader/#config-parameters) in `VmReader` is available.
|
||||
@@ -400,6 +434,10 @@ models:
|
||||
]
|
||||
```
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Score outside data range" %}}
|
||||
|
||||
### Score outside data range
|
||||
|
||||
The `anomaly_score_outside_data_range` {{% available_from "v1.20.0" anomaly %}} parameter allows overriding the default **anomaly score (`1.01`)** assigned when actual values (`y`) fall **outside the defined `data_range` if defined in [reader](https://docs.victoriametrics.com/anomaly-detection/components/reader/)**. This provides greater flexibility for **alerting rule configurations** and enables **clearer visual differentiation** between different types of anomalies:
|
||||
@@ -445,6 +483,10 @@ models:
|
||||
anomaly_score_outside_data_range: 3.0
|
||||
```
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Decay" %}}
|
||||
|
||||
### Decay
|
||||
|
||||
> The `decay` argument works only in combination with [online models](#online-models) like [`ZScoreOnlineModel`](#online-z-score) or [`OnlineQuantileModel`](#online-seasonal-quantile).
|
||||
@@ -477,6 +519,10 @@ models:
|
||||
queries: ['q1']
|
||||
```
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
## Model types
|
||||
|
||||
@@ -500,9 +546,9 @@ If during an inference, you got a series having **new labelset** (not present in
|
||||
|
||||
**Implications:** Univariate models are a go-to default, when your queries returns **changing** amount of **individual** time series of **different** magnitude, [trend](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-1/#trend) or [seasonality](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-1/#seasonality), so you won't be mixing incompatible data with different behavior within a single fit model (context isolation).
|
||||
|
||||
**Examples:** [Prophet](#prophet), [Holt-Winters](#holt-winters)
|
||||
**Examples:** [Temporal Envelope](#temporal-envelope), [Online MAD](#online-mad), [Online Z-score](#online-z-score), [Online Seasonal Quantile](#online-seasonal-quantile)
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
### Multivariate Models
|
||||
@@ -517,9 +563,24 @@ If during an inference, you got a **different amount of series** or some series
|
||||
|
||||
**Implications:** Multivariate models are a go-to default, when your queries returns **fixed** amount of **individual** time series (say, some aggregations), to be used for adding cross-series (and cross-query) context, useful for catching [collective anomalies](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-2/#collective-anomalies) or [novelties](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-2/#novelties) (expanded to multi-input scenario). For example, you may set it up for anomaly detection of CPU usage in different modes (`idle`, `user`, `system`, etc.) and use its cross-dependencies to detect **unseen (in fit data)** behavior.
|
||||
|
||||
**Examples:** [IsolationForest](#isolation-forest-multivariate)
|
||||
**Recommended:** [Temporal Envelope](#temporal-envelope). Existing [Isolation Forest](#isolation-forest-multivariate) configurations can migrate to its multivariate form.
|
||||
|
||||

|
||||

|
||||
|
||||
The following configuration applies a multivariate Temporal Envelope model to the same aligned input series:
|
||||
|
||||
```yaml
|
||||
models:
|
||||
service_dependency_envelope:
|
||||
class: temporal_envelope_multivariate
|
||||
queries: [request_rate, error_rate, latency]
|
||||
groupby: [cluster]
|
||||
dependency_rank: 8
|
||||
score_aggregation: l2
|
||||
seasonalities: [hod_smooth, dow_smooth]
|
||||
provide_series: [anomaly_score]
|
||||
|
||||
```
|
||||
|
||||
|
||||
### Online Models
|
||||
@@ -561,6 +622,9 @@ Each of the ([built-in](#built-in-models) or [custom](#custom-model-guide)) onli
|
||||
|
||||
Every other model that isn't [online](#online-models). Offline models are completely re-trained during `fit` call and aren't updated during consecutive `infer` calls.
|
||||
|
||||
> [!NOTE]
|
||||
> Built-in offline model classes are planned for deprecation in a future release in favor of online counterparts. For complex temporal data, prefer [Temporal Envelope](#temporal-envelope), which supports incremental adaptation, forecasting, and both univariate and multivariate operation.
|
||||
|
||||
|
||||
## Built-in Models
|
||||
|
||||
@@ -576,14 +640,14 @@ Built-in models support 2 groups of arguments:
|
||||
**Models**:
|
||||
- [AutoTuned](#autotuned) - designed to take the cognitive load off the user, allowing any of built-in models below to be re-tuned for best hyperparameters on data seen during each `fit` phase of the algorithm. Tradeoff is between increased computational time and optimized results / simpler maintenance.
|
||||
- [Temporal Envelope](#temporal-envelope) - the preferred **online model for complex operational data** with trends, changepoints, multiple calendar patterns, holidays, capable of [forecasting](https://docs.victoriametrics.com/anomaly-detection/faq/#forecasting). Its multivariate form also learns cross-series relationships.
|
||||
- [Prophet](#prophet) - an offline forecasting alternative when Prophet-specific decomposition outputs are required. Favor `Temporal Envelope` for online adaptation and multivariate support.
|
||||
- [Online Z-score](#online-z-score) - useful for initial testing and for simpler data ([de-trended](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-1/#trend) data without strict [seasonality](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-1/#seasonality) and with anomalies of similar magnitude as your "normal" data)
|
||||
- [MAD](#online-mad) - similarly to [Z-score](#online-z-score), is effective for **identifying outliers in relatively consistent data**. Useful for detecting sudden, stark deviations from the median, being less prone to outlier's magnitude than z-score.
|
||||
- [Rolling Quantile](#rolling-quantile) - best for **data with evolving patterns**, as it adapts to changes over a rolling window.
|
||||
- [Online Seasonal Quantile](#online-seasonal-quantile) - best used on **[de-trended](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-1/#trend) data with strong (possibly multiple) [seasonalities](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-1/#seasonality)**. Can act as a (slightly less powerful) [online](#online-models) replacement to [`ProphetModel`](#prophet).
|
||||
- [Seasonal Trend Decomposition](#seasonal-trend-decomposition) - similarly to Holt-Winters, is best for **data with pronounced [seasonal](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-1/#seasonality) and [trend](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-1/#trend) components**
|
||||
- [Isolation forest (Multivariate)](#isolation-forest-multivariate) - an offline alternative for **metrics data interaction** (several queries/metrics -> single anomaly score) and high-dimensional feature-space outliers. Prefer multivariate Temporal Envelope when temporal profiles and *online* adaptation matter.
|
||||
- [Holt-Winters](#holt-winters) - well-suited for **data with moderate complexity**, exhibiting distinct [trends](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-1/#trend) and/or [single seasonal pattern](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-1/#seasonality).
|
||||
- [Prophet](#prophet) - an offline model retained for existing deployments. Migrate forecasting and seasonal anomaly-detection configurations to [Temporal Envelope](#temporal-envelope), unless Prophet-specific decomposition output must be preserved.
|
||||
- [Isolation forest (Multivariate)](#isolation-forest-multivariate) - an offline model retained for existing univariate and multivariate deployments. Migrate to the corresponding [Temporal Envelope](#temporal-envelope) form for online adaptation and temporal or cross-series context.
|
||||
- [Holt-Winters](#holt-winters) - an offline model retained for existing trend and single-seasonality configurations. Migrate these configurations to [Temporal Envelope](#temporal-envelope).
|
||||
- [Custom model](#custom-model-guide) - benefit from your own models and expertise to better support your **unique use case**.
|
||||
|
||||
|
||||
@@ -664,7 +728,7 @@ models:
|
||||
|
||||
</div>
|
||||
|
||||

|
||||

|
||||
|
||||
#### Shared asynchronous autotune workflow
|
||||
|
||||
@@ -712,6 +776,8 @@ The requested anomaly percentage is treated as an alert-volume constraint rather
|
||||
|
||||
{{% available_from "v1.30.0" anomaly %}} Temporal Envelope is the preferred online model for complex operational and business metrics. It learns an evolving expected range from robust trend, calendar and holiday patterns, persistent level shifts, uncertainty, and optional future forecasts. The model adapts during inference while limiting the lasting influence of short-lived spikes.
|
||||
|
||||
{{% available_from "v1.30.1" anomaly %}} When the fit window ends in a recently established level, the model initializes its adaptive state from that recent regime while preserving supported calendar structure. This improves the first periodic predictions after a level shift and reduces false seasonal oscillation without requiring additional configuration.
|
||||
|
||||
> `TemporalEnvelopeModel` is [univariate](#univariate-models) and [online](#online-models). `TemporalEnvelopeMultivariateModel` also learns normal cross-series relationships as a [multivariate](#multivariate-models) model.
|
||||
|
||||
Use it for:
|
||||
@@ -720,7 +786,7 @@ Use it for:
|
||||
- deployments, traffic migrations, and capacity changes that create persistent shifts, including short-horizon forecasts through `forecast_at`;
|
||||
- aligned related metrics where each channel keeps its own temporal pattern while their joint behavior contributes to one anomaly score.
|
||||
|
||||
For simple profiles without strong trend or seasonality, prefer [Online MAD](#online-mad) or [Online Z-score](#online-z-score). [Prophet](#prophet) and [Isolation Forest](#isolation-forest-multivariate) remain offline alternatives when their distinct capabilities are required or validation favors them.
|
||||
For simple profiles without strong trend or seasonality, prefer [Online MAD](#online-mad) or [Online Z-score](#online-z-score). Existing [Prophet](#prophet) and [Isolation Forest](#isolation-forest-multivariate) configurations can be migrated to the corresponding univariate or multivariate Temporal Envelope form.
|
||||
|
||||
<div class="model-details">
|
||||
|
||||
@@ -800,11 +866,224 @@ For independent per-series detection, use `temporal_envelope`. Use `temporal_env
|
||||
|
||||
</div>
|
||||
|
||||
### Online MAD
|
||||
|
||||
> `OnlineMADModel` is a [univariate](#univariate-models), [online](#online-models) model.
|
||||
|
||||
The MAD model is a robust method for anomaly detection that is *less sensitive* to outliers in data compared to standard deviation-based models. It considers a point as an anomaly if the absolute deviation from the median is significantly large. This is the online approximate version, based on [t-digests](https://www.sciencedirect.com/science/article/pii/S2665963820300403) for online quantile estimation{{% available_from "v1.15.0" anomaly %}}.
|
||||
|
||||
<div class="model-details">
|
||||
|
||||
{{% collapse name="Model-specific arguments" %}}
|
||||
|
||||
- `class` (string) - model class name `"model.online.OnlineMADModel"` (or `mad_online` with class alias support{{% available_from "v1.13.0" anomaly %}})
|
||||
- `threshold` (float, optional) - The threshold multiplier for the MAD to determine anomalies. Defaults to `2.5`. Higher values will identify fewer points as anomalies.
|
||||
- `min_n_samples_seen` (int, optional) - the minimum number of samples to be seen (`n_samples_seen_` property) before computing the anomaly score. Otherwise, the **anomaly score will be 0**, as there is not enough data to trust the model's predictions. Defaults to 16.
|
||||
- `history_strength` (float, optional) - {{% available_from "v1.30.0" anomaly %}} strength of the initial history learned by `fit`. Values above `1` preserve fitted quantiles initially but reduce the leverage of subsequent updates. Defaults to `1`.
|
||||
- `compression` (int, optional) - the compression parameter for underlying [t-digest](https://www.sciencedirect.com/science/article/pii/S2665963820300403). Higher values mean higher accuracy but higher memory usage. By default 100.
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Configuration example" %}}
|
||||
|
||||
|
||||
```yaml
|
||||
models:
|
||||
your_desired_alias_for_a_model:
|
||||
class: "mad_online" # or 'model.online.OnlineMADModel'
|
||||
threshold: 2.5
|
||||
min_n_samples_seen: 128 # i.e. calculate it as full seasonality / data freq
|
||||
history_strength: 2 # retain fitted history as a stronger prior
|
||||
compression: 100 # higher values mean higher accuracy but higher memory usage
|
||||
provide_series: ['anomaly_score', 'yhat'] # common arg example
|
||||
# Common arguments for built-in model, if not set, default to
|
||||
# See https://docs.victoriametrics.com/anomaly-detection/components/models/#common-args
|
||||
#
|
||||
# provide_series: ['anomaly_score', 'yhat', 'yhat_lower', 'yhat_upper']
|
||||
# schedulers: [all scheduler aliases defined in `scheduler` section]
|
||||
# queries: [all query aliases defined in `reader.queries` section]
|
||||
# detection_direction: 'both' # meaning both drops and spikes will be captured
|
||||
# min_dev_from_expected: [0.0, 0.0] # meaning, no minimal threshold is applied to prevent smaller anomalies
|
||||
# scale: [1.0, 1.0] # if needed, prediction intervals' width can be increased (>1) or narrowed (<1)
|
||||
# clip_predictions: False # if data_range for respective `queries` is set in reader, `yhat.*` columns will be clipped
|
||||
# anomaly_score_outside_data_range: 1.01 # auto anomaly score (1.01) if `y` (real value) is outside of data_range, if set
|
||||
```
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
</div>
|
||||
|
||||
Resulting metrics of the model are described [here](#vmanomaly-output).
|
||||
|
||||
|
||||
### Online Seasonal Quantile
|
||||
|
||||
> `OnlineQuantileModel` is a [univariate](#univariate-models), [online](#online-models) model.
|
||||
|
||||
Online (seasonal) quantile utilizes a set of approximate distributions, based on [t-digests](https://www.sciencedirect.com/science/article/pii/S2665963820300403) for online quantile estimation {{% available_from "v1.15.0" anomaly %}}.
|
||||
|
||||
Best used on **[de-trended](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-1/#trend) data with strong (potentially multiple) [seasonalities](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-1/#seasonality)**. Can act as a (slightly less flexible) replacement to [`ProphetModel`](#prophet).
|
||||
|
||||
It uses the `quantiles` triplet to calculate `yhat_lower`, `yhat`, and `yhat_upper` [output](#vmanomaly-output), respectively, for each of the `min_subseason` sub-intervals contained in `seasonal_interval`. For example, with '4d' + '2h' seasonality patterns (multiple), it will hold and update 24*4 / 2 = 48 consecutive estimates (each 2 hours long).
|
||||
|
||||
<div class="model-details">
|
||||
|
||||
{{% collapse name="Model-specific arguments" %}}
|
||||
|
||||
- `class` (string) - model class name `"model.online.OnlineQuantileModel"` (or `quantile_online` with class alias support{{% available_from "v1.13.0" anomaly %}})
|
||||
- `quantiles` (list[float], optional) - The quantiles to estimate. `yhat_lower`, `yhat`, `yhat_upper` are the quantile order. By default (0.01, 0.5, 0.99).
|
||||
- `iqr_threshold` (float, optional) - {{% available_from "v1.25.0" anomaly %}} The [interquartile range (IQR)](https://en.wikipedia.org/wiki/Interquartile_range) multiplier to increase the width of the prediction intervals. Defaults to 0 (no adjustment) for backward compatibility. If set > 0, the model will add IQR * `iqr_threshold` to `yhat_lower` and `yhat_upper` (respecting `min_subseason` seasonal buckets). This is useful for data with high variance or outliers, as it helps to avoid false positives in anomaly detection. Best used with **robust** `quantiles` set to (0.25, 0.5, 0.75) or similar.
|
||||
- `seasonal_interval` (string, optional) - the interval for the seasonal adjustment. If not set, the model will equal to a simple online quantile model. By default not set.
|
||||
- `min_subseason` (str, optional) - the minimum interval to estimate quantiles for. By default not set. Note that the minimum interval should be a multiple of the seasonal interval, i.e. if seasonal_interval='2h', then min_subseason='15m' is valid, but '37m' is not.
|
||||
- `use_transform` (bool, optional) - whether to internally apply a `log1p(abs(x)) * sign(x)` transformation to the data to stabilize internal quantile estimation. Does not affect the scale of produced output (i.e. `yhat`) By default False.
|
||||
- `global_smoothing` (float, optional) - the smoothing parameter for the global quantiles. i.e. the output is a weighted average of the global and seasonal quantiles (if `seasonal_interval` and `min_subseason` args are set). Should be from `[0, 1]` interval, where 0 means no smoothing and 1 means using only global quantile values.
|
||||
- `scale` (float, optional) - Is used to adjust the margins between `yhat` and [`yhat_lower`, `yhat_upper`]. New margin = `|yhat_* - yhat_lower| * scale`. Defaults to 1 (no scaling is applied). See `scale`[common arg](https://docs.victoriametrics.com/anomaly-detection/components/models/#scale) section for detailed instructions and 2-sided option.
|
||||
- `season_starts_from` (str, optional) - the start date for the seasonal adjustment, as a reference point to start counting the intervals. By default '1970-01-01'.
|
||||
- `min_n_samples_seen` (int, optional) - the minimum number of samples to be seen (`n_samples_seen_` property) before computing the anomaly score. Otherwise, the **anomaly score will be 0**, as there is not enough data to trust the model's predictions. Defaults to 16.
|
||||
- `history_strength` (float, optional) - {{% available_from "v1.30.0" anomaly %}} strength of the initial history learned by `fit`. Values above `1` preserve fitted quantiles initially but reduce the leverage of subsequent updates. Defaults to `1`.
|
||||
- `compression` (int, optional) - the compression parameter for the underlying [t-digests](https://www.sciencedirect.com/science/article/pii/S2665963820300403). Higher values mean higher accuracy but higher memory usage. By default 100.
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Configuration example" %}}
|
||||
|
||||
Suppose we have a data with strong intra-day (hourly) and intra-week (daily) seasonality, data granularity is '5m' with up to 5% expected outliers present in data. Then you can apply similar config:
|
||||
|
||||
```yaml
|
||||
models:
|
||||
your_desired_alias_for_a_model:
|
||||
class: "quantile_online" # or 'model.online.OnlineQuantileModel'
|
||||
quantiles: [0.25, 0.5, 0.75] # lowered to exclude anomalous edges, can be compensated by `scale` param > 1 and `iqr_threshold` > 0
|
||||
iqr_threshold: 2.5 # to increase prediction intervals' width to avoid false positives while still keeping the model robust
|
||||
seasonal_interval: '7d' # longest seasonality (week, day) = week, starting from `season_starts_from`
|
||||
min_subseason: '1h' # smallest seasonality (week, day, hour) = hour, will have its own quantile estimates
|
||||
min_n_samples_seen: 288 # 1440 / 5 - at least 1 full day, ideal = 1440 / 5 * 7 - one full week (seasonal_interval)
|
||||
history_strength: 2 # retain fitted history as a stronger prior
|
||||
scale: 1.1 # to compensate lowered quantile boundaries with wider intervals
|
||||
season_starts_from: '2024-01-01' # interval calculation starting point, especially for uncommon seasonalities like '36h' or '12d'
|
||||
compression: 100 # higher values mean higher accuracy but higher memory usage
|
||||
provide_series: ['anomaly_score', 'yhat'] # common arg example
|
||||
# Common arguments for built-in model, if not set, default to
|
||||
# See https://docs.victoriametrics.com/anomaly-detection/components/models/#common-args
|
||||
#
|
||||
# provide_series: ['anomaly_score', 'yhat', 'yhat_lower', 'yhat_upper']
|
||||
# schedulers: [all scheduler aliases defined in `scheduler` section]
|
||||
# queries: [all query aliases defined in `reader.queries` section]
|
||||
# detection_direction: 'both' # meaning both drops and spikes will be captured
|
||||
# min_dev_from_expected: [0.0, 0.0] # meaning, no minimal threshold is applied to prevent smaller anomalies
|
||||
# scale: [1.0, 1.0] # if needed, prediction intervals' width can be increased (>1) or narrowed (<1)
|
||||
# clip_predictions: False # if data_range for respective `queries` is set in reader, `yhat.*` columns will be clipped
|
||||
# anomaly_score_outside_data_range: 1.01 # auto anomaly score (1.01) if `y` (real value) is outside of data_range, if set
|
||||
```
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
</div>
|
||||
|
||||
Resulting metrics of the model are described [here](#vmanomaly-output).
|
||||
|
||||
|
||||
### Online Z-score
|
||||
|
||||
> `OnlineZscoreModel` is a [univariate](#univariate-models), [online](#online-models) model.
|
||||
|
||||
Online version of existing [Z-score](#z-score) implementation with the same exact behavior and implications {{% available_from "v1.15.0" anomaly %}}.
|
||||
|
||||
<div class="model-details">
|
||||
|
||||
{{% collapse name="Model-specific arguments" %}}
|
||||
|
||||
- `class` (string) - model class name `"model.online.OnlineZscoreModel"` (or `zscore_online`with class alias support{{% available_from "v1.13.0" anomaly %}})
|
||||
- `z_threshold` (float, optional) - [standard score](https://en.wikipedia.org/wiki/Standard_score) for calculation boundaries and anomaly score. Defaults to `2.5`.
|
||||
- `min_n_samples_seen` (int, optional) - the minimum number of samples to be seen (`n_samples_seen_` property) before computing the anomaly score. Otherwise, the **anomaly score will be 0**, as there is not enough data to trust the model's predictions. Defaults to 16.
|
||||
- `history_strength` (float, optional) - {{% available_from "v1.30.0" anomaly %}} strength of the initial history learned by `fit`. Values above `1` keep fitted mean and variance unchanged initially but reduce the leverage of subsequent updates. Defaults to `1`.
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Configuration example" %}}
|
||||
|
||||
```yaml
|
||||
models:
|
||||
your_desired_alias_for_a_model:
|
||||
class: "zscore_online" # or 'model.online.OnlineZscoreModel'
|
||||
z_threshold: 3.5
|
||||
min_n_samples_seen: 128 # i.e. calculate it as full seasonality / data freq
|
||||
history_strength: 2 # retain fitted history as a stronger prior
|
||||
provide_series: ['anomaly_score', 'yhat'] # common arg example
|
||||
# Common arguments for built-in model, if not set, default to
|
||||
# See https://docs.victoriametrics.com/anomaly-detection/components/models/#common-args
|
||||
#
|
||||
# provide_series: ['anomaly_score', 'yhat', 'yhat_lower', 'yhat_upper']
|
||||
# schedulers: [all scheduler aliases defined in `scheduler` section]
|
||||
# queries: [all query aliases defined in `reader.queries` section]
|
||||
# detection_direction: 'both' # meaning both drops and spikes will be captured
|
||||
# min_dev_from_expected: [0.0, 0.0] # meaning, no minimal threshold is applied to prevent smaller anomalies
|
||||
# scale: [1.0, 1.0] # if needed, prediction intervals' width can be increased (>1) or narrowed (<1)
|
||||
# clip_predictions: False # if data_range for respective `queries` is set in reader, `yhat.*` columns will be clipped
|
||||
# anomaly_score_outside_data_range: 1.01 # auto anomaly score (1.01) if `y` (real value) is outside of data_range, if set
|
||||
```
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
</div>
|
||||
|
||||
Resulting metrics of the model are described [here](#vmanomaly-output).
|
||||
|
||||
|
||||
### [Rolling Quantile](https://en.wikipedia.org/wiki/Quantile)
|
||||
|
||||
> `RollingQuantileModel` **is** {{% available_from "v1.28.0" anomaly %}} a [univariate](#univariate-models), [online](#online-models) model. It **was** {{% deprecated_from "v1.28.0" anomaly %}} a [univariate](#univariate-models), [rolling](#rolling-models), [offline](#offline-models) model.
|
||||
|
||||
This model is best used on **data with short evolving patterns** (i.e. 10-100 datapoints of particular frequency), as it adapts to changes over a rolling window.
|
||||
|
||||
<div class="model-details">
|
||||
|
||||
{{% collapse name="Model-specific arguments" %}}
|
||||
|
||||
- `class` (string) - model class name `"model.rolling_quantile.RollingQuantileModel"` (or `rolling_quantile` with class alias support {{% available_from "v1.13.0" anomaly %}})
|
||||
- `quantile` (float) - quantile value, from 0.5 to 1.0. This constraint is implied by 2-sided confidence interval.
|
||||
- `window_steps` (integer) - size of the moving window. (see 'sampling_period')
|
||||
- `iqr_threshold` (float, optional) - {{% available_from "v1.25.0" anomaly %}} The [interquartile range (IQR)](https://en.wikipedia.org/wiki/Interquartile_range) multiplier to increase the width of the prediction intervals. Defaults to 0 (no adjustment) for backward compatibility. If set > 0, the model will add half IQR * `iqr_threshold` to `yhat_lower` and `yhat_upper`. This is useful for data with high variance or outliers, as it helps to avoid false positives in anomaly detection.
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Configuration example" %}}
|
||||
|
||||
```yaml
|
||||
models:
|
||||
your_desired_alias_for_a_model:
|
||||
class: "rolling_quantile"
|
||||
quantile: 0.9
|
||||
window_steps: 96
|
||||
iqr_threshold: 1
|
||||
# Common arguments for built-in model, if not set, default to
|
||||
# See https://docs.victoriametrics.com/anomaly-detection/components/models/#common-args
|
||||
#
|
||||
# provide_series: ['anomaly_score', 'yhat', 'yhat_lower', 'yhat_upper']
|
||||
# schedulers: [all scheduler aliases defined in `scheduler` section]
|
||||
# queries: [all query aliases defined in `reader.queries` section]
|
||||
# detection_direction: 'both' # meaning both drops and spikes will be captured
|
||||
# min_dev_from_expected: [0.0, 0.0] # meaning, no minimal threshold is applied to prevent smaller anomalies
|
||||
# scale: [1.0, 1.0] # if needed, prediction intervals' width can be increased (>1) or narrowed (<1)
|
||||
# clip_predictions: False # if data_range for respective `queries` is set in reader, `yhat.*` columns will be clipped
|
||||
# anomaly_score_outside_data_range: 1.01 # auto anomaly score (1.01) if `y` (real value) is outside of data_range, if set
|
||||
```
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
</div>
|
||||
|
||||
Resulting metrics of the model are described [here](#vmanomaly-output).
|
||||
|
||||
|
||||
### [Prophet](https://facebook.github.io/prophet/)
|
||||
`vmanomaly` uses the Facebook Prophet implementation for time series forecasting, with detailed usage provided in the [Prophet library documentation](https://facebook.github.io/prophet/docs/quick_start#python-api). All original Prophet parameters are supported and can be directly passed to the model via `args` argument.
|
||||
|
||||
> `ProphetModel` is a [univariate](#univariate-models), [offline](#offline-models) model.
|
||||
|
||||
> [!NOTE]
|
||||
> Prophet is planned for deprecation in a future release. For new forecasting and anomaly-detection deployments, prefer the online [Temporal Envelope](#temporal-envelope) model unless Prophet-specific decomposition output is required.
|
||||
|
||||
> {{% available_from "v1.25.3" anomaly %}} Producing forecasts for future timestamps is now supported. To enable this, set the `forecast_at` argument to a list of relative future offsets (e.g., `['1h', '1d']`). The model will then generate forecasts for these future timestamps, which can be useful for planning and resource allocation. Output series are affected by [provide_series](#provide-series) argument, which need to include at least `yhat` for point-wise forecasts (and `yhat_lower` or/and `yhat_upper` for respective confidence intervals). See the example below for more details.
|
||||
|
||||
<div class="model-details">
|
||||
@@ -920,271 +1199,15 @@ Depending on chosen `seasonality` parameter FB Prophet can return additional met
|
||||
|
||||
Resulting metrics of the model are described [here](#vmanomaly-output)
|
||||
|
||||
### Online Z-score
|
||||
|
||||
> `OnlineZscoreModel` is a [univariate](#univariate-models), [online](#online-models) model.
|
||||
|
||||
Online version of existing [Z-score](#z-score) implementation with the same exact behavior and implications {{% available_from "v1.15.0" anomaly %}}.
|
||||
|
||||
<div class="model-details">
|
||||
|
||||
{{% collapse name="Model-specific arguments" %}}
|
||||
|
||||
- `class` (string) - model class name `"model.online.OnlineZscoreModel"` (or `zscore_online`with class alias support{{% available_from "v1.13.0" anomaly %}})
|
||||
- `z_threshold` (float, optional) - [standard score](https://en.wikipedia.org/wiki/Standard_score) for calculation boundaries and anomaly score. Defaults to `2.5`.
|
||||
- `min_n_samples_seen` (int, optional) - the minimum number of samples to be seen (`n_samples_seen_` property) before computing the anomaly score. Otherwise, the **anomaly score will be 0**, as there is not enough data to trust the model's predictions. Defaults to 16.
|
||||
- `history_strength` (float, optional) - {{% available_from "v1.30.0" anomaly %}} strength of the initial history learned by `fit`. Values above `1` keep fitted mean and variance unchanged initially but reduce the leverage of subsequent updates. Defaults to `1`.
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Configuration example" %}}
|
||||
|
||||
```yaml
|
||||
models:
|
||||
your_desired_alias_for_a_model:
|
||||
class: "zscore_online" # or 'model.online.OnlineZscoreModel'
|
||||
z_threshold: 3.5
|
||||
min_n_samples_seen: 128 # i.e. calculate it as full seasonality / data freq
|
||||
history_strength: 2 # retain fitted history as a stronger prior
|
||||
provide_series: ['anomaly_score', 'yhat'] # common arg example
|
||||
# Common arguments for built-in model, if not set, default to
|
||||
# See https://docs.victoriametrics.com/anomaly-detection/components/models/#common-args
|
||||
#
|
||||
# provide_series: ['anomaly_score', 'yhat', 'yhat_lower', 'yhat_upper']
|
||||
# schedulers: [all scheduler aliases defined in `scheduler` section]
|
||||
# queries: [all query aliases defined in `reader.queries` section]
|
||||
# detection_direction: 'both' # meaning both drops and spikes will be captured
|
||||
# min_dev_from_expected: [0.0, 0.0] # meaning, no minimal threshold is applied to prevent smaller anomalies
|
||||
# scale: [1.0, 1.0] # if needed, prediction intervals' width can be increased (>1) or narrowed (<1)
|
||||
# clip_predictions: False # if data_range for respective `queries` is set in reader, `yhat.*` columns will be clipped
|
||||
# anomaly_score_outside_data_range: 1.01 # auto anomaly score (1.01) if `y` (real value) is outside of data_range, if set
|
||||
```
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
</div>
|
||||
|
||||
Resulting metrics of the model are described [here](#vmanomaly-output).
|
||||
|
||||
|
||||
### Online MAD
|
||||
|
||||
> `OnlineMADModel` is a [univariate](#univariate-models), [online](#online-models) model.
|
||||
|
||||
The MAD model is a robust method for anomaly detection that is *less sensitive* to outliers in data compared to standard deviation-based models. It considers a point as an anomaly if the absolute deviation from the median is significantly large. This is the online approximate version, based on [t-digests](https://www.sciencedirect.com/science/article/pii/S2665963820300403) for online quantile estimation{{% available_from "v1.15.0" anomaly %}}.
|
||||
|
||||
<div class="model-details">
|
||||
|
||||
{{% collapse name="Model-specific arguments" %}}
|
||||
|
||||
- `class` (string) - model class name `"model.online.OnlineMADModel"` (or `mad_online` with class alias support{{% available_from "v1.13.0" anomaly %}})
|
||||
- `threshold` (float, optional) - The threshold multiplier for the MAD to determine anomalies. Defaults to `2.5`. Higher values will identify fewer points as anomalies.
|
||||
- `min_n_samples_seen` (int, optional) - the minimum number of samples to be seen (`n_samples_seen_` property) before computing the anomaly score. Otherwise, the **anomaly score will be 0**, as there is not enough data to trust the model's predictions. Defaults to 16.
|
||||
- `history_strength` (float, optional) - {{% available_from "v1.30.0" anomaly %}} strength of the initial history learned by `fit`. Values above `1` preserve fitted quantiles initially but reduce the leverage of subsequent updates. Defaults to `1`.
|
||||
- `compression` (int, optional) - the compression parameter for underlying [t-digest](https://www.sciencedirect.com/science/article/pii/S2665963820300403). Higher values mean higher accuracy but higher memory usage. By default 100.
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Configuration example" %}}
|
||||
|
||||
|
||||
```yaml
|
||||
models:
|
||||
your_desired_alias_for_a_model:
|
||||
class: "mad_online" # or 'model.online.OnlineMADModel'
|
||||
threshold: 2.5
|
||||
min_n_samples_seen: 128 # i.e. calculate it as full seasonality / data freq
|
||||
history_strength: 2 # retain fitted history as a stronger prior
|
||||
compression: 100 # higher values mean higher accuracy but higher memory usage
|
||||
provide_series: ['anomaly_score', 'yhat'] # common arg example
|
||||
# Common arguments for built-in model, if not set, default to
|
||||
# See https://docs.victoriametrics.com/anomaly-detection/components/models/#common-args
|
||||
#
|
||||
# provide_series: ['anomaly_score', 'yhat', 'yhat_lower', 'yhat_upper']
|
||||
# schedulers: [all scheduler aliases defined in `scheduler` section]
|
||||
# queries: [all query aliases defined in `reader.queries` section]
|
||||
# detection_direction: 'both' # meaning both drops and spikes will be captured
|
||||
# min_dev_from_expected: [0.0, 0.0] # meaning, no minimal threshold is applied to prevent smaller anomalies
|
||||
# scale: [1.0, 1.0] # if needed, prediction intervals' width can be increased (>1) or narrowed (<1)
|
||||
# clip_predictions: False # if data_range for respective `queries` is set in reader, `yhat.*` columns will be clipped
|
||||
# anomaly_score_outside_data_range: 1.01 # auto anomaly score (1.01) if `y` (real value) is outside of data_range, if set
|
||||
```
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
</div>
|
||||
|
||||
Resulting metrics of the model are described [here](#vmanomaly-output).
|
||||
|
||||
|
||||
### [Rolling Quantile](https://en.wikipedia.org/wiki/Quantile)
|
||||
|
||||
> `RollingQuantileModel` **is** {{% available_from "v1.28.0" anomaly %}} a [univariate](#univariate-models), [online](#online-models) model. It **was** {{% deprecated_from "v1.28.0" anomaly %}} a [univariate](#univariate-models), [rolling](#rolling-models), [offline](#offline-models) model.
|
||||
|
||||
This model is best used on **data with short evolving patterns** (i.e. 10-100 datapoints of particular frequency), as it adapts to changes over a rolling window.
|
||||
|
||||
<div class="model-details">
|
||||
|
||||
{{% collapse name="Model-specific arguments" %}}
|
||||
|
||||
- `class` (string) - model class name `"model.rolling_quantile.RollingQuantileModel"` (or `rolling_quantile` with class alias support {{% available_from "v1.13.0" anomaly %}})
|
||||
- `quantile` (float) - quantile value, from 0.5 to 1.0. This constraint is implied by 2-sided confidence interval.
|
||||
- `window_steps` (integer) - size of the moving window. (see 'sampling_period')
|
||||
- `iqr_threshold` (float, optional) - {{% available_from "v1.25.0" anomaly %}} The [interquartile range (IQR)](https://en.wikipedia.org/wiki/Interquartile_range) multiplier to increase the width of the prediction intervals. Defaults to 0 (no adjustment) for backward compatibility. If set > 0, the model will add half IQR * `iqr_threshold` to `yhat_lower` and `yhat_upper`. This is useful for data with high variance or outliers, as it helps to avoid false positives in anomaly detection.
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Configuration example" %}}
|
||||
|
||||
```yaml
|
||||
models:
|
||||
your_desired_alias_for_a_model:
|
||||
class: "rolling_quantile"
|
||||
quantile: 0.9
|
||||
window_steps: 96
|
||||
iqr_threshold: 1
|
||||
# Common arguments for built-in model, if not set, default to
|
||||
# See https://docs.victoriametrics.com/anomaly-detection/components/models/#common-args
|
||||
#
|
||||
# provide_series: ['anomaly_score', 'yhat', 'yhat_lower', 'yhat_upper']
|
||||
# schedulers: [all scheduler aliases defined in `scheduler` section]
|
||||
# queries: [all query aliases defined in `reader.queries` section]
|
||||
# detection_direction: 'both' # meaning both drops and spikes will be captured
|
||||
# min_dev_from_expected: [0.0, 0.0] # meaning, no minimal threshold is applied to prevent smaller anomalies
|
||||
# scale: [1.0, 1.0] # if needed, prediction intervals' width can be increased (>1) or narrowed (<1)
|
||||
# clip_predictions: False # if data_range for respective `queries` is set in reader, `yhat.*` columns will be clipped
|
||||
# anomaly_score_outside_data_range: 1.01 # auto anomaly score (1.01) if `y` (real value) is outside of data_range, if set
|
||||
```
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
</div>
|
||||
|
||||
Resulting metrics of the model are described [here](#vmanomaly-output).
|
||||
|
||||
|
||||
### Online Seasonal Quantile
|
||||
|
||||
> `OnlineQuantileModel` is a [univariate](#univariate-models), [online](#online-models) model.
|
||||
|
||||
Online (seasonal) quantile utilizes a set of approximate distributions, based on [t-digests](https://www.sciencedirect.com/science/article/pii/S2665963820300403) for online quantile estimation {{% available_from "v1.15.0" anomaly %}}.
|
||||
|
||||
Best used on **[de-trended](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-1/#trend) data with strong (potentially multiple) [seasonalities](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-1/#seasonality)**. Can act as a (slightly less flexible) replacement to [`ProphetModel`](#prophet).
|
||||
|
||||
It uses the `quantiles` triplet to calculate `yhat_lower`, `yhat`, and `yhat_upper` [output](#vmanomaly-output), respectively, for each of the `min_subseasons` sub-intervals contained in `seasonal_interval`. For example, with '4d' + '2h' seasonality patterns (multiple), it will hold and update 24*4 / 2 = 48 consecutive estimates (each 2 hours long).
|
||||
|
||||
<div class="model-details">
|
||||
|
||||
{{% collapse name="Model-specific arguments" %}}
|
||||
|
||||
- `class` (string) - model class name `"model.online.OnlineQuantileModel"` (or `quantile_online` with class alias support{{% available_from "v1.13.0" anomaly %}})
|
||||
- `quantiles` (list[float], optional) - The quantiles to estimate. `yhat_lower`, `yhat`, `yhat_upper` are the quantile order. By default (0.01, 0.5, 0.99).
|
||||
- `iqr_threshold` (float, optional) - {{% available_from "v1.25.0" anomaly %}} The [interquartile range (IQR)](https://en.wikipedia.org/wiki/Interquartile_range) multiplier to increase the width of the prediction intervals. Defaults to 0 (no adjustment) for backward compatibility. If set > 0, the model will add IQR * `iqr_threshold` to `yhat_lower` and `yhat_upper` (respecting `min_subseason` seasonal buckets). This is useful for data with high variance or outliers, as it helps to avoid false positives in anomaly detection. Best used with **robust** `quantiles` set to (0.25, 0.5, 0.75) or similar.
|
||||
- `seasonal_interval` (string, optional) - the interval for the seasonal adjustment. If not set, the model will equal to a simple online quantile model. By default not set.
|
||||
- `min_subseason` (str, optional) - the minimum interval to estimate quantiles for. By default not set. Note that the minimum interval should be a multiple of the seasonal interval, i.e. if seasonal_interval='2h', then min_subseason='15m' is valid, but '37m' is not.
|
||||
- `use_transform` (bool, optional) - whether to internally apply a `log1p(abs(x)) * sign(x)` transformation to the data to stabilize internal quantile estimation. Does not affect the scale of produced output (i.e. `yhat`) By default False.
|
||||
- `global_smoothing` (float, optional) - the smoothing parameter for the global quantiles. i.e. the output is a weighted average of the global and seasonal quantiles (if `seasonal_interval` and `min_subseason` args are set). Should be from `[0, 1]` interval, where 0 means no smoothing and 1 means using only global quantile values.
|
||||
- `scale` (float, optional) - Is used to adjust the margins between `yhat` and [`yhat_lower`, `yhat_upper`]. New margin = `|yhat_* - yhat_lower| * scale`. Defaults to 1 (no scaling is applied). See `scale`[common arg](https://docs.victoriametrics.com/anomaly-detection/components/models/#scale) section for detailed instructions and 2-sided option.
|
||||
- `season_starts_from` (str, optional) - the start date for the seasonal adjustment, as a reference point to start counting the intervals. By default '1970-01-01'.
|
||||
- `min_n_samples_seen` (int, optional) - the minimum number of samples to be seen (`n_samples_seen_` property) before computing the anomaly score. Otherwise, the **anomaly score will be 0**, as there is not enough data to trust the model's predictions. Defaults to 16.
|
||||
- `history_strength` (float, optional) - {{% available_from "v1.30.0" anomaly %}} strength of the initial history learned by `fit`. Values above `1` preserve fitted quantiles initially but reduce the leverage of subsequent updates. Defaults to `1`.
|
||||
- `compression` (int, optional) - the compression parameter for the underlying [t-digests](https://www.sciencedirect.com/science/article/pii/S2665963820300403). Higher values mean higher accuracy but higher memory usage. By default 100.
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Configuration example" %}}
|
||||
|
||||
Suppose we have a data with strong intra-day (hourly) and intra-week (daily) seasonality, data granularity is '5m' with up to 5% expected outliers present in data. Then you can apply similar config:
|
||||
|
||||
```yaml
|
||||
models:
|
||||
your_desired_alias_for_a_model:
|
||||
class: "quantile_online" # or 'model.online.OnlineQuantileModel'
|
||||
quantiles: [0.25, 0.5, 0.75] # lowered to exclude anomalous edges, can be compensated by `scale` param > 1 and `iqr_threshold` > 0
|
||||
iqr_threshold: 2.5 # to increase prediction intervals' width to avoid false positives while still keeping the model robust
|
||||
seasonal_interval: '7d' # longest seasonality (week, day) = week, starting from `season_starts_from`
|
||||
min_subseason: '1h' # smallest seasonality (week, day, hour) = hour, will have its own quantile estimates
|
||||
min_n_samples_seen: 288 # 1440 / 5 - at least 1 full day, ideal = 1440 / 5 * 7 - one full week (seasonal_interval)
|
||||
history_strength: 2 # retain fitted history as a stronger prior
|
||||
scale: 1.1 # to compensate lowered quantile boundaries with wider intervals
|
||||
season_starts_from: '2024-01-01' # interval calculation starting point, especially for uncommon seasonalities like '36h' or '12d'
|
||||
compression: 100 # higher values mean higher accuracy but higher memory usage
|
||||
provide_series: ['anomaly_score', 'yhat'] # common arg example
|
||||
# Common arguments for built-in model, if not set, default to
|
||||
# See https://docs.victoriametrics.com/anomaly-detection/components/models/#common-args
|
||||
#
|
||||
# provide_series: ['anomaly_score', 'yhat', 'yhat_lower', 'yhat_upper']
|
||||
# schedulers: [all scheduler aliases defined in `scheduler` section]
|
||||
# queries: [all query aliases defined in `reader.queries` section]
|
||||
# detection_direction: 'both' # meaning both drops and spikes will be captured
|
||||
# min_dev_from_expected: [0.0, 0.0] # meaning, no minimal threshold is applied to prevent smaller anomalies
|
||||
# scale: [1.0, 1.0] # if needed, prediction intervals' width can be increased (>1) or narrowed (<1)
|
||||
# clip_predictions: False # if data_range for respective `queries` is set in reader, `yhat.*` columns will be clipped
|
||||
# anomaly_score_outside_data_range: 1.01 # auto anomaly score (1.01) if `y` (real value) is outside of data_range, if set
|
||||
```
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
</div>
|
||||
|
||||
Resulting metrics of the model are described [here](#vmanomaly-output).
|
||||
|
||||
|
||||
### [Seasonal Trend Decomposition](https://en.wikipedia.org/wiki/Seasonal_adjustment)
|
||||
|
||||
> `StdModel` **is** {{% available_from "v1.28.0" anomaly %}} a [univariate](#univariate-models), [online](#online-models) model. It **was** {{% deprecated_from "v1.28.0" anomaly %}} a [univariate](#univariate-models), [rolling](#rolling-models), [offline](#offline-models) model.
|
||||
|
||||
Here we use Seasonal Decompose implementation from `statsmodels` [library](https://www.statsmodels.org/dev/generated/statsmodels.tsa.seasonal.seasonal_decompose). Parameters from this library can be passed to the model. Some parameters are specifically predefined in `vmanomaly` and can't be changed by user (`model`='additive', `two_sided`=False).
|
||||
|
||||
<div class="model-details">
|
||||
|
||||
{{% collapse name="Model-specific arguments" %}}
|
||||
|
||||
- `class` (string) - model class name `"model.std.StdModel"` (or `std` with class alias support{{% available_from "v1.13.0" anomaly %}})
|
||||
- `period` (integer) - Number of datapoints in one season.
|
||||
- `z_threshold` (float, optional) - [standard score](https://en.wikipedia.org/wiki/Standard_score) for calculating boundaries to define anomaly score. Defaults to `2.5`.
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Configuration example" %}}
|
||||
|
||||
|
||||
```yaml
|
||||
models:
|
||||
your_desired_alias_for_a_model:
|
||||
class: "std" # or 'model.std.StdModel' starting from v1.13.0
|
||||
period: 2
|
||||
# Common arguments for built-in model, if not set, default to
|
||||
# See https://docs.victoriametrics.com/anomaly-detection/components/models/#common-args
|
||||
#
|
||||
# provide_series: ['anomaly_score', 'yhat', 'yhat_lower', 'yhat_upper']
|
||||
# schedulers: [all scheduler aliases defined in `scheduler` section]
|
||||
# queries: [all query aliases defined in `reader.queries` section]
|
||||
# detection_direction: 'both' # meaning both drops and spikes will be captured
|
||||
# min_dev_from_expected: [0.0, 0.0] # meaning, no minimal threshold is applied to prevent smaller anomalies
|
||||
# scale: [1.0, 1.0] # if needed, prediction intervals' width can be increased (>1) or narrowed (<1)
|
||||
# clip_predictions: False # if data_range for respective `queries` is set in reader, `yhat.*` columns will be clipped
|
||||
# anomaly_score_outside_data_range: 1.01 # auto anomaly score (1.01) if `y` (real value) is outside of data_range, if set
|
||||
```
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
</div>
|
||||
|
||||
Resulting metrics of the model are described [here](#vmanomaly-output).
|
||||
|
||||
**Additional output metrics produced by Seasonal Trend Decomposition model**
|
||||
- `resid` - The residual component of the data series.
|
||||
- `trend` - The trend component of the data series.
|
||||
- `seasonal` - The seasonal component of the data series.
|
||||
|
||||
|
||||
### [Isolation forest](https://en.wikipedia.org/wiki/Isolation_forest) (Multivariate)
|
||||
|
||||
> `IsolationForestModel` is a [univariate](#univariate-models), [offline](#offline-models) model.
|
||||
|
||||
> `IsolationForestMultivariateModel` is a [multivariate](#multivariate-models), [offline](#offline-models) model.
|
||||
|
||||
> [!NOTE]
|
||||
> Both univariate `isolation_forest` and multivariate `isolation_forest_multivariate` are planned for deprecation in a future release. For new deployments, use the corresponding univariate or multivariate online [Temporal Envelope](#temporal-envelope) model.
|
||||
|
||||
Detects anomalies using binary trees. The algorithm has a linear time complexity and a low memory requirement, which works well with high-volume data. It can be used on both univariate and multivariate data, but it is more effective in multivariate case.
|
||||
|
||||
**Important**: Be aware of [the curse of dimensionality](https://en.wikipedia.org/wiki/Curse_of_dimensionality). Don't use single multivariate model if you expect your queries to return many time series of less datapoints that the number of metrics. In such case it is hard for a model to learn meaningful dependencies from too sparse data hypercube.
|
||||
@@ -1246,6 +1269,9 @@ Resulting metrics of the model are described [here](#vmanomaly-output).
|
||||
|
||||
> `HoltWinters` is a [univariate](#univariate-models), [offline](#offline-models) model.
|
||||
|
||||
> [!NOTE]
|
||||
> Holt-Winters is planned for deprecation in a future release. For new deployments, prefer the online [Temporal Envelope](#temporal-envelope) model.
|
||||
|
||||
Here we use Holt-Winters Exponential Smoothing implementation from `statsmodels` [library](https://www.statsmodels.org/dev/generated/statsmodels.tsa.holtwinters.ExponentialSmoothing). All parameters from this library can be passed to the model.
|
||||
|
||||
<div class="model-details">
|
||||
@@ -1305,6 +1331,55 @@ models:
|
||||
|
||||
Resulting metrics of the model are described [here](#vmanomaly-output).
|
||||
|
||||
### [Seasonal Trend Decomposition](https://en.wikipedia.org/wiki/Seasonal_adjustment)
|
||||
|
||||
> `StdModel` **is** {{% available_from "v1.28.0" anomaly %}} a [univariate](#univariate-models), [online](#online-models) model. It **was** {{% deprecated_from "v1.28.0" anomaly %}} a [univariate](#univariate-models), [rolling](#rolling-models), [offline](#offline-models) model.
|
||||
|
||||
Here we use Seasonal Decompose implementation from `statsmodels` [library](https://www.statsmodels.org/dev/generated/statsmodels.tsa.seasonal.seasonal_decompose). Parameters from this library can be passed to the model. Some parameters are specifically predefined in `vmanomaly` and can't be changed by user (`model`='additive', `two_sided`=False).
|
||||
|
||||
<div class="model-details">
|
||||
|
||||
{{% collapse name="Model-specific arguments" %}}
|
||||
|
||||
- `class` (string) - model class name `"model.std.StdModel"` (or `std` with class alias support{{% available_from "v1.13.0" anomaly %}})
|
||||
- `period` (integer) - Number of datapoints in one season.
|
||||
- `z_threshold` (float, optional) - [standard score](https://en.wikipedia.org/wiki/Standard_score) for calculating boundaries to define anomaly score. Defaults to `2.5`.
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Configuration example" %}}
|
||||
|
||||
|
||||
```yaml
|
||||
models:
|
||||
your_desired_alias_for_a_model:
|
||||
class: "std" # or 'model.std.StdModel' starting from v1.13.0
|
||||
period: 2
|
||||
# Common arguments for built-in model, if not set, default to
|
||||
# See https://docs.victoriametrics.com/anomaly-detection/components/models/#common-args
|
||||
#
|
||||
# provide_series: ['anomaly_score', 'yhat', 'yhat_lower', 'yhat_upper']
|
||||
# schedulers: [all scheduler aliases defined in `scheduler` section]
|
||||
# queries: [all query aliases defined in `reader.queries` section]
|
||||
# detection_direction: 'both' # meaning both drops and spikes will be captured
|
||||
# min_dev_from_expected: [0.0, 0.0] # meaning, no minimal threshold is applied to prevent smaller anomalies
|
||||
# scale: [1.0, 1.0] # if needed, prediction intervals' width can be increased (>1) or narrowed (<1)
|
||||
# clip_predictions: False # if data_range for respective `queries` is set in reader, `yhat.*` columns will be clipped
|
||||
# anomaly_score_outside_data_range: 1.01 # auto anomaly score (1.01) if `y` (real value) is outside of data_range, if set
|
||||
```
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
</div>
|
||||
|
||||
Resulting metrics of the model are described [here](#vmanomaly-output).
|
||||
|
||||
**Additional output metrics produced by Seasonal Trend Decomposition model**
|
||||
- `resid` - The residual component of the data series.
|
||||
- `trend` - The trend component of the data series.
|
||||
- `seasonal` - The seasonal component of the data series.
|
||||
|
||||
|
||||
## vmanomaly output
|
||||
|
||||
`vmanomaly` generates model-dependent output series. Their metric names can be configured in the writer section.
|
||||
@@ -1344,6 +1419,10 @@ This guide shows how to:
|
||||
|
||||
> The file containing the model must be written in [Python](https://www.python.org/) 3.14 or later. A custom model runs inside the `vmanomaly` Python environment, so keep its dependencies compatible with the target image and keep the module available when restoring serialized model state after a restart.
|
||||
|
||||
<div class="collapse-group">
|
||||
|
||||
{{% collapse name="Custom model implementation guide" %}}
|
||||
|
||||
### 1. Custom model
|
||||
|
||||
Create `custom_model.py` with a `CustomModel` class derived from `Model`. A concrete model must implement:
|
||||
@@ -1482,7 +1561,7 @@ See the [component configuration reference](https://docs.victoriametrics.com/ano
|
||||
Pull the `vmanomaly` image:
|
||||
|
||||
```sh
|
||||
docker pull victoriametrics/vmanomaly:v1.30.0
|
||||
docker pull victoriametrics/vmanomaly:v1.30.1
|
||||
```
|
||||
|
||||
Mount the module at `/vmanomaly/src/model/custom.py`, which matches the configured import path `model.custom.CustomModel`. Validate the complete configuration with `--dryRun` before starting the long-running service.
|
||||
@@ -1492,7 +1571,7 @@ docker run --rm \
|
||||
-v "$PWD/license:/license:ro" \
|
||||
-v "$PWD/custom_model.py:/vmanomaly/src/model/custom.py:ro" \
|
||||
-v "$PWD/config.yaml:/config.yaml:ro" \
|
||||
victoriametrics/vmanomaly:v1.30.0 \
|
||||
victoriametrics/vmanomaly:v1.30.1 \
|
||||
/config.yaml \
|
||||
--licenseFile=/license \
|
||||
--dryRun
|
||||
@@ -1510,6 +1589,10 @@ The writer emits one `custom_anomaly_score` series for each input series. It ret
|
||||
{__name__="custom_anomaly_score", for="churn_rate", model_alias="custom_model", scheduler_alias="s1", run="test-format"}
|
||||
```
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
</div>
|
||||
|
||||
## Deprecations
|
||||
|
||||
{{% collapse name="Deprecated model types and models" %}}
|
||||
|
||||
@@ -24,6 +24,10 @@ There are 2 models to monitor VictoriaMetrics Anomaly Detection behavior - [push
|
||||
- Adding `preset` and `scheduler_alias` keys to [VmReader](https://docs.victoriametrics.com/anomaly-detection/components/monitoring/#reader-behaviour-metrics) and [VmWriter](https://docs.victoriametrics.com/anomaly-detection/components/monitoring/#writer-behaviour-metrics) metrics for consistency in multi-[scheduler](https://docs.victoriametrics.com/anomaly-detection/components/scheduler/) setups.
|
||||
- Renaming [Counters](https://prometheus.io/docs/concepts/metric_types/#counter) `vmanomaly_reader_response_count` to `vmanomaly_reader_responses` and `vmanomaly_writer_response_count` to `vmanomaly_writer_responses`.
|
||||
|
||||
<div class="collapse-group">
|
||||
|
||||
{{% collapse name="Pull model config parameters" %}}
|
||||
|
||||
## Pull Model Config parameters
|
||||
|
||||
<table class="params">
|
||||
@@ -60,6 +64,10 @@ There are 2 models to monitor VictoriaMetrics Anomaly Detection behavior - [push
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Push config parameters" %}}
|
||||
|
||||
## Push Config parameters
|
||||
|
||||
By default, metrics are pushed only after the completion of specific stages, e.g., `fit`, `infer`, or `fit_infer` (for each [scheduler](https://docs.victoriametrics.com/anomaly-detection/components/scheduler/) if using a multi-scheduler configuration).
|
||||
@@ -227,6 +235,10 @@ Path to a file with the client certificate key, i.e. `client.key`{{% available_f
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
</div>
|
||||
|
||||
## Monitoring section config example
|
||||
|
||||
``` yaml
|
||||
@@ -260,6 +272,10 @@ For detailed guidance on configuring mTLS parameters such as `verify_tls`, `tls_
|
||||
- [Model metrics](#models-behaviour-metrics)
|
||||
- [Writer metrics](#writer-behaviour-metrics)
|
||||
|
||||
<div class="collapse-group">
|
||||
|
||||
{{% collapse name="Startup metrics" %}}
|
||||
|
||||
### Startup metrics
|
||||
|
||||
<table class="params">
|
||||
@@ -385,6 +401,10 @@ For detailed guidance on configuring mTLS parameters such as `verify_tls`, `tls_
|
||||
|
||||
[Back to metric sections](#metrics-generated-by-vmanomaly)
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Reader behaviour metrics" %}}
|
||||
|
||||
### Reader behaviour metrics
|
||||
Label names [description](#labelnames)
|
||||
|
||||
@@ -509,6 +529,10 @@ Label names [description](#labelnames)
|
||||
|
||||
[Back to metric sections](#metrics-generated-by-vmanomaly)
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Models behaviour metrics" %}}
|
||||
|
||||
### Models behaviour metrics
|
||||
Label names [description](#labelnames)
|
||||
|
||||
@@ -564,7 +588,7 @@ Label names [description](#labelnames)
|
||||
|
||||
`Counter`
|
||||
</td>
|
||||
<td>The number of valid datapoints accepted by `model_alias`, excluding NaN and Inf values, during `fit`, `infer`, or combined `fit_infer` execution for the `query_key` query.</td>
|
||||
<td>The number of valid datapoints accepted by `model_alias`, excluding NaN and Inf values, during `fit`, `infer`, or combined `fit_infer` execution for the `query_key` query. During inference, only previously unseen valid rows are counted {{% available_from "v1.30.1" anomaly %}}.</td>
|
||||
<td>
|
||||
|
||||
`stage`, `query_key`, `model_alias`, `scheduler_alias`, `preset`
|
||||
@@ -635,6 +659,10 @@ Label names [description](#labelnames)
|
||||
|
||||
[Back to metric sections](#metrics-generated-by-vmanomaly)
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Writer behaviour metrics" %}}
|
||||
|
||||
### Writer behaviour metrics
|
||||
Label names [description](#labelnames)
|
||||
|
||||
@@ -659,7 +687,7 @@ Label names [description](#labelnames)
|
||||
|
||||
`Histogram` (was `Summary`{{% deprecated_from "v1.17.0" anomaly %}})
|
||||
</td>
|
||||
<td>The total time (in seconds) taken by write requests to VictoriaMetrics `url` for the `query_key` query within the specified scheduler `scheduler_alias`, in the `vmanomaly` service running in `preset` mode.
|
||||
<td>The total time (in seconds) taken by write requests to VictoriaMetrics `url` for the `query_key` query within the specified scheduler `scheduler_alias`, in the `vmanomaly` service running in `preset` mode. Successful and handled failed attempts, including connection retries, are observed {{% available_from "v1.30.1" anomaly %}}.
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -746,6 +774,10 @@ Label names [description](#labelnames)
|
||||
|
||||
[Back to metric sections](#metrics-generated-by-vmanomaly)
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
</div>
|
||||
|
||||
### Labelnames
|
||||
|
||||
* `stage` - model execution stage: `fit`, `infer`, or `fit_infer` for a combined fit/inference scheduler run. See [model types](https://docs.victoriametrics.com/anomaly-detection/components/models/#model-types).
|
||||
@@ -793,6 +825,10 @@ and the [command-line arguments](https://docs.victoriametrics.com/anomaly-detect
|
||||
- [Query server and task logs](#query-server-and-task-logs)
|
||||
- [AI Copilot logs](#ai-copilot-logs)
|
||||
|
||||
<div class="collapse-group">
|
||||
|
||||
|
||||
{{% collapse name="Startup logs" %}}
|
||||
|
||||
### Startup logs
|
||||
|
||||
@@ -812,6 +848,10 @@ server addresses, hot-reload state, and active schedulers. The most useful prefi
|
||||
|
||||
[Back to logging sections](#logs-generated-by-vmanomaly)
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Reader logs" %}}
|
||||
|
||||
### Reader logs
|
||||
|
||||
Reader logs cover endpoint checks, request splitting, network failures, response parsing, and coordination between
|
||||
@@ -863,6 +903,10 @@ or parsed. See [reader behaviour metrics](#reader-behaviour-metrics).
|
||||
|
||||
[Back to logging sections](#logs-generated-by-vmanomaly)
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Service logs" %}}
|
||||
|
||||
### Service logs
|
||||
|
||||
The service logs `fit`, `infer`, and combined `fit_infer`/backtesting work for each model alias and scheduler.
|
||||
@@ -891,6 +935,10 @@ an unsuccessful stage. See [models behaviour metrics](#models-behaviour-metrics)
|
||||
|
||||
[Back to logging sections](#logs-generated-by-vmanomaly)
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Writer logs" %}}
|
||||
|
||||
### Writer logs
|
||||
|
||||
Writer logs cover serialization and delivery of produced series such as
|
||||
@@ -918,6 +966,10 @@ and datapoints are recorded only after a successful response. See [writer behavi
|
||||
|
||||
[Back to logging sections](#logs-generated-by-vmanomaly)
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Scheduler supervision logs" %}}
|
||||
|
||||
### Scheduler supervision logs
|
||||
|
||||
Scheduler supervision{{% available_from "v1.30.0" anomaly %}} logs a dead worker, automatic restart, successful
|
||||
@@ -927,6 +979,10 @@ Correlate them with `vmanomaly_scheduler_alive` and `vmanomaly_scheduler_restart
|
||||
|
||||
[Back to logging sections](#logs-generated-by-vmanomaly)
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Hot-reload logs" %}}
|
||||
|
||||
### Hot-reload logs
|
||||
|
||||
Hot reload logs config-change detection, validation, staged service restart, success, and rollback. `Reload aborted
|
||||
@@ -936,6 +992,10 @@ without restarting services`.
|
||||
|
||||
[Back to logging sections](#logs-generated-by-vmanomaly)
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Persisted-state logs" %}}
|
||||
|
||||
### Persisted-state logs
|
||||
|
||||
With `settings.restore_state`, startup logs the stored/runtime version assessment, reusable components, required
|
||||
@@ -944,6 +1004,10 @@ stored artifacts completely` indicates a full reset; missing or unreadable model
|
||||
|
||||
[Back to logging sections](#logs-generated-by-vmanomaly)
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Query server and task logs" %}}
|
||||
|
||||
### Query server and task logs
|
||||
|
||||
The query server logs its listening address and datasource-proxy timeouts/failures. Background anomaly-detection
|
||||
@@ -952,6 +1016,10 @@ background raw query finishing cleanly.
|
||||
|
||||
[Back to logging sections](#logs-generated-by-vmanomaly)
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="AI Copilot logs" %}}
|
||||
|
||||
### AI Copilot logs
|
||||
|
||||
AI Copilot{{% available_from "v1.30.0" anomaly %}} reports whether it is initialized, disabled, misconfigured, or
|
||||
@@ -960,3 +1028,7 @@ request failed` identifies provider execution failure, and `MCP server unreachab
|
||||
guidance tools.
|
||||
|
||||
[Back to logging sections](#logs-generated-by-vmanomaly)
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
</div>
|
||||
|
||||
@@ -25,6 +25,8 @@ Use the following playgrounds to develop and test input queries:
|
||||
|
||||
## VM reader
|
||||
|
||||
<div class="collapse-group">
|
||||
|
||||
{{% collapse name="Queries format migration (to v1.13.0+)" %}}
|
||||
|
||||
> The backward-compatible `queries` format introduced in v1.13.0 allows [VmReader](#vm-reader) parameters such as `step` to be configured per query. This can reduce the amount of data read from VictoriaMetrics. See [per-query parameters](#per-query-parameters) for details.
|
||||
@@ -61,6 +63,8 @@ reader:
|
||||
```
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="VM reader per-query parameters and example" %}}
|
||||
|
||||
### Per-query parameters
|
||||
|
||||
There is change {{% available_from "v1.13.0" anomaly %}} of [`queries`](https://docs.victoriametrics.com/anomaly-detection/components/reader/#vm-reader) arg format. Now each query alias supports the next (sub)fields, which *override reader-level parameters*, if set:
|
||||
@@ -83,7 +87,7 @@ There is change {{% available_from "v1.13.0" anomaly %}} of [`queries`](https://
|
||||
|
||||
- `max_points_per_query`{{% available_from "v1.17.0" anomaly %}} (int): Optional arg, overrides how `search.maxPointsPerTimeseries` flag{{% available_from "v1.14.1" anomaly %}} impacts `vmanomaly` on splitting long `fit_window` [queries](https://docs.victoriametrics.com/anomaly-detection/components/reader/#vm-reader) into smaller sub-intervals. This helps users avoid hitting the `search.maxQueryDuration` limit for individual queries by distributing initial query across multiple subquery requests with minimal overhead. Set less than `search.maxPointsPerTimeseries` if hitting `maxQueryDuration` limits. If set on a query-level, it overrides the global `max_points_per_query` (reader-level).
|
||||
|
||||
- `tz`{{% available_from "v1.18.0" anomaly %}} (string): this optional argument enables timezone specification per query, overriding the reader’s default `tz`. This setting helps to account for local timezone shifts, such as [DST](https://en.wikipedia.org/wiki/Daylight_saving_time), in models that are sensitive to seasonal variations (e.g., [`ProphetModel`](https://docs.victoriametrics.com/anomaly-detection/components/models/#prophet) or [`OnlineQuantileModel`](https://docs.victoriametrics.com/anomaly-detection/components/models/#online-seasonal-quantile)).
|
||||
- `tz`{{% available_from "v1.18.0" anomaly %}} (string): this optional argument enables timezone specification per query, overriding the reader’s default `tz`. This setting helps to account for local timezone shifts, such as [DST](https://en.wikipedia.org/wiki/Daylight_saving_time), in models that are sensitive to seasonal variations (e.g., [`TemporalEnvelopeModel`](https://docs.victoriametrics.com/anomaly-detection/components/models/#temporal-envelope) or [`OnlineQuantileModel`](https://docs.victoriametrics.com/anomaly-detection/components/models/#online-seasonal-quantile)).
|
||||
|
||||
- `tenant_id` {{% available_from "v1.19.0" anomaly %}} (string): this optional argument enables tenant-level separation for queries (e.g. `query1` to get the data from tenant "0:0", `query2` - from tenant "1:0"). It works as follows:
|
||||
- if *not set, inherits* reader-level `tenant_id`
|
||||
@@ -125,6 +129,10 @@ reader:
|
||||
offset: '-15s' # to override reader-wise `offset` and query data 15 seconds earlier to account for data collection delays
|
||||
```
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="VM reader config parameters and example" %}}
|
||||
|
||||
### Config parameters
|
||||
|
||||
<table class="params">
|
||||
@@ -433,7 +441,7 @@ Optional arg{{% available_from "v1.17.0" anomaly %}} overrides how `search.maxPo
|
||||
`UTC`
|
||||
</td>
|
||||
<td>
|
||||
Optional argument {{% available_from "v1.18.0" anomaly %}} specifies the [IANA](https://nodatime.org/TimeZones) timezone to account for local shifts, like [DST](https://en.wikipedia.org/wiki/Daylight_saving_time), in models sensitive to seasonal patterns (e.g., [`TemporalEnvelopeModel`](https://docs.victoriametrics.com/anomaly-detection/components/models/#temporal-envelope), [`ProphetModel`](https://docs.victoriametrics.com/anomaly-detection/components/models/#prophet), or [`OnlineQuantileModel`](https://docs.victoriametrics.com/anomaly-detection/components/models/#online-seasonal-quantile)). Defaults to `UTC` if not set and can be overridden on a [per-query basis](#per-query-parameters).
|
||||
Optional argument {{% available_from "v1.18.0" anomaly %}} specifies the [IANA](https://nodatime.org/TimeZones) timezone to account for local shifts, like [DST](https://en.wikipedia.org/wiki/Daylight_saving_time), in models sensitive to seasonal patterns (e.g., [`TemporalEnvelopeModel`](https://docs.victoriametrics.com/anomaly-detection/components/models/#temporal-envelope) or [`OnlineQuantileModel`](https://docs.victoriametrics.com/anomaly-detection/components/models/#online-seasonal-quantile)). Defaults to `UTC` if not set and can be overridden on a [per-query basis](#per-query-parameters).
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -507,10 +515,16 @@ reader:
|
||||
series_processing_batch_size: 8
|
||||
```
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
</div>
|
||||
|
||||
### MetricsQL Playground
|
||||
|
||||
To experiment with MetricsQL queries for `VmReader`, you can use the [VictoriaMetrics MetricsQL Playground](https://play.victoriametrics.com/), which provides an interactive environment to test and visualize your queries against sample data. You can also access embedded version of the playground below:
|
||||
|
||||
<div class="collapse-group">
|
||||
|
||||
{{% collapse name="VictoriaMetrics Playground" %}}
|
||||
|
||||
<div class="position-relative mb-3">
|
||||
@@ -536,6 +550,8 @@ To experiment with MetricsQL queries for `VmReader`, you can use the [VictoriaMe
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
</div>
|
||||
|
||||
### mTLS protection
|
||||
|
||||
`vmanomaly` supports [mutual TLS (mTLS)](https://en.wikipedia.org/wiki/Mutual_authentication){{% available_from "v1.16.3" anomaly %}} for secure communication across its components, including [VmReader](https://docs.victoriametrics.com/anomaly-detection/components/reader/#vm-reader), [VmWriter](https://docs.victoriametrics.com/anomaly-detection/components/writer/#vm-writer), and [Monitoring/Push](https://docs.victoriametrics.com/anomaly-detection/components/monitoring/#push-config-parameters). This allows for mutual authentication between the client and server when querying or writing data to [VictoriaMetrics Enterprise, configured for mTLS](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#mtls-protection).
|
||||
@@ -680,6 +696,8 @@ Similarly, [VictoriaTraces LogsQL Playground](https://play-vtraces.victoriametri
|
||||
|
||||
You can also access **embedded version of the playground below** (VictoriaLogs datasource):
|
||||
|
||||
<div class="collapse-group">
|
||||
|
||||
{{% collapse name="VictoriaLogs LogsQL Playground" %}}
|
||||
|
||||
<div class="position-relative mb-3">
|
||||
@@ -705,6 +723,11 @@ You can also access **embedded version of the playground below** (VictoriaLogs d
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="collapse-group">
|
||||
|
||||
{{% collapse name="VictoriaLogs reader config parameters" %}}
|
||||
|
||||
### Config parameters
|
||||
|
||||
@@ -802,7 +825,7 @@ Frequency of the points returned. Will be converted to `/select/stats_query_rang
|
||||
`America/New_York`
|
||||
</td>
|
||||
<td>
|
||||
(Optional) Specifies the [IANA](https://nodatime.org/TimeZones) timezone to account for local shifts, like [DST](https://en.wikipedia.org/wiki/Daylight_saving_time), in models sensitive to seasonal patterns (e.g., [`ProphetModel`](https://docs.victoriametrics.com/anomaly-detection/components/models/#prophet) or [`OnlineQuantileModel`](https://docs.victoriametrics.com/anomaly-detection/components/models/#online-seasonal-quantile)). Defaults to `UTC` if not set and can be overridden on a [per-query basis](#per-query-parameters).
|
||||
(Optional) Specifies the [IANA](https://nodatime.org/TimeZones) timezone to account for local shifts, like [DST](https://en.wikipedia.org/wiki/Daylight_saving_time), in models sensitive to seasonal patterns (e.g., [`TemporalEnvelopeModel`](https://docs.victoriametrics.com/anomaly-detection/components/models/#temporal-envelope) or [`OnlineQuantileModel`](https://docs.victoriametrics.com/anomaly-detection/components/models/#online-seasonal-quantile)). Defaults to `UTC` if not set and can be overridden on a [per-query basis](#per-query-parameters).
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -989,6 +1012,10 @@ Optional hard cap {{% available_from "v1.30.0" anomaly %}} for how far last-seen
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="VictoriaLogs reader per-query parameters and example" %}}
|
||||
|
||||
### Per-query parameters
|
||||
|
||||
The names, types and the logic of the per-query parameters subset used in `VLogsReader` are exactly the same as those of [`VmReader`](#vm-reader), please see [per-query parameters](#per-query-parameters) section above for the details. The only difference is that `expr` parameter should contain a valid [LogsQL](https://docs.victoriametrics.com/victorialogs/logsql/) expression with `stats` [pipe](https://docs.victoriametrics.com/victorialogs/logsql/#stats-pipe), as described in [query examples](#query-examples) section above.
|
||||
@@ -1036,6 +1063,10 @@ reader:
|
||||
# other config sections, like models, schedulers, writer, ...
|
||||
```
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
</div>
|
||||
|
||||
### mTLS protection
|
||||
|
||||
Please refer to the [mTLS protection](#mtls-protection) section above for details on how to configure mTLS for `VLogsReader`. It uses the same config parameters as `VmReader` for mTLS setup.
|
||||
|
||||
@@ -74,6 +74,8 @@ options={`"scheduler.periodic.PeriodicScheduler"`, `"scheduler.oneoff.OneoffSche
|
||||
|
||||
> {{% available_from "v1.30.0" anomaly %}} If a periodic scheduler worker exits unexpectedly, the service attempts bounded restarts with exponential backoff instead of shutting down unrelated schedulers. Monitor [`vmanomaly_scheduler_alive`](https://docs.victoriametrics.com/anomaly-detection/components/monitoring/#startup-metrics) and `vmanomaly_scheduler_restarts_total` to alert on persistent failures.
|
||||
|
||||
> {{% available_from "v1.30.1" anomaly %}} For exact-capable [online models](https://docs.victoriametrics.com/anomaly-detection/components/models/#online-models), `infer_every` is also the causal model-update cadence. If a delayed periodic job fetches several observations at once, they are processed on the same chronological grid used by exact backtesting rather than as one behaviorally different batch.
|
||||
|
||||
### Parameters
|
||||
|
||||
For periodic scheduler parameters are defined as differences in times, expressed in difference units, e.g. days, hours, minutes, seconds. Time granularity is defined by the last characters of a string. Examples: `"50s"` (seconds), `"4m"` (minutes), `"3h"` (hours), `"2d"` (days), `"1w"` (weeks).
|
||||
@@ -440,7 +442,7 @@ In **Inference only** mode {{% available_from "v1.22.1" anomaly %}}, the schedul
|
||||
- `fit_window`: Duration of historical data used for each training run (e.g. `P7D`, `PT1H`).
|
||||
- `fit_every`: Interval between consecutive training/inference cycles.
|
||||
- {{% available_from "v1.28.0" anomaly %}} `exact`: If set to `true`, BacktestingScheduler will execute inference for online models in small chronological batches equal to `infer_every` to mimic the production scheduler. (default: `false`)
|
||||
- {{% available_from "v1.28.0" anomaly %}} `infer_every`: Optional inference cadence for exact mode, defining how often the scheduler should call infer between two fits, otherwise defaults to `fit_every` when unset.
|
||||
- {{% available_from "v1.28.0" anomaly %}} `infer_every`: Optional inference grid and, in exact mode, model-call cadence between two fits. {{% available_from "v1.30.1" anomaly %}} In `inference_only` mode, an omitted value is derived from the effective query step or reader sampling period and capped by `fit_every`; it falls back to `fit_every` only when neither reader value is available.
|
||||
- `n_jobs`: Number of parallel jobs for backtesting (default: `1`).
|
||||
|
||||
#### Example
|
||||
|
||||
@@ -74,5 +74,7 @@ Rest API endpoints (e.g. `/metrics`) can be accessed at `<vmanomaly-host>:8490/v
|
||||
- `GET /api/v1/autotune/tasks/{task_id}` returns progress and the concrete suggested `modelConfig` when complete.
|
||||
- `DELETE /api/v1/autotune/tasks/{task_id}` cancels pending work cooperatively.
|
||||
|
||||
{{% available_from "v1.30.1" anomaly %}} Seasonality analysis preserves the original timestamp grid when samples are offset from whole step boundaries. This avoids missing daily or weekly patterns solely because timestamps are shifted within the configured sampling interval.
|
||||
|
||||
> [!TIP]
|
||||
> For a complete request and recommended workflow, see [Shared asynchronous autotune workflow](https://docs.victoriametrics.com/anomaly-detection/components/models/#shared-asynchronous-autotune-workflow). OpenAPI schemas for the running version are available at `/docs` endpoint of a running `vmanomaly` instance.
|
||||
|
||||
@@ -42,7 +42,7 @@ schedulers:
|
||||
# other schedulers
|
||||
|
||||
models:
|
||||
zscore_online_override:
|
||||
zscore_online_inherited:
|
||||
class: zscore_online
|
||||
z_threshold: 3.5
|
||||
clip_predictions: True
|
||||
@@ -73,6 +73,7 @@ reader:
|
||||
writer:
|
||||
class: "vm"
|
||||
datasource_url: http://localhost:8428
|
||||
tenant_id: "0"
|
||||
metric_format:
|
||||
__name__: "$VAR"
|
||||
for: "$QUERY_KEY"
|
||||
@@ -226,6 +227,10 @@ monitoring:
|
||||
# other monitoring settings
|
||||
```
|
||||
|
||||
<div class="collapse-group">
|
||||
|
||||
{{% collapse name="State restoration example" %}}
|
||||
|
||||
### Example
|
||||
|
||||
For a configuration with the following models, queries and schedulers:
|
||||
@@ -245,12 +250,11 @@ models:
|
||||
class: zscore_online
|
||||
z_threshold: 3.5
|
||||
schedulers: ['periodic_1d']
|
||||
prophet:
|
||||
class: prophet
|
||||
temporal_envelope:
|
||||
class: temporal_envelope
|
||||
schedulers: ['periodic_1d']
|
||||
queries: ['q1', 'q2']
|
||||
args:
|
||||
interval_width: 0.98
|
||||
seasonalities: ['hod_smooth', 'dow_smooth']
|
||||
reader:
|
||||
class: vm
|
||||
datasource_url: 'https://play.victoriametrics.com'
|
||||
@@ -264,7 +268,7 @@ reader:
|
||||
# other components like writer, monitoring, etc.
|
||||
```
|
||||
|
||||
if the service is restarted in less than 1 hour after the last training (now < next scheduled fit time), it will restore the state of the `zscore_online` and `prophet` models if their signature (class, hyperparameters, schedulers, etc.) has not changed. It will load the trained model instances or their training data from disk and continue producing [anomaly scores](https://docs.victoriametrics.com/anomaly-detection/faq/#what-is-anomaly-score) without retraining. If there are changes or new queries added to the configuration, the service will add these to scheduled jobs for fit and infer. That's what is changed and what is restored in a config below:
|
||||
if the service is restarted in less than 1 hour after the last training (now < next scheduled fit time), it will restore the state of the `zscore_online` and `temporal_envelope` models if their signature (class, hyperparameters, schedulers, etc.) has not changed. It will load the trained model instances or their training data from disk and continue producing [anomaly scores](https://docs.victoriametrics.com/anomaly-detection/faq/#what-is-anomaly-score) without retraining. If there are changes or new queries added to the configuration, the service will add these to scheduled jobs for fit and infer. That's what is changed and what is restored in a config below:
|
||||
|
||||
```yaml
|
||||
settings:
|
||||
@@ -281,12 +285,11 @@ models:
|
||||
class: zscore_online # unchanged, still the same model class
|
||||
z_threshold: 3.0 # changed, needs retraining!
|
||||
schedulers: ['periodic_1d'] # unchanged, still attached to the same scheduler
|
||||
prophet: # can be partially reused, because its class and schedulers are unchanged but queries have changed
|
||||
class: prophet # unchanged, still the same model class
|
||||
temporal_envelope: # can be partially reused, because its class and schedulers are unchanged but queries have changed
|
||||
class: temporal_envelope # unchanged, still the same model class
|
||||
schedulers: ['periodic_1d'] # unchanged, still attached to the same scheduler
|
||||
queries: ['q1', 'q3'] # changed, added new query 'q3', drops 'q2', so (prophet, q2) should be trained from scratch
|
||||
args:
|
||||
interval_width: 0.98 # unchanged, still the same argument
|
||||
queries: ['q1', 'q3'] # changed, added new query 'q3', drops 'q2', so (temporal_envelope, q2) should be trained from scratch
|
||||
seasonalities: ['hod_smooth', 'dow_smooth'] # unchanged
|
||||
reader: # can be partially reused, because its class and datasource URL are unchanged, but queries have changed
|
||||
class: vm # unchanged, still the same reader class
|
||||
datasource_url: 'https://play.victoriametrics.com' # unchanged, still the same datasource URL
|
||||
@@ -297,13 +300,17 @@ reader: # can be partially reused, because its class and datasource URL are unc
|
||||
q2:
|
||||
expr: 'some_metricsql_query_2' # will be removed, no longer used by any model
|
||||
q3:
|
||||
expr: 'some_metricsql_query_3' # new query, added to the reader, and used by the `prophet` model
|
||||
expr: 'some_metricsql_query_3' # new query, added to the reader, and used by the `temporal_envelope` model
|
||||
sampling_period: 30s # unchanged, still the same sampling period
|
||||
# other components like writer, monitoring, etc. remain unchanged
|
||||
```
|
||||
This means that the service upon restart:
|
||||
1. Won't restore the state of `zscore_online` model, because its `z_threshold` argument **has changed**, retraining from scratch is needed on the last `fit_window` = 24 hours of data for `q1`, `q2` and `q3` (as model's `queries` arg is not set so it defaults to all queries found in the reader).
|
||||
2. Will **partially** restore the state of `prophet` model, because its class and schedulers are unchanged, but **only instances trained on timeseries returned by `q1` query**. New fit/infer jobs will be set for new query `q3`. The old query `q2` artifacts will be dropped upon restart - all respective models and data for (`prophet`, `q2`) combination will be removed from the database file and from the disk.
|
||||
2. Will **partially** restore the state of `temporal_envelope` model, because its class and schedulers are unchanged, but **only instances trained on timeseries returned by `q1` query**. New fit/infer jobs will be set for new query `q3`. The old query `q2` artifacts will be dropped upon restart - all respective models and data for (`temporal_envelope`, `q2`) combination will be removed from the database file and from the disk.
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
</div>
|
||||
|
||||
## Retention
|
||||
|
||||
@@ -363,7 +370,7 @@ models:
|
||||
queries: ['q1']
|
||||
# other model args
|
||||
m2: # model instances will be likely dropped during retention checks due to high churn rate
|
||||
class: prophet
|
||||
class: temporal_envelope
|
||||
schedulers: ['s1']
|
||||
queries: ['q2']
|
||||
# other model args
|
||||
@@ -398,7 +405,7 @@ settings:
|
||||
restore_state: True # enables state restoration
|
||||
logger_levels:
|
||||
reader.vm: DEBUG # affects only VmReader logs
|
||||
model: WARNING # applies to all components with 'model' prefix, such as 'model.zscore_online', 'model.prophet', etc.
|
||||
model: WARNING # applies to all components with 'model' prefix, such as 'model.zscore_online', 'model.online.temporal_envelope', etc.
|
||||
# once commented out in hot-reload mode, will use the default logger level set by --loggerLevel command line argument
|
||||
# monitoring.push: critical
|
||||
```
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
build:
|
||||
list: never
|
||||
publishResources: false
|
||||
render: never
|
||||
sitemap:
|
||||
disable: true
|
||||
---
|
||||
|
||||
The required path is `config.yml` → Scheduler → Reader → Model → Writer. The Reader queries the configured VictoriaMetrics, VictoriaLogs, or VictoriaTraces datasource; the Writer stores inferred anomaly scores in VictoriaMetrics. Monitoring is optional and can push metrics or expose them for collection.
|
||||
|
||||
Solid nodes and arrows show the required anomaly-detection path. Dashed nodes and arrows show optional self-monitoring integrations.
|
||||
|
||||

|
||||
{style="display:block; width:80%; min-width:320px; margin:1.5rem auto"}
|
||||
152
docs/anomaly-detection/components/vmanomaly-components.svg
Normal file
@@ -0,0 +1,152 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1920 1600" role="img" aria-labelledby="title description">
|
||||
<title id="title">How vmanomaly operates during a scheduled iteration</title>
|
||||
<desc id="description">The required flow starts from config.yml and the Scheduler. The Reader queries either VictoriaMetrics through query_range or VictoriaLogs and VictoriaTraces through stats_query_range, then sends data to a Model. The Model produces anomaly scores, and the Writer stores them in VictoriaMetrics through import. Reader, Model, and Writer can optionally report self-monitoring metrics using push or pull monitoring.</desc>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 12 12" refX="10.5" refY="6" markerWidth="16" markerHeight="16" markerUnits="userSpaceOnUse" orient="auto-start-reverse">
|
||||
<path d="M1.5 1.5 10.5 6 1.5 10.5" fill="none" stroke="#303038" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</marker>
|
||||
<marker id="arrow-optional" viewBox="0 0 12 12" refX="10.5" refY="6" markerWidth="16" markerHeight="16" markerUnits="userSpaceOnUse" orient="auto-start-reverse">
|
||||
<path d="M1.5 1.5 10.5 6 1.5 10.5" fill="none" stroke="#666a73" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</marker>
|
||||
<style>
|
||||
.label { font-family: Arial, Helvetica, sans-serif; fill: #303038; }
|
||||
.title { font-size: 54px; font-weight: 400; }
|
||||
.service-title { font-size: 46px; font-weight: 400; }
|
||||
.node-text { font-size: 34px; font-weight: 600; text-anchor: middle; }
|
||||
.body { font-size: 28px; }
|
||||
.edge-label { font-size: 25px; }
|
||||
.endpoint-text { font-size: 23px; }
|
||||
.source-text { font-size: 21px; }
|
||||
.required-node { fill: #fff; stroke: #303038; stroke-width: 4; }
|
||||
.optional-node { fill: #fff; stroke: #666a73; stroke-width: 4; stroke-dasharray: 14 12; }
|
||||
.required-line { fill: none; stroke: #303038; stroke-width: 4; marker-end: url(#arrow); }
|
||||
.optional-line { fill: none; stroke: #666a73; stroke-width: 4; stroke-dasharray: 14 12; marker-end: url(#arrow-optional); }
|
||||
.boundary { fill: none; stroke: #303038; stroke-width: 4; }
|
||||
.optional-boundary { fill: none; stroke: #666a73; stroke-width: 4; stroke-dasharray: 14 12; }
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<rect width="1920" height="1600" fill="#fff"/>
|
||||
<text x="105" y="72" class="label title">How does vmanomaly operate (scheduled iteration example)</text>
|
||||
|
||||
<!-- Required / optional legend -->
|
||||
<g aria-label="Legend" transform="translate(1425 145)">
|
||||
<rect x="0" y="0" width="135" height="62" class="optional-node"/>
|
||||
<rect x="205" y="0" width="135" height="62" class="required-node"/>
|
||||
<path d="M0 100 H135" class="optional-line"/>
|
||||
<path d="M205 100 H340" class="required-line"/>
|
||||
<text x="67" y="162" class="label body" text-anchor="middle">Optional</text>
|
||||
<text x="272" y="162" class="label body" text-anchor="middle">Required</text>
|
||||
</g>
|
||||
|
||||
<!-- Exactly one configured read datasource is used. -->
|
||||
<g aria-label="Configured datasource choice">
|
||||
<rect x="25" y="315" width="300" height="550" class="optional-boundary"/>
|
||||
|
||||
<g transform="translate(34 326)">
|
||||
<path d="M8 32 V184 C8 224 280 224 280 184 V32 C280 70 8 70 8 32Z" class="required-node"/>
|
||||
<ellipse cx="144" cy="32" rx="136" ry="33" class="required-node"/>
|
||||
<rect x="30" y="72" width="228" height="66" rx="16" class="required-node"/>
|
||||
<text x="144" y="99" class="label endpoint-text" text-anchor="middle">/query_range</text>
|
||||
<text x="144" y="126" class="label endpoint-text" text-anchor="middle">endpoint</text>
|
||||
<text x="144" y="158" class="label source-text" text-anchor="middle">VictoriaMetrics</text>
|
||||
<text x="144" y="182" class="label source-text" text-anchor="middle">TSDB</text>
|
||||
</g>
|
||||
|
||||
<text x="178" y="600" class="label body" text-anchor="middle">OR</text>
|
||||
|
||||
<g transform="translate(34 632)">
|
||||
<path d="M8 32 V184 C8 224 280 224 280 184 V32 C280 70 8 70 8 32Z" class="required-node"/>
|
||||
<ellipse cx="144" cy="32" rx="136" ry="33" class="required-node"/>
|
||||
<rect x="30" y="72" width="228" height="66" rx="16" class="required-node"/>
|
||||
<text x="144" y="99" class="label endpoint-text" text-anchor="middle">/stats_query_range</text>
|
||||
<text x="144" y="126" class="label endpoint-text" text-anchor="middle">endpoint</text>
|
||||
<text x="144" y="158" class="label source-text" text-anchor="middle">VictoriaLogs /</text>
|
||||
<text x="144" y="182" class="label source-text" text-anchor="middle">VictoriaTraces</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- vmanomaly service boundary and components -->
|
||||
<g aria-label="vmanomaly service">
|
||||
<rect x="720" y="145" width="610" height="1145" class="boundary"/>
|
||||
<rect x="1005" y="175" width="270" height="100" class="required-node"/>
|
||||
<text x="1140" y="235" class="label node-text">config.yml</text>
|
||||
<text x="1025" y="352" class="label service-title" text-anchor="middle">vmanomaly</text>
|
||||
<text x="1025" y="405" class="label service-title" text-anchor="middle">service</text>
|
||||
|
||||
<rect x="765" y="448" width="445" height="76" rx="16" class="required-node"/>
|
||||
<text x="987" y="500" class="label node-text">Scheduler</text>
|
||||
<rect x="765" y="590" width="445" height="76" rx="16" class="required-node"/>
|
||||
<text x="987" y="642" class="label node-text">Reader</text>
|
||||
<rect x="765" y="732" width="445" height="76" rx="16" class="required-node"/>
|
||||
<text x="987" y="784" class="label node-text">Model</text>
|
||||
<rect x="765" y="874" width="445" height="76" rx="16" class="required-node"/>
|
||||
<text x="987" y="926" class="label node-text">Writer</text>
|
||||
<rect x="778" y="1180" width="420" height="82" rx="16" class="optional-node"/>
|
||||
<text x="988" y="1235" class="label node-text">Monitoring</text>
|
||||
|
||||
<path d="M1005 225 H900 V448" class="required-line"/>
|
||||
<path d="M1210 486 H1290 V628 H1210" class="required-line"/>
|
||||
<path d="M1210 628 H1290 V770 H1210" class="required-line"/>
|
||||
<path d="M1210 770 H1290 V912 H1210" class="required-line"/>
|
||||
|
||||
<path d="M875 666 V1178" class="optional-line"/>
|
||||
<path d="M987 808 V1178" class="optional-line"/>
|
||||
<path d="M1100 950 V1178" class="optional-line"/>
|
||||
</g>
|
||||
|
||||
<!-- Required flow labels -->
|
||||
<text x="1365" y="455" class="label body">
|
||||
<tspan x="1365" dy="0">1. Get the metrics to</tspan>
|
||||
<tspan x="1365" dy="37">a. fit the model, or</tspan>
|
||||
<tspan x="1365" dy="37">b. produce anomaly scores</tspan>
|
||||
</text>
|
||||
<text x="1365" y="620" class="label body">
|
||||
<tspan x="1365" dy="0">3. Model receives data</tspan>
|
||||
<tspan x="1365" dy="37">to train or infer on</tspan>
|
||||
</text>
|
||||
<text x="1365" y="775" class="label body">
|
||||
<tspan x="1365" dy="0">4. Produce anomaly</tspan>
|
||||
<tspan x="1365" dy="37">scores (inference)</tspan>
|
||||
</text>
|
||||
|
||||
<!-- Datasource request and response -->
|
||||
<path d="M765 610 H338" class="required-line"/>
|
||||
<path d="M338 652 H765" class="required-line"/>
|
||||
<text x="370" y="580" class="label edge-label">2.1 Query request</text>
|
||||
<text x="370" y="700" class="label edge-label">2.2 Metrics response</text>
|
||||
|
||||
<!-- Anomaly-score output -->
|
||||
<g aria-label="VictoriaMetrics write endpoint" transform="translate(26 1145)">
|
||||
<path d="M8 32 V184 C8 224 290 224 290 184 V32 C290 70 8 70 8 32Z" class="required-node"/>
|
||||
<ellipse cx="149" cy="32" rx="141" ry="33" class="required-node"/>
|
||||
<rect x="34" y="72" width="230" height="66" rx="16" class="required-node"/>
|
||||
<text x="149" y="99" class="label endpoint-text" text-anchor="middle">/import</text>
|
||||
<text x="149" y="126" class="label endpoint-text" text-anchor="middle">endpoint</text>
|
||||
<text x="149" y="158" class="label source-text" text-anchor="middle">VictoriaMetrics</text>
|
||||
<text x="149" y="182" class="label source-text" text-anchor="middle">TSDB</text>
|
||||
</g>
|
||||
<path d="M765 912 H650 V1095 L316 1197" class="required-line"/>
|
||||
<rect x="338" y="1018" width="292" height="74" fill="#fff"/>
|
||||
<text x="350" y="1048" class="label edge-label">
|
||||
<tspan x="340" dy="0">5. Write anomaly scores</tspan>
|
||||
<tspan x="340" dy="33">to VictoriaMetrics</tspan>
|
||||
</text>
|
||||
|
||||
<!-- Optional monitoring integrations -->
|
||||
<rect x="748" y="1083" width="490" height="48" fill="#fff"/>
|
||||
<text x="993" y="1116" class="label edge-label" text-anchor="middle">6. Produce self-monitoring metrics</text>
|
||||
<rect x="390" y="1460" width="300" height="118" rx="45" class="optional-node"/>
|
||||
<text x="540" y="1515" class="label body" text-anchor="middle">Monitoring system</text>
|
||||
<text x="540" y="1552" class="label body" text-anchor="middle">push approach</text>
|
||||
<rect x="1490" y="1460" width="300" height="118" rx="45" class="optional-node"/>
|
||||
<text x="1640" y="1515" class="label body" text-anchor="middle">Monitoring system</text>
|
||||
<text x="1640" y="1552" class="label body" text-anchor="middle">pull approach</text>
|
||||
<path d="M850 1262 L540 1460" class="optional-line"/>
|
||||
<path d="M1198 1222 L1640 1460" class="optional-line"/>
|
||||
<rect x="590" y="1350" width="180" height="42" fill="#fff"/>
|
||||
<text x="680" y="1380" class="label edge-label" text-anchor="middle">Push metrics</text>
|
||||
<rect x="1230" y="1340" width="415" height="42" fill="#fff"/>
|
||||
<text x="1438" y="1370" class="label edge-label" text-anchor="middle">HTTP GET /metrics or /health</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 107 KiB |
@@ -17,6 +17,10 @@ Future updates will introduce additional export methods, offering users more fle
|
||||
|
||||
## VM writer
|
||||
|
||||
<div class="collapse-group">
|
||||
|
||||
{{% collapse name="VM writer config parameters and example" %}}
|
||||
|
||||
### Config parameters
|
||||
|
||||
<table class="params">
|
||||
@@ -35,8 +39,7 @@ Future updates will introduce additional export methods, offering users more fle
|
||||
</td>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`writer.vm.VmWriter` or `vm`{{% available_from "v1.13.0" anomaly %}}
|
||||
</span>
|
||||
`writer.vm.VmWriter` or `vm`{{% available_from "v1.13.0" anomaly %}}
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -63,10 +66,7 @@ Datasource URL address
|
||||
<span style="white-space: nowrap;">`tenant_id`</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>
|
||||
|
||||
`0:0`, `multitenant`{{% available_from "v1.16.2" anomaly %}}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -253,9 +253,8 @@ Token is passed in the standard format with header: `Authorization: bearer {toke
|
||||
`path_to_file`
|
||||
</td>
|
||||
<td>
|
||||
<span>
|
||||
Path to a file, which contains token, that is passed in the standard format with header: `Authorization: bearer {token}`{{% available_from "v1.15.9" anomaly %}}
|
||||
</span> </td>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
@@ -267,9 +266,8 @@ Path to a file, which contains token, that is passed in the standard format with
|
||||
`1`
|
||||
</td>
|
||||
<td>
|
||||
<span>
|
||||
Number of attempts to retry the connection in case of failure {{% available_from "v1.29.2" anomaly %}}.
|
||||
</span> </td>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -293,6 +291,10 @@ writer:
|
||||
connection_retry_attempts: 2 # if not specified, it will be 1 by default
|
||||
```
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
</div>
|
||||
|
||||
### Multitenancy support
|
||||
|
||||
> This feature applies to the VictoriaMetrics Cluster version only. Tenants are identified by either `accountID` or `accountID:projectID`. `multitenant` [endpoint](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#multitenancy-via-labels){{% available_from "v1.15.9" anomaly %}} is supported for writing data across multiple [tenants](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#multitenancy). For more details, refer to the VictoriaMetrics Cluster [multitenancy documentation](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#multitenancy).
|
||||
|
||||
@@ -10,14 +10,14 @@ sitemap:
|
||||
|
||||
- To use *vmanomaly*, part of the enterprise package, a license key is required. Obtain your key [here](https://victoriametrics.com/products/enterprise/trial/) for this tutorial or for enterprise use.
|
||||
- In the tutorial, we'll be using the following VictoriaMetrics components:
|
||||
- [VictoriaMetrics Single-Node](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) (v1.148.0)
|
||||
- [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/) (v1.148.0)
|
||||
- [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/) (v1.148.0)
|
||||
- [VictoriaMetrics Single-Node](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) (v1.149.0)
|
||||
- [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/) (v1.149.0)
|
||||
- [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/) (v1.149.0)
|
||||
- [Grafana](https://grafana.com/) (v12.2.0)
|
||||
- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/)
|
||||
- [Node exporter](https://github.com/prometheus/node_exporter#node-exporter) (v1.9.1) and [Alertmanager](https://prometheus.io/docs/alerting/latest/alertmanager/) (v0.28.1)
|
||||
|
||||

|
||||

|
||||
|
||||
> **Configurations used throughout this guide can be found [here](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker/vmanomaly/vmanomaly-integration/)**
|
||||
|
||||
@@ -323,7 +323,7 @@ Let's wrap it all up together into the `docker-compose.yml` file.
|
||||
services:
|
||||
vmagent:
|
||||
container_name: vmagent
|
||||
image: victoriametrics/vmagent:v1.148.0
|
||||
image: victoriametrics/vmagent:v1.149.0
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
@@ -340,7 +340,7 @@ services:
|
||||
|
||||
victoriametrics:
|
||||
container_name: victoriametrics
|
||||
image: victoriametrics/victoria-metrics:v1.148.0
|
||||
image: victoriametrics/victoria-metrics:v1.149.0
|
||||
ports:
|
||||
- 8428:8428
|
||||
volumes:
|
||||
@@ -373,7 +373,7 @@ services:
|
||||
|
||||
vmalert:
|
||||
container_name: vmalert
|
||||
image: victoriametrics/vmalert:v1.148.0
|
||||
image: victoriametrics/vmalert:v1.149.0
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
@@ -395,7 +395,7 @@ services:
|
||||
restart: always
|
||||
vmanomaly:
|
||||
container_name: vmanomaly
|
||||
image: victoriametrics/vmanomaly:v1.30.0
|
||||
image: victoriametrics/vmanomaly:v1.30.1
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1706 1069" role="img" aria-labelledby="title description">
|
||||
<title id="title">Typical vmanomaly observability pipeline</title>
|
||||
<desc id="description">vmagent scrapes node-exporter metrics and writes them to VictoriaMetrics. vmanomaly reads those metrics and writes anomaly scores back. Grafana visualizes the results. vmalert evaluates rules based on anomaly scores and sends alerts to Alertmanager.</desc>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
|
||||
<path d="M0 0 10 5 0 10Z" fill="#252525"/>
|
||||
</marker>
|
||||
<style>
|
||||
text { font-family: Arial, Helvetica, sans-serif; fill: #252525; font-size: 40px; }
|
||||
.box { fill: #fff; stroke: #252525; stroke-width: 3; }
|
||||
.arrow { fill: none; stroke: #252525; stroke-width: 4; marker-end: url(#arrow); }
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<rect width="1706" height="1069" fill="#fff"/>
|
||||
|
||||
<rect x="20" y="55" width="390" height="100" class="box"/>
|
||||
<text x="215" y="119" text-anchor="middle">node-exporter</text>
|
||||
<rect x="730" y="50" width="395" height="100" class="box"/>
|
||||
<text x="928" y="115" text-anchor="middle">vmagent</text>
|
||||
<path d="M710 100 H435" class="arrow"/>
|
||||
<text x="570" y="56" text-anchor="middle">Scrape metrics</text>
|
||||
|
||||
<rect x="745" y="325" width="400" height="180" class="box"/>
|
||||
<text x="945" y="405" text-anchor="middle">VictoriaMetrics</text>
|
||||
<text x="945" y="457" text-anchor="middle">TSDB</text>
|
||||
<path d="M945 150 V322" class="arrow"/>
|
||||
<text x="1018" y="204">
|
||||
<tspan x="1018" dy="0">Push node</tspan>
|
||||
<tspan x="1018" dy="50">exporter</tspan>
|
||||
<tspan x="1018" dy="50">metrics</tspan>
|
||||
</text>
|
||||
|
||||
<rect x="25" y="340" width="395" height="100" class="box"/>
|
||||
<text x="222" y="405" text-anchor="middle">vmanomaly</text>
|
||||
<path d="M725 365 H440" class="arrow"/>
|
||||
<text x="575" y="319" text-anchor="middle">Read metrics</text>
|
||||
<path d="M440 421 H725" class="arrow"/>
|
||||
<text x="570" y="508" text-anchor="middle">
|
||||
<tspan x="570" dy="0">Write produced</tspan>
|
||||
<tspan x="570" dy="50">anomaly scores</tspan>
|
||||
</text>
|
||||
|
||||
<rect x="1440" y="330" width="245" height="170" class="box"/>
|
||||
<text x="1562" y="430" text-anchor="middle">Grafana</text>
|
||||
<path d="M1160 417 H1418" class="arrow"/>
|
||||
<text x="1285" y="486" text-anchor="middle">
|
||||
<tspan x="1285" dy="0">Visualize the</tspan>
|
||||
<tspan x="1285" dy="50">results</tspan>
|
||||
</text>
|
||||
|
||||
<rect x="750" y="738" width="395" height="100" class="box"/>
|
||||
<text x="948" y="805" text-anchor="middle">vmalert</text>
|
||||
<path d="M946 525 V716" class="arrow"/>
|
||||
<text x="1000" y="644">
|
||||
<tspan x="1000" dy="0">Evaluate rules based</tspan>
|
||||
<tspan x="1000" dy="50">on anomaly scores</tspan>
|
||||
</text>
|
||||
|
||||
<rect x="755" y="948" width="395" height="100" class="box"/>
|
||||
<text x="952" y="1015" text-anchor="middle">alertmanager</text>
|
||||
<path d="M948 858 V928" class="arrow"/>
|
||||
<text x="1004" y="906">Send alerts</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 29 KiB |
107
docs/anomaly-detection/vmanomaly-sharding-ha-diagram.d2
Normal file
@@ -0,0 +1,107 @@
|
||||
# Render with D2 v0.7.1:
|
||||
# d2 --layout elk --theme 0 --pad 20 vmanomaly-sharding-ha-diagram.d2 vmanomaly-sharding-ha-diagram.svg
|
||||
|
||||
grid-columns: 1
|
||||
grid-gap: 36
|
||||
|
||||
classes: {
|
||||
boundary: {
|
||||
style.fill: "#F7F7F8"
|
||||
style.stroke: "#A7AAB2"
|
||||
style.stroke-width: 2
|
||||
}
|
||||
process: {
|
||||
style.fill: "#FFFFFF"
|
||||
style.stroke: "#303038"
|
||||
style.stroke-width: 2
|
||||
style.border-radius: 8
|
||||
}
|
||||
member: {
|
||||
style.fill: "#F1F3F5"
|
||||
style.stroke: "#59616A"
|
||||
style.stroke-width: 2
|
||||
style.border-radius: 8
|
||||
}
|
||||
}
|
||||
|
||||
flow: "" {
|
||||
grid-columns: 3
|
||||
grid-gap: 36
|
||||
style.fill: transparent
|
||||
style.stroke: transparent
|
||||
|
||||
global: "Global YAML\nconfiguration" {
|
||||
shape: page
|
||||
class: process
|
||||
}
|
||||
|
||||
splitting: Configuration splitting {
|
||||
class: boundary
|
||||
grid-columns: 1
|
||||
grid-gap: 24
|
||||
|
||||
split: "Split by VMANOMALY_SPLIT_BY\ndefault: complete" {
|
||||
class: process
|
||||
}
|
||||
subconfigs: "N valid sub-configurations\nn = 0 ... N-1" {
|
||||
class: process
|
||||
style.multiple: true
|
||||
}
|
||||
|
||||
split -> subconfigs: {
|
||||
style.stroke: "#303038"
|
||||
}
|
||||
}
|
||||
|
||||
placement: Deterministic placement {
|
||||
class: boundary
|
||||
grid-columns: 1
|
||||
grid-gap: 24
|
||||
|
||||
settings: "K members: VMANOMALY_MEMBERS_COUNT\nMember index: VMANOMALY_MEMBER_NUM\nR replicas: VMANOMALY_REPLICATION_FACTOR" {
|
||||
class: process
|
||||
}
|
||||
assign: "Assign every sub-configuration\nto exactly R members" {
|
||||
class: process
|
||||
}
|
||||
|
||||
settings -> assign: {
|
||||
style.stroke: "#303038"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
members: K vmanomaly members {
|
||||
class: boundary
|
||||
grid-columns: 3
|
||||
grid-gap: 30
|
||||
|
||||
member0: "Member 0\nvmanomaly service 1" {
|
||||
class: member
|
||||
}
|
||||
member1: "Member 1\nvmanomaly service 2" {
|
||||
class: member
|
||||
}
|
||||
memberK: "Member K-1\nvmanomaly service K" {
|
||||
class: member
|
||||
}
|
||||
}
|
||||
|
||||
flow.global -> flow.splitting.split: {
|
||||
style.stroke: "#303038"
|
||||
}
|
||||
flow.splitting.subconfigs -> flow.placement.assign: {
|
||||
style.stroke: "#303038"
|
||||
}
|
||||
flow.placement.assign -> members.member0: "assigned subset" {
|
||||
style.stroke: "#59616A"
|
||||
style.stroke-width: 2
|
||||
}
|
||||
flow.placement.assign -> members.member1: "assigned subset" {
|
||||
style.stroke: "#59616A"
|
||||
style.stroke-width: 2
|
||||
}
|
||||
flow.placement.assign -> members.memberK: "assigned subset" {
|
||||
style.stroke: "#59616A"
|
||||
style.stroke-width: 2
|
||||
}
|
||||
13
docs/anomaly-detection/vmanomaly-sharding-ha-diagram.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
build:
|
||||
list: never
|
||||
publishResources: false
|
||||
render: never
|
||||
sitemap:
|
||||
disable: true
|
||||
---
|
||||
|
||||
The global configuration is split into `N` independently valid sub-configurations. Deterministic placement distributes them across `K` members, and each sub-configuration is assigned to exactly `R` members when replication is enabled. Member `k` processes only its assigned subset.
|
||||
|
||||

|
||||
{style="display:block; width:50%; min-width:320px; margin:1.5rem auto"}
|
||||
111
docs/anomaly-detection/vmanomaly-sharding-ha-diagram.svg
Normal file
|
After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 87 KiB |
@@ -1,3 +1,11 @@
|
||||
---
|
||||
build:
|
||||
list: never
|
||||
publishResources: false
|
||||
render: never
|
||||
sitemap:
|
||||
disable: true
|
||||
---
|
||||
Several VictoriaMetrics components can connect to cloud storage to read or write object data.
|
||||
|
||||
The following table shows the supported types of storage for each component:
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
build:
|
||||
list: never
|
||||
publishResources: false
|
||||
render: never
|
||||
sitemap:
|
||||
disable: true
|
||||
---
|
||||
Using [Grafana](https://grafana.com/) with [vmauth](https://docs.victoriametrics.com/victoriametrics/vmauth/) is an effective way to provide [multi-tenant](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#multitenancy) access to your metrics, logs, and traces.
|
||||
vmauth provides a way to authenticate users using [JWT tokens](https://en.wikipedia.org/wiki/JSON_Web_Token) {{% available_from "v1.138.0" %}} issued by an external identity provider.
|
||||
Those tokens can include information about the user and their tenant, which vmauth can use to restrict access so users only see metrics in their own tenant.
|
||||
@@ -240,23 +248,23 @@ vmagent will write data into VictoriaMetrics single-node and cluster (with tenan
|
||||
# compose.yaml
|
||||
services:
|
||||
vmsingle:
|
||||
image: victoriametrics/victoria-metrics:v1.148.0
|
||||
image: victoriametrics/victoria-metrics:v1.149.0
|
||||
|
||||
vmstorage:
|
||||
image: victoriametrics/vmstorage:v1.148.0-cluster
|
||||
image: victoriametrics/vmstorage:v1.149.0-cluster
|
||||
|
||||
vminsert:
|
||||
image: victoriametrics/vminsert:v1.148.0-cluster
|
||||
image: victoriametrics/vminsert:v1.149.0-cluster
|
||||
command:
|
||||
- -storageNode=vmstorage:8400
|
||||
|
||||
vmselect:
|
||||
image: victoriametrics/vmselect:v1.148.0-cluster
|
||||
image: victoriametrics/vmselect:v1.149.0-cluster
|
||||
command:
|
||||
- -storageNode=vmstorage:8401
|
||||
|
||||
vmagent:
|
||||
image: victoriametrics/vmagent:v1.148.0
|
||||
image: victoriametrics/vmagent:v1.149.0
|
||||
volumes:
|
||||
- ./scrape.yaml:/etc/vmagent/config.yaml
|
||||
command:
|
||||
@@ -308,7 +316,7 @@ Now add the vmauth service to `compose.yaml`:
|
||||
# compose.yaml
|
||||
services:
|
||||
vmauth:
|
||||
image: docker.io/victoriametrics/vmauth:v1.148.0
|
||||
image: docker.io/victoriametrics/vmauth:v1.149.0
|
||||
ports:
|
||||
- 8427:8427
|
||||
volumes:
|
||||
|
||||
@@ -6,255 +6,80 @@ build:
|
||||
sitemap:
|
||||
disable: true
|
||||
---
|
||||
### Scenario
|
||||
|
||||
## Overview {#scenario}
|
||||
Let's cover the case. You have multiple regions with workloads and want to collect metrics.
|
||||
|
||||
This guide shows how to run VictoriaMetrics across many regions in high-availability mode. Each workload runs a local vmagent and sends metrics to dedicated monitoring deployments, so metric data is duplicated and available even if one monitoring region is down.
|
||||
The monitoring setup is in the dedicated regions as shown below:
|
||||
|
||||
Use this architecture when you need region-level resilience and want monitoring to keep working even if one region becomes unavailable.
|
||||

|
||||
|
||||
This setup gives you:
|
||||
Every workload region (Earth, Mars, Venus) has a vmagent that sends data to multiple regions with a monitoring setup.
|
||||
The monitoring setup (Ground Control 1,2) contains VictoriaMetrics Time Series Database(TSDB) cluster or single.
|
||||
|
||||
* High availability of metric data across regions.
|
||||
* A single global query endpoint.
|
||||
* Simpler disaster recovery.
|
||||
Using this schema, you can achieve:
|
||||
|
||||
The trade-off is that you store and send the same data twice, so storage and compute requirements are increased.
|
||||
* Global Querying View
|
||||
* Querying all metrics from one monitoring installation
|
||||
* High Availability
|
||||
* You can lose one region, but your experience will be the same.
|
||||
* Of course, that means you duplicate your traffic twice.
|
||||
|
||||
## Architecture
|
||||
### How to write the data to Ground Control regions
|
||||
|
||||
The example architecture separates workloads into three regions, called Earth, Mars, and Venus. These represent the systems you want to monitor (e.g., your applications or your infrastructure). For monitoring, there are two separate regions, Ground Control 1 and 2, each running its own VictoriaMetrics deployment. The workload regions (the planets) run a local vmagent that forwards the same metrics to the two dedicated Ground Control regions.
|
||||
* You need to pass two `-remoteWrite.url` command-line options to `vmagent`:
|
||||
|
||||

|
||||
{width="700"}
|
||||
```sh
|
||||
/path/to/vmagent-prod \
|
||||
-remoteWrite.url=<ground-control-1-remote-write> \
|
||||
-remoteWrite.url=<ground-control-2-remote-write>
|
||||
```
|
||||
|
||||
* If you scrape data from Prometheus-compatible targets, then please specify `-promscrape.config` parameter as well.
|
||||
|
||||
Here is a Quickstart guide for [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/#quick-start)
|
||||
|
||||
### How to read the data from Ground Control regions
|
||||
|
||||
You can use one of the following options:
|
||||
|
||||
1. Multi-level [vmselect setup](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#multi-level-cluster-setup) in cluster setup, top-level vmselect(s) reads data from cluster-level vmselects
|
||||
* Returns data in one of the clusters is unavailable
|
||||
* Merges data from both sources. You need to turn on [deduplication](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#deduplication) to remove duplicates
|
||||
1. Regional endpoints - use one regional endpoint as default and switch to another if there is an issue.
|
||||
1. Load balancer - that sends queries to a particular region. The benefit and disadvantage of this setup is that it's simple.
|
||||
1. Promxy - proxy that reads data from multiple Prometheus-like sources. It allows reading data more intelligently to cover the region's unavailability out of the box. It doesn't support MetricsQL yet (please check this issue).
|
||||
1. Global vmselect in cluster setup - you can set up an additional subset of vmselects that knows about all storages in all regions.
|
||||
* The [deduplication](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#deduplication) in 1ms on the vmselect side must be turned on. This setup allows you to query data using MetricsQL.
|
||||
* The downside is that vmselect waits for a response from all storages in all regions.
|
||||
|
||||
The role of the Ground Controls can be filled by VictoriaMetrics in [single-node](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) or [cluster mode](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/).
|
||||
|
||||
### High Availability
|
||||
|
||||
The architecture provides high availability by storing two full copies of the data: one in Ground Control 1 and the other in Ground Control 2. Since both store the same data, losing one region doesn't result in a monitoring outage. You can still run queries, view dashboards, and receive alerts.
|
||||
The data is duplicated twice, and every region contains a full copy of the data. That means one region can be offline.
|
||||
|
||||
vmagent keeps a separate persistent queue for each `-remoteWrite.url` destination. If one Ground Control region is unavailable, vmagent continues sending data to the other region. The samples for the unavailable region stay in the queue, and vmagent delivers them after the region recovers. This helps restore consistency across both regions.
|
||||
You don't need to set up a replication factor using the VictoriaMetrics cluster.
|
||||
|
||||
This setup provides two logical copies of the data in separate monitoring regions. That lets you fail over to the healthy region if one region becomes unavailable, or spread read load across both regions if needed.
|
||||
### Alerting
|
||||
|
||||
## How to write the data to Ground Control regions
|
||||
You can set up vmalert in each Ground control region that evaluates recording and alerting rules. As every region contains a full copy of the data, you don't need to synchronize recording rules from one region to another.
|
||||
|
||||
Run one or more vmagent nodes in each workload region and configure them to send metrics to both Ground Control regions. This gives each workload region a local write path and keeps delivery going if one monitoring region is unavailable.
|
||||
For alert deduplication, please use [cluster mode in Alertmanager](https://prometheus.io/docs/alerting/latest/alertmanager/#high-availability).
|
||||
|
||||
For example, a vmagent that sends data to two single-node VictoriaMetrics instances looks like this:
|
||||
We also recommend adopting the list of [alerting rules](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker#alerts)
|
||||
for VictoriaMetrics components.
|
||||
|
||||
```sh
|
||||
/path/to/vmagent-prod \
|
||||
-remoteWrite.url=https://ground-control-1:8428/api/v1/write \
|
||||
-remoteWrite.url=https://ground-control-2:8428/api/v1/write
|
||||
```
|
||||
### Monitoring
|
||||
|
||||
For a VictoriaMetrics cluster, use the following URLs for [`accountID=0`](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#multitenancy)
|
||||
An additional VictoriaMetrics single can be set up in every region, scraping metrics from the main TSDB.
|
||||
|
||||
```sh
|
||||
/path/to/vmagent-prod \
|
||||
-remoteWrite.url=https://ground-control-1-vminsert:8480/insert/0/prometheus/api/v1/write \
|
||||
-remoteWrite.url=https://ground-control-2-vminsert:8480/insert/0/prometheus/api/v1/write
|
||||
```
|
||||
For more details, see [data ingestion with vmagent](https://docs.victoriametrics.com/victoriametrics/data-ingestion/vmagent/).
|
||||
You also may evaluate the option to send these metrics to the neighbour region to achieve HA.
|
||||
|
||||
## How to read the data from Ground Control regions
|
||||
Additional context
|
||||
* VictoriaMetrics Single - [https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#monitoring](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#monitoring)
|
||||
* VictoriaMetrics Cluster - [https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#monitoring](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#monitoring)
|
||||
|
||||
You can read data from Ground Control regions in a few different ways. The best option depends on your needs and operational complexity:
|
||||
|
||||
* Regional endpoints: use one region endpoint as default and manually switch to the other during an outage. This is the simplest option but needs manual failover.
|
||||
* Load balancer: put a load balancer in front of both Ground Control regions. Route traffic to a preferred region, with automatic failover to the other region in case of failure.
|
||||
* Multi-level vmselect: run a dedicated vmselect on top of the Ground Control local vmselect nodes. This setup also requires both Ground Control instances to run in cluster mode.
|
||||
|
||||
You can read more about choosing the right architecture in the [VictoriaMetrics topologies guide](https://docs.victoriametrics.com/guides/vm-architectures/).
|
||||
|
||||
### Regional endpoints
|
||||
|
||||
In this setup, Grafana, vmalert, or any other query client sends requests to one region. This is the default datasource. In case of an outage, you manually switch to the other region (standby datasource). For instance, use Ground Control 1 as the primary datasource and keep Ground Control 2 as a standby endpoint.
|
||||
|
||||

|
||||
{width="700"}
|
||||
|
||||
Choose this option if you prioritize operational simplicity over automatic failover or a unified global query endpoint.
|
||||
|
||||
If you use VictoriaMetrics single-node, the endpoints should point directly to the single-node HTTP API. For example:
|
||||
|
||||
- Primary endpoint: `https://ground-control-1:8428/api/v1/query`
|
||||
- Standby endpoint: `https://ground-control-2:8428/api/v1/query`
|
||||
|
||||
On the VictoriaMetrics cluster, the endpoints point to the cluster's vmselect HTTP API. For example:
|
||||
|
||||
- Primary endpoint: `https://ground-control-1-vmselect:8481/select/0/prometheus/api/v1/query`
|
||||
- Standby endpoint: `https://ground-control-2-vmselect:8481/select/0/prometheus/api/v1/query`
|
||||
|
||||
### Load balancer
|
||||
|
||||
Use a load balancer when you want one stable query endpoint in front of your Ground Control regions. In this setup, dashboards and tools send queries to a single URL, and vmauth routes each request to one available region.
|
||||
|
||||
The following diagram shows [vmauth](https://docs.victoriametrics.com/victoriametrics/vmauth/) performing the role of load balancer.
|
||||
|
||||

|
||||
{width="700"}
|
||||
|
||||
This approach is faster than merging results from multiple regions, because each query goes to only one region. It can also reduce query latency by roughly half compared with a topology that reads and merges data from both regions.
|
||||
|
||||
The main downside is that vmauth does not know whether a recovered region has already finished replaying delayed data from the vmagent queue. If you send queries to that region too early, recent data may still be incomplete. In that case, it is better to wait until the region catches up before routing traffic there.
|
||||
|
||||
For VictoriaMetrics single node, you can vmauth it with the following configuration:
|
||||
|
||||
```yaml
|
||||
unauthorized_user:
|
||||
url_prefix:
|
||||
- "http://ground-control-1:8428"
|
||||
- "http://ground-control-2:8428"
|
||||
load_balancing_policy: first_available
|
||||
```
|
||||
|
||||
On the VictoriaMetrics cluster, the URLs must point to the Ground Control vmselect nodes. For example:
|
||||
|
||||
```yaml
|
||||
unauthorized_user:
|
||||
url_prefix:
|
||||
- "http://ground-control-1-vmselect:8481"
|
||||
- "http://ground-control-2-vmselect:8481"
|
||||
load_balancing_policy: first_available
|
||||
```
|
||||
|
||||
The examples above show how to load balance requests without authentication. You can optionally configure authentication in several ways; for more details, read the [vmauth authorization section](https://docs.victoriametrics.com/victoriametrics/vmauth/#authorization).
|
||||
|
||||
To start vmauth with your configuration, use the `-auth.config` flag. For example:
|
||||
|
||||
```sh
|
||||
/path/to/vmauth-prod -auth.config=/path/to/auth.yaml
|
||||
```
|
||||
|
||||
You can test that queries work with curl:
|
||||
|
||||
```sh
|
||||
# single node
|
||||
curl http://vmauth-node:8427/api/v1/query?query=up
|
||||
|
||||
# cluster
|
||||
curl http://vmauth-node:8427/select/0/prometheus/api/v1/query?query=up
|
||||
```
|
||||
|
||||
For an example of this topology in Kubernetes, see the [`VMDistributed` resource](https://docs.victoriametrics.com/helm/victoriametrics-k8s-stack/#vmdistributed-enabled).
|
||||
|
||||
### Multi-level vmselect
|
||||
|
||||
> This option requires that Ground Control regions are deployed in one of these modes:
|
||||
> - As a [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/).
|
||||
> - Or as VictoriaMetrics [single-node with multitenant support enabled](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#multi-tenancy). In other words, VictoriaMetrics should be started with the optional `-vmselectAddr=:8401` command line flag to enable the vmselect RPC server.
|
||||
|
||||
In this setup, each Ground Control region has its own local vmselect. A top-level vmselect queries these instead of connecting directly to vmstorage nodes.
|
||||
|
||||

|
||||
{width="700"}
|
||||
|
||||
This option is useful when direct access to vmstorage nodes is not practical or desirable. For example, when running on Kubernetes, the vmstorage services don't provide an HTTP query endpoint by default.
|
||||
|
||||
To enable this setup, each Ground Control regional vmselect must listen for requests from the top layer by setting the `-clusternativeListenAddr` flag. The top-level vmselect must then use `-storageNode` to point to the regional vmselect nodes and must set a [deduplication](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#deduplication) interval to handle duplicated data.
|
||||
|
||||
For example, here's how we can run the local cluster vmselect nodes and a top-level vmselect node:
|
||||
|
||||
```sh
|
||||
# Ground Control 1 cluster vmselect
|
||||
/path/to/vmselect-prod \
|
||||
-storageNode=ground-control-1-vmstorage-1:8401,ground-control-1-vmstorage-2:8401 \
|
||||
-clusternativeListenAddr=:8401
|
||||
|
||||
# Ground Control 2 cluster vmselect
|
||||
/path/to/vmselect-prod \
|
||||
-storageNode=ground-control-2-vmstorage-1:8401,ground-control-2-vmstorage-2:8401 \
|
||||
-clusternativeListenAddr=:8401
|
||||
|
||||
# Top-level vmselect
|
||||
/path/to/vmselect-prod \
|
||||
-storageNode=ground-control-1-vmselect:8401,ground-control-2-vmselect:8401 \
|
||||
-dedup.minScrapeInterval=1ms \
|
||||
-replicationFactor=2
|
||||
```
|
||||
|
||||
This option provides a single query endpoint for both Ground Control regions. If one region becomes unavailable, the global vmselect can still query the healthy region, so dashboards and queries can continue to work.
|
||||
|
||||
The main trade-off is performance. In a two-level vmselect topology, queries pass through two query layers, so they usually take longer than using regional endpoints directly, or through a load balancer. The benefit is that the topology is easy to understand; it keeps working if one region is lost, and it can merge data from both regions while one region is still catching up after recovery.
|
||||
|
||||
## Alerting
|
||||
|
||||
Run a vmalert node in each Ground Control region and point it to the local VictoriaMetrics endpoint. Since each region stores the same data, you can deploy the same alerting and recording rules in every region without needing cross-region rule synchronization. Send alerts to an [Alertmanager cluster](https://prometheus.io/docs/alerting/latest/alertmanager/#high-availability) to deduplicate firing alerts.
|
||||
|
||||

|
||||
{width="700"}
|
||||
|
||||
A simple vmalert example for a single-node VictoriaMetrics looks like this:
|
||||
|
||||
```sh
|
||||
/path/to/vmalert \
|
||||
-rule=/path/to/rules.yaml \
|
||||
-datasource.url=http://ground-control-1:8428 \
|
||||
-notifier.url=http://alertmanager-1:9093 \
|
||||
-notifier.url=http://alertmanager-2:9093
|
||||
```
|
||||
|
||||
In VictoriaMetrics cluster mode, point `-datasource.url` to the regional vmselect endpoint. For example:
|
||||
|
||||
```sh
|
||||
/path/to/vmalert \
|
||||
-rule=/path/to/rules.yaml \
|
||||
-datasource.url=http://ground-control-1-vmselect:8481/select/0/prometheus \
|
||||
-notifier.url=http://alertmanager-1:9093,http://alertmanager-2:9093
|
||||
```
|
||||
|
||||
If you want vmalert to preserve alert state and recording rule results across restarts, configure `-remoteWrite.url` and `-remoteRead.url` to point to VictoriaMetrics as well. For example, for a VictoriaMetrics cluster:
|
||||
|
||||
```sh
|
||||
/path/to/vmalert \
|
||||
-rule=/path/to/rules.yaml \
|
||||
-datasource.url=http://ground-control-1-vmselect:8481/select/0/prometheus \
|
||||
-remoteRead.url=http://ground-control-1-vmselect:8481/select/0/prometheus \
|
||||
-remoteWrite.url=http://ground-control-1-vminsert:8480/insert/0/prometheus \
|
||||
-notifier.url=http://alertmanager-1:9093,http://alertmanager-2:9093
|
||||
```
|
||||
|
||||
We recommend using the list of [VictoriaMetrics alerting rules](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker#alerts).
|
||||
|
||||
## Monitoring
|
||||
|
||||
You can monitor Ground Control instances themselves using a separate monitoring path. In this setup, each region runs its own monitoring instance that scrapes metrics from the Ground Control components.
|
||||
|
||||

|
||||
{width="700"}
|
||||
|
||||
You can optionally duplicate the monitored metrics to the neighboring region for extra resilience. That way, if a whole Ground Control region goes down, you still have access to the telemetry of the downed VictoriaMetrics instance, which can help you troubleshoot and restore service more easily.
|
||||
|
||||
Refer to the following pages on how to monitor your VictoriaMetrics deployments:
|
||||
|
||||
* [How to monitor VictoriaMetrics single node](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#monitoring)
|
||||
* [How to monitor a VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#monitoring)
|
||||
|
||||
## What more can we do?
|
||||
|
||||
You can deploy extra vmagent instances in Ground Control regions and use them as regional ingestion proxies. This places the write endpoint closer to storage and adds another disk-backed buffer, which improves resilience when storage is temporarily unavailable.
|
||||
|
||||

|
||||
{width="700"}
|
||||
|
||||
This pattern is useful when you want more reliable delivery, local relabeling, or a cleaner separation between cross-region traffic and local storage ingestion.
|
||||
|
||||
For a Ground Control running VictoriaMetrics single node, you can run vmagent as follows:
|
||||
|
||||
```sh
|
||||
# vmagent next to Ground Control 1
|
||||
/path/to/vmagent-prod \
|
||||
-remoteWrite.url=http://ground-control-1:8428/api/v1/write
|
||||
```
|
||||
|
||||
If running in cluster mode, use this instead:
|
||||
|
||||
```sh
|
||||
# vmagent next to Ground Control 1 for cluster mode
|
||||
/path/to/vmagent-prod \
|
||||
-remoteWrite.url=http://ground-control-1-vminsert:8480/insert/0/prometheus/api/v1/write
|
||||
```
|
||||
### What more can we do?
|
||||
|
||||
Setup vmagents in Ground Control regions. That allows it to accept data close to storage and add more reliability if storage is temporarily offline.
|
||||
|
||||
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 111 KiB |