Compare commits
5 Commits
nwanduka-p
...
RequestErr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0266cb5716 | ||
|
|
5bd60e3b39 | ||
|
|
4c8f5b8369 | ||
|
|
2388f30ba1 | ||
|
|
12ad538f64 |
@@ -63,7 +63,6 @@ func main() {
|
||||
flag.CommandLine.SetOutput(os.Stdout)
|
||||
flag.Usage = usage
|
||||
envflag.Parse()
|
||||
initSecretFlags()
|
||||
buildinfo.Init()
|
||||
logger.Init()
|
||||
|
||||
@@ -171,9 +170,3 @@ See the docs at https://docs.victoriametrics.com/victoriametrics/
|
||||
`
|
||||
flagutil.Usage(s)
|
||||
}
|
||||
|
||||
// initSecretFlags manages the secret flags for this app and must be called after flag parsing and before logger init.
|
||||
func initSecretFlags() {
|
||||
pushmetrics.InitSecretFlags()
|
||||
vmselect.InitSecretFlags()
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ var (
|
||||
maxLabelNameLen = flag.Int("maxLabelNameLen", 0, "The maximum length of label names in the accepted time series. Series with longer label name are ignored. In this case the vm_rows_ignored_total{reason=\"too_long_label_name\"} metric at /metrics page is incremented")
|
||||
maxLabelValueLen = flag.Int("maxLabelValueLen", 0, "The maximum length of label values in the accepted time series. Series with longer label value are ignored. In this case the vm_rows_ignored_total{reason=\"too_long_label_value\"} metric at /metrics page is incremented")
|
||||
|
||||
enableMultitenancyViaHeaders = flag.Bool("enableMultitenancyViaHeaders", true, "Enables multitenancy via HTTP headers. "+
|
||||
enableMultitenancyViaHeaders = flag.Bool("enableMultitenancyViaHeaders", false, "Enables multitenancy via HTTP headers. "+
|
||||
"See https://docs.victoriametrics.com/victoriametrics/vmagent/#multitenancy")
|
||||
)
|
||||
|
||||
@@ -115,7 +115,7 @@ func main() {
|
||||
flag.CommandLine.SetOutput(os.Stdout)
|
||||
flag.Usage = usage
|
||||
envflag.Parse()
|
||||
initSecretFlags()
|
||||
remotewrite.InitSecretFlags()
|
||||
buildinfo.Init()
|
||||
logger.Init()
|
||||
opentelemetry.Init()
|
||||
@@ -843,9 +843,3 @@ See the docs at https://docs.victoriametrics.com/victoriametrics/vmagent/ .
|
||||
`
|
||||
flagutil.Usage(s)
|
||||
}
|
||||
|
||||
// initSecretFlags manages the secret flags for this app and must be called after flag parsing and before logger init.
|
||||
func initSecretFlags() {
|
||||
remotewrite.InitSecretFlags()
|
||||
pushmetrics.InitSecretFlags()
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ func setUp() {
|
||||
|
||||
func tearDown() {
|
||||
protoparserutil.StopUnmarshalWorkers()
|
||||
remotewrite.Stop()
|
||||
srv.Close()
|
||||
logger.ResetOutputForTest()
|
||||
tmpDataDir := flag.Lookup("remoteWrite.tmpDataPath").Value.String()
|
||||
|
||||
@@ -156,8 +156,7 @@ var maxQueues = cgroup.AvailableCPUs() * 16
|
||||
|
||||
const persistentQueueDirname = "persistent-queue"
|
||||
|
||||
// InitSecretFlags manages the secret flags for this pkg and must be called by app-level initSecretFlags.
|
||||
// It should run before logger initialization and package Init() (if exists).
|
||||
// InitSecretFlags must be called after flag.Parse and before any logging.
|
||||
func InitSecretFlags() {
|
||||
if !*showRemoteWriteURL {
|
||||
// remoteWrite.url can contain authentication codes, so hide it at `/metrics` output.
|
||||
@@ -246,8 +245,6 @@ func Init() {
|
||||
dropDanglingQueues()
|
||||
|
||||
// Start config reloader.
|
||||
configReloaderStopCh = make(chan struct{})
|
||||
configReloaderWG = sync.WaitGroup{}
|
||||
configReloaderWG.Go(func() {
|
||||
for {
|
||||
select {
|
||||
@@ -334,7 +331,7 @@ func initRemoteWriteCtxs(urls []string) {
|
||||
}
|
||||
|
||||
var (
|
||||
configReloaderStopCh chan struct{}
|
||||
configReloaderStopCh = make(chan struct{})
|
||||
configReloaderWG sync.WaitGroup
|
||||
)
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ groups:
|
||||
concurrency: 2
|
||||
rules:
|
||||
- alert: RequestErrorsToAPI
|
||||
expr: increase(vm_http_request_errors_total[5m]) > 0
|
||||
expr: increase(vm_http_request_errors_total{path=~".+"}[5m]) > 0
|
||||
for: 15m
|
||||
labels:
|
||||
severity: warning
|
||||
|
||||
@@ -60,8 +60,7 @@ var (
|
||||
`Only valid for VictoriaMetrics as the datasource.`)
|
||||
)
|
||||
|
||||
// InitSecretFlags manages the secret flags for this pkg and must be called by app-level initSecretFlags.
|
||||
// It should run before logger initialization and package Init() (if exists).
|
||||
// InitSecretFlags must be called after flag.Parse and before any logging
|
||||
func InitSecretFlags() {
|
||||
if !*showDatasourceURL {
|
||||
flagutil.RegisterSecretFlag("datasource.url")
|
||||
|
||||
@@ -88,7 +88,10 @@ func main() {
|
||||
flag.CommandLine.SetOutput(os.Stdout)
|
||||
flag.Usage = usage
|
||||
envflag.Parse()
|
||||
initSecretFlags()
|
||||
remoteread.InitSecretFlags()
|
||||
remotewrite.InitSecretFlags()
|
||||
datasource.InitSecretFlags()
|
||||
notifier.InitSecretFlags()
|
||||
buildinfo.Init()
|
||||
logger.Init()
|
||||
|
||||
@@ -435,12 +438,3 @@ func getLastConfigError() error {
|
||||
defer lastConfigErrMu.RUnlock()
|
||||
return lastConfigErr
|
||||
}
|
||||
|
||||
// initSecretFlags manages the secret flags for this app and must be called after flag parsing and before logger init.
|
||||
func initSecretFlags() {
|
||||
remoteread.InitSecretFlags()
|
||||
remotewrite.InitSecretFlags()
|
||||
datasource.InitSecretFlags()
|
||||
notifier.InitSecretFlags()
|
||||
pushmetrics.InitSecretFlags()
|
||||
}
|
||||
|
||||
@@ -189,8 +189,7 @@ func Init(extLabels map[string]string, extURL string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitSecretFlags manages the secret flags for this pkg and must be called by app-level initSecretFlags.
|
||||
// It should run before logger initialization and package Init() (if exists).
|
||||
// InitSecretFlags must be called after flag.Parse and before any logging
|
||||
func InitSecretFlags() {
|
||||
if !*showNotifierURL {
|
||||
flagutil.RegisterSecretFlag("notifier.url")
|
||||
|
||||
@@ -56,8 +56,7 @@ var (
|
||||
oauth2Scopes = flag.String("remoteRead.oauth2.scopes", "", "Optional OAuth2 scopes to use for -remoteRead.url. Scopes must be delimited by ';'.")
|
||||
)
|
||||
|
||||
// InitSecretFlags manages the secret flags for this pkg and must be called by app-level initSecretFlags.
|
||||
// It should run before logger initialization and package Init() (if exists).
|
||||
// InitSecretFlags must be called after flag.Parse and before any logging
|
||||
func InitSecretFlags() {
|
||||
if !*showRemoteReadURL {
|
||||
flagutil.RegisterSecretFlag("remoteRead.url")
|
||||
|
||||
@@ -57,8 +57,7 @@ var (
|
||||
oauth2Scopes = flag.String("remoteWrite.oauth2.scopes", "", "Optional OAuth2 scopes to use for -notifier.url. Scopes must be delimited by ';'.")
|
||||
)
|
||||
|
||||
// InitSecretFlags manages the secret flags for this pkg and must be called by app-level initSecretFlags.
|
||||
// It should run before logger initialization and package Init() (if exists).
|
||||
// InitSecretFlags must be called after flag.Parse and before any logging
|
||||
func InitSecretFlags() {
|
||||
if !*showRemoteWriteURL {
|
||||
flagutil.RegisterSecretFlag("remoteWrite.url")
|
||||
|
||||
@@ -30,8 +30,7 @@ var (
|
||||
"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 400 or 422 response code, "+
|
||||
"which can happen due to an expression syntax error or a resource limit being hit.")
|
||||
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) {
|
||||
|
||||
@@ -462,11 +462,7 @@ func (ar *AlertingRule) exec(ctx context.Context, ts time.Time, limit int) ([]pr
|
||||
}
|
||||
|
||||
isPartial := isPartialResponse(res)
|
||||
seriesFetched := 0
|
||||
if res.SeriesFetched != nil {
|
||||
seriesFetched = *res.SeriesFetched
|
||||
}
|
||||
ar.logDebugf(ts, nil, "query returned %d series (series_fetched: %d, elapsed: %s, isPartial: %t)", curState.Samples, seriesFetched, curState.Duration, isPartial)
|
||||
ar.logDebugf(ts, nil, "query returned %d series (elapsed: %s, isPartial: %t)", curState.Samples, curState.Duration, isPartial)
|
||||
qFn := func(query string) ([]datasource.Metric, error) {
|
||||
res, _, err := ar.q.Query(ctx, query, ts)
|
||||
return res.Data, err
|
||||
|
||||
@@ -290,8 +290,6 @@ 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
|
||||
@@ -339,7 +337,7 @@ func (g *Group) Init() {
|
||||
i := g.Interval.Seconds()
|
||||
return i
|
||||
})
|
||||
g.metrics.iterationLimit = g.metrics.set.NewGauge(fmt.Sprintf(`vmalert_group_rule_results_limit{%s}`, labels), func() float64 {
|
||||
g.metrics.iterationLimit = g.metrics.set.NewGauge(fmt.Sprintf(`vmalert_rule_group_results_limit{%s}`, labels), func() float64 {
|
||||
g.mu.RLock()
|
||||
limit := g.Limit
|
||||
g.mu.RUnlock()
|
||||
@@ -375,7 +373,7 @@ func (g *Group) Start(ctx context.Context, rw remotewrite.RWClient, rr datasourc
|
||||
g.mu.Lock()
|
||||
err := g.updateWith(ng)
|
||||
if err != nil {
|
||||
logger.Errorf("group %q (file=%q): failed to update: %s", g.Name, g.File, err)
|
||||
logger.Errorf("group %q: failed to update: %s", g.Name, err)
|
||||
g.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
@@ -414,7 +412,7 @@ func (g *Group) Start(ctx context.Context, rw remotewrite.RWClient, rr datasourc
|
||||
errs := e.execConcurrently(ctx, g.Rules, ts, g.Concurrency, resolveDuration, g.Limit)
|
||||
for err := range errs {
|
||||
if err != nil {
|
||||
logger.Errorf("group %q (file=%q): %s", g.Name, g.File, err)
|
||||
logger.Errorf("group %q: %s", g.Name, err)
|
||||
}
|
||||
}
|
||||
g.metrics.iterationDuration.UpdateDuration(start)
|
||||
@@ -443,17 +441,17 @@ func (g *Group) Start(ctx context.Context, rw remotewrite.RWClient, rr datasourc
|
||||
if rr != nil {
|
||||
err := g.restore(ctx, rr, realEvalTS, *remoteReadLookBack)
|
||||
if err != nil {
|
||||
logger.Errorf("error while restoring ruleState for group %q (file=%q): %s", g.Name, g.File, err)
|
||||
logger.Errorf("error while restoring ruleState for group %q: %s", g.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
logger.Infof("group %q (file=%q): context cancelled", g.Name, g.File)
|
||||
logger.Infof("group %q: context cancelled", g.Name)
|
||||
return
|
||||
case <-g.doneCh:
|
||||
logger.Infof("group %q (file=%q): received stop signal", g.Name, g.File)
|
||||
logger.Infof("group %q: received stop signal", g.Name)
|
||||
return
|
||||
case ng := <-g.updateCh:
|
||||
g.mu.Lock()
|
||||
@@ -467,7 +465,7 @@ func (g *Group) Start(ctx context.Context, rw remotewrite.RWClient, rr datasourc
|
||||
|
||||
err := g.updateWith(ng)
|
||||
if err != nil {
|
||||
logger.Errorf("group %q (file=%q): failed to update: %s", g.Name, g.File, err)
|
||||
logger.Errorf("group %q: failed to update: %s", g.Name, err)
|
||||
g.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
@@ -545,8 +543,8 @@ func (g *Group) delayBeforeStart(ts time.Time, maxDelay time.Duration) time.Dura
|
||||
|
||||
func (g *Group) infof(format string, args ...any) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
logger.Infof("group %q (file=%q; interval=%v; eval_offset=%v; concurrency=%d) %s",
|
||||
g.Name, g.File, g.Interval, g.EvalOffset, g.Concurrency, msg)
|
||||
logger.Infof("group %q %s; interval=%v; eval_offset=%v; concurrency=%d",
|
||||
g.Name, msg, g.Interval, g.EvalOffset, g.Concurrency)
|
||||
}
|
||||
|
||||
// Replay performs group replay
|
||||
|
||||
@@ -78,12 +78,6 @@ 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
|
||||
@@ -243,37 +237,6 @@ 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) {
|
||||
|
||||
@@ -208,11 +208,7 @@ func (rr *RecordingRule) exec(ctx context.Context, ts time.Time, limit int) ([]p
|
||||
return nil, curState.Err
|
||||
}
|
||||
|
||||
seriesFetched := 0
|
||||
if res.SeriesFetched != nil {
|
||||
seriesFetched = *res.SeriesFetched
|
||||
}
|
||||
rr.logDebugf(ts, "query returned %d samples (series_fetched: %d, elapsed: %s, isPartial: %t)", curState.Samples, seriesFetched, curState.Duration, isPartialResponse(res))
|
||||
rr.logDebugf(ts, "query returned %d samples (elapsed: %s, isPartial: %t)", curState.Samples, curState.Duration, isPartialResponse(res))
|
||||
|
||||
qMetrics := res.Data
|
||||
numSeries := len(qMetrics)
|
||||
|
||||
@@ -132,10 +132,9 @@ func replayRule(r Rule, start, end time.Time, rw remotewrite.RWClient, replayRul
|
||||
var esc *httpserver.ErrorWithStatusCode
|
||||
if errors.As(err, &esc) {
|
||||
statusCode := esc.StatusCode
|
||||
// if the status code is 400 or 422, the query failed due to reasons such as an expression syntax error or a resource limit being hit,
|
||||
// rather than datasource unavailability.
|
||||
// Continue replaying but skip the problematic execution if continueWithExecutionErr is true, otherwise, return the error without retry.
|
||||
if statusCode == http.StatusUnprocessableEntity || statusCode == http.StatusBadRequest {
|
||||
// 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
|
||||
|
||||
@@ -96,7 +96,6 @@ func main() {
|
||||
flag.CommandLine.SetOutput(os.Stdout)
|
||||
flag.Usage = usage
|
||||
envflag.Parse()
|
||||
initSecretFlags()
|
||||
buildinfo.Init()
|
||||
logger.Init()
|
||||
|
||||
@@ -912,8 +911,3 @@ func slowdownUnauthorizedResponse(r *http.Request) {
|
||||
}
|
||||
timerpool.Put(t)
|
||||
}
|
||||
|
||||
// initSecretFlags manages the secret flags for this app and must be called after flag parsing and before logger init.
|
||||
func initSecretFlags() {
|
||||
pushmetrics.InitSecretFlags()
|
||||
}
|
||||
|
||||
@@ -47,8 +47,9 @@ func main() {
|
||||
// Write flags and help message to stdout, since it is easier to grep or pipe.
|
||||
flag.CommandLine.SetOutput(os.Stdout)
|
||||
flag.Usage = usage
|
||||
flagutil.RegisterSecretFlag("snapshot.createURL")
|
||||
flagutil.RegisterSecretFlag("snapshot.deleteURL")
|
||||
envflag.Parse()
|
||||
initSecretFlags()
|
||||
buildinfo.Init()
|
||||
logger.Init()
|
||||
|
||||
@@ -272,10 +273,3 @@ func newRemoteOriginFS(ctx context.Context) (common.RemoteFS, error) {
|
||||
}
|
||||
return fs, nil
|
||||
}
|
||||
|
||||
// initSecretFlags manages the secret flags for this app and must be called after flag parsing and before logger init.
|
||||
func initSecretFlags() {
|
||||
flagutil.RegisterSecretFlag("snapshot.createURL")
|
||||
flagutil.RegisterSecretFlag("snapshot.deleteURL")
|
||||
pushmetrics.InitSecretFlags()
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ func main() {
|
||||
start := time.Now()
|
||||
beforeFn := func(c *cli.Context) error {
|
||||
flag.Parse()
|
||||
initSecretFlags()
|
||||
logger.Init()
|
||||
isSilent = c.Bool(globalSilent)
|
||||
if c.Bool(globalDisableProgressBar) {
|
||||
@@ -620,8 +619,3 @@ func initConfigVM(c *cli.Context) (vm.Config, error) {
|
||||
Backoff: bf,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// initSecretFlags manages the secret flags for this app and must be called after flag parsing and before logger init.
|
||||
func initSecretFlags() {
|
||||
pushmetrics.InitSecretFlags()
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ func main() {
|
||||
flag.CommandLine.SetOutput(os.Stdout)
|
||||
flag.Usage = usage
|
||||
envflag.Parse()
|
||||
initSecretFlags()
|
||||
buildinfo.Init()
|
||||
logger.Init()
|
||||
|
||||
@@ -113,8 +112,3 @@ func newSrcFS(ctx context.Context) (common.RemoteFS, error) {
|
||||
}
|
||||
return fs, nil
|
||||
}
|
||||
|
||||
// initSecretFlags manages the secret flags for this app and must be called after flag parsing and before logger init.
|
||||
func initSecretFlags() {
|
||||
pushmetrics.InitSecretFlags()
|
||||
}
|
||||
|
||||
@@ -59,12 +59,6 @@ func Init(vmselectMaxConcurrentRequests int, vmselectMaxQueueDuration time.Durat
|
||||
initVMUIConfig()
|
||||
|
||||
vmalertproxy.Init(*vmalertProxyURL)
|
||||
|
||||
}
|
||||
|
||||
// InitSecretFlags manages the secret flags for this pkg and must be called by app-level initSecretFlags.
|
||||
// It should run before logger initialization and package Init() (if exists).
|
||||
func InitSecretFlags() {
|
||||
flagutil.RegisterSecretFlag("vmalert.proxyURL")
|
||||
}
|
||||
|
||||
|
||||
@@ -516,7 +516,7 @@ func DeleteHandler(startTime time.Time, r *http.Request) error {
|
||||
cp.deadline = searchutil.GetDeadlineForDelete(r, startTime)
|
||||
|
||||
if !cp.IsDefaultTimeRange() {
|
||||
return fmt.Errorf("delete API does not support specific time ranges using start and end args, the series can only be deleted completely")
|
||||
return fmt.Errorf("start=%d and end=%d args aren't supported. Remove these args from the query in order to delete all the matching metrics", cp.start, cp.end)
|
||||
}
|
||||
sq := storage.NewSearchQuery(cp.start, cp.end, cp.filterss, *maxDeleteSeries)
|
||||
deletedCount, err := netstorage.DeleteSeries(nil, sq, cp.deadline)
|
||||
@@ -540,11 +540,11 @@ func LabelValuesHandler(qt *querytracer.Tracer, startTime time.Time, labelName s
|
||||
|
||||
cp, err := getCommonParamsForLabelsAPI(r, startTime, false)
|
||||
if err != nil {
|
||||
return httpserver.InvalidParamError(err)
|
||||
return err
|
||||
}
|
||||
limit, err := httputil.GetInt(r, "limit")
|
||||
if err != nil {
|
||||
return httpserver.InvalidParamError(err)
|
||||
return err
|
||||
}
|
||||
sq := storage.NewSearchQuery(cp.start, cp.end, cp.filterss, *maxLabelsAPISeries)
|
||||
|
||||
@@ -584,7 +584,7 @@ func TSDBStatusHandler(qt *querytracer.Tracer, startTime time.Time, w http.Respo
|
||||
|
||||
cp, err := getCommonParams(r, startTime, false)
|
||||
if err != nil {
|
||||
return httpserver.InvalidParamError(err)
|
||||
return err
|
||||
}
|
||||
cp.deadline = searchutil.GetDeadlineForStatusRequest(r, startTime)
|
||||
|
||||
@@ -596,7 +596,7 @@ func TSDBStatusHandler(qt *querytracer.Tracer, startTime time.Time, w http.Respo
|
||||
} else {
|
||||
t, err := time.Parse("2006-01-02", dateStr)
|
||||
if err != nil {
|
||||
return httpserver.InvalidParamError(fmt.Errorf("cannot parse `date` arg %q: %w", dateStr, err))
|
||||
return fmt.Errorf("cannot parse `date` arg %q: %w", dateStr, err)
|
||||
}
|
||||
date = uint64(t.Unix()) / secsPerDay
|
||||
}
|
||||
@@ -607,7 +607,7 @@ func TSDBStatusHandler(qt *querytracer.Tracer, startTime time.Time, w http.Respo
|
||||
if len(topNStr) > 0 {
|
||||
n, err := strconv.Atoi(topNStr)
|
||||
if err != nil {
|
||||
return httpserver.InvalidParamError(fmt.Errorf("cannot parse `topN` arg %q: %w", topNStr, err))
|
||||
return fmt.Errorf("cannot parse `topN` arg %q: %w", topNStr, err)
|
||||
}
|
||||
if n <= 0 {
|
||||
n = 1
|
||||
@@ -645,11 +645,11 @@ func LabelsHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseW
|
||||
|
||||
cp, err := getCommonParamsForLabelsAPI(r, startTime, false)
|
||||
if err != nil {
|
||||
return httpserver.InvalidParamError(err)
|
||||
return err
|
||||
}
|
||||
limit, err := httputil.GetInt(r, "limit")
|
||||
if err != nil {
|
||||
return httpserver.InvalidParamError(err)
|
||||
return err
|
||||
}
|
||||
sq := storage.NewSearchQuery(cp.start, cp.end, cp.filterss, *maxLabelsAPISeries)
|
||||
labels, err := netstorage.LabelNames(qt, sq, limit, cp.deadline)
|
||||
@@ -671,9 +671,10 @@ func LabelsHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseW
|
||||
//
|
||||
// See https://prometheus.io/docs/prometheus/latest/querying/api/#querying-metric-metadata
|
||||
func MetadataHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWriter, r *http.Request) error {
|
||||
|
||||
limit, err := httputil.GetInt(r, "limit")
|
||||
if err != nil {
|
||||
return httpserver.InvalidParamError(err)
|
||||
return err
|
||||
}
|
||||
if limit < 0 {
|
||||
limit = 0
|
||||
@@ -733,11 +734,11 @@ func SeriesHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseW
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/91
|
||||
cp, err := getCommonParamsForLabelsAPI(r, startTime, true)
|
||||
if err != nil {
|
||||
return httpserver.InvalidParamError(err)
|
||||
return err
|
||||
}
|
||||
limit, err := httputil.GetInt(r, "limit")
|
||||
if err != nil {
|
||||
return httpserver.InvalidParamError(err)
|
||||
return err
|
||||
}
|
||||
|
||||
sq := storage.NewSearchQuery(cp.start, cp.end, cp.filterss, *maxSeriesLimit)
|
||||
@@ -771,19 +772,19 @@ func QueryHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWr
|
||||
mayCache := !httputil.GetBool(r, "nocache")
|
||||
query := r.FormValue("query")
|
||||
if len(query) == 0 {
|
||||
return httpserver.InvalidParamError(fmt.Errorf("missing `query` arg"))
|
||||
return fmt.Errorf("missing `query` arg")
|
||||
}
|
||||
start, err := httputil.GetTime(r, "time", ct)
|
||||
if err != nil {
|
||||
return httpserver.InvalidParamError(err)
|
||||
return err
|
||||
}
|
||||
lookbackDelta, err := getMaxLookback(r)
|
||||
if err != nil {
|
||||
return httpserver.InvalidParamError(err)
|
||||
return err
|
||||
}
|
||||
step, err := httputil.GetDuration(r, "step", lookbackDelta)
|
||||
if err != nil {
|
||||
return httpserver.InvalidParamError(err)
|
||||
return err
|
||||
}
|
||||
if step <= 0 {
|
||||
step = defaultStep
|
||||
@@ -791,16 +792,16 @@ func QueryHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWr
|
||||
|
||||
maxLen := searchutil.GetMaxQueryLen()
|
||||
if len(query) > maxLen {
|
||||
return httpserver.InvalidParamError(fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(query), maxLen))
|
||||
return fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(query), maxLen)
|
||||
}
|
||||
etfs, err := searchutil.GetExtraTagFilters(r)
|
||||
if err != nil {
|
||||
return httpserver.InvalidParamError(err)
|
||||
return err
|
||||
}
|
||||
if childQuery, windowExpr, offsetExpr := promql.IsMetricSelectorWithRollup(query); childQuery != "" {
|
||||
window, err := windowExpr.NonNegativeDuration(step)
|
||||
if err != nil {
|
||||
return httpserver.InvalidParamError(fmt.Errorf("cannot parse lookbehind window in square brackets at %s: %w", query, err))
|
||||
return fmt.Errorf("cannot parse lookbehind window in square brackets at %s: %w", query, err)
|
||||
}
|
||||
offset := offsetExpr.Duration(step)
|
||||
start -= offset
|
||||
@@ -814,7 +815,7 @@ func QueryHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWr
|
||||
|
||||
tagFilterss, err := getTagFilterssFromMatches([]string{childQuery})
|
||||
if err != nil {
|
||||
return httpserver.InvalidParamError(err)
|
||||
return err
|
||||
}
|
||||
filterss := searchutil.JoinTagFilterss(tagFilterss, etfs)
|
||||
|
||||
@@ -830,25 +831,22 @@ func QueryHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWr
|
||||
return nil
|
||||
}
|
||||
if childQuery, windowExpr, stepExpr, offsetExpr := promql.IsRollup(query); childQuery != "" {
|
||||
if len(childQuery) > maxLen {
|
||||
return httpserver.InvalidParamError(fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(childQuery), maxLen))
|
||||
}
|
||||
newStep, err := stepExpr.NonNegativeDuration(step)
|
||||
if err != nil {
|
||||
return httpserver.InvalidParamError(fmt.Errorf("cannot parse step in square brackets at %s: %w", query, err))
|
||||
return fmt.Errorf("cannot parse step in square brackets at %s: %w", query, err)
|
||||
}
|
||||
if newStep > 0 {
|
||||
step = newStep
|
||||
}
|
||||
window, err := windowExpr.NonNegativeDuration(step)
|
||||
if err != nil {
|
||||
return httpserver.InvalidParamError(fmt.Errorf("cannot parse lookbehind window in square brackets at %s: %w", query, err))
|
||||
return fmt.Errorf("cannot parse lookbehind window in square brackets at %s: %w", query, err)
|
||||
}
|
||||
offset := offsetExpr.Duration(step)
|
||||
start -= offset
|
||||
end := start
|
||||
start = end - window
|
||||
if err := queryRangeHandler(qt, startTime, w, childQuery, start, end, step, lookbackDelta, r, ct, etfs); err != nil {
|
||||
if err := queryRangeHandler(qt, startTime, w, childQuery, start, end, step, r, ct, etfs); err != nil {
|
||||
return fmt.Errorf("error when executing query=%q on the time range (start=%d, end=%d, step=%d): %w", childQuery, start, end, step, err)
|
||||
}
|
||||
return nil
|
||||
@@ -856,7 +854,7 @@ func QueryHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWr
|
||||
|
||||
queryOffset, err := getLatencyOffsetMilliseconds(r)
|
||||
if err != nil {
|
||||
return httpserver.InvalidParamError(err)
|
||||
return err
|
||||
}
|
||||
if !httputil.GetBool(r, "nocache") && ct-start < queryOffset && start-ct < queryOffset {
|
||||
// Adjust start time only if `nocache` arg isn't set.
|
||||
@@ -930,43 +928,45 @@ func QueryRangeHandler(qt *querytracer.Tracer, startTime time.Time, w http.Respo
|
||||
ct := startTime.UnixNano() / 1e6
|
||||
query := r.FormValue("query")
|
||||
if len(query) == 0 {
|
||||
return httpserver.InvalidParamError(fmt.Errorf("missing `query` arg"))
|
||||
}
|
||||
maxLen := searchutil.GetMaxQueryLen()
|
||||
if len(query) > maxLen {
|
||||
return httpserver.InvalidParamError(fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(query), maxLen))
|
||||
return fmt.Errorf("missing `query` arg")
|
||||
}
|
||||
start, err := httputil.GetTime(r, "start", ct-defaultStep)
|
||||
if err != nil {
|
||||
return httpserver.InvalidParamError(err)
|
||||
return err
|
||||
}
|
||||
end, err := httputil.GetTime(r, "end", ct)
|
||||
if err != nil {
|
||||
return httpserver.InvalidParamError(err)
|
||||
return err
|
||||
}
|
||||
step, err := httputil.GetDuration(r, "step", defaultStep)
|
||||
if err != nil {
|
||||
return httpserver.InvalidParamError(err)
|
||||
return err
|
||||
}
|
||||
etfs, err := searchutil.GetExtraTagFilters(r)
|
||||
if err != nil {
|
||||
return httpserver.InvalidParamError(err)
|
||||
return err
|
||||
}
|
||||
lookbackDelta, err := getMaxLookback(r)
|
||||
if err != nil {
|
||||
return httpserver.InvalidParamError(err)
|
||||
}
|
||||
if err := queryRangeHandler(qt, startTime, w, query, start, end, step, lookbackDelta, r, ct, etfs); err != nil {
|
||||
if err := queryRangeHandler(qt, startTime, w, query, start, end, step, r, ct, etfs); err != nil {
|
||||
return fmt.Errorf("error when executing query=%q on the time range (start=%d, end=%d, step=%d): %w", query, start, end, step, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func queryRangeHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWriter, query string,
|
||||
start, end, step, lookbackDelta int64, r *http.Request, ct int64, etfs [][]storage.TagFilter) error {
|
||||
start, end, step int64, r *http.Request, ct int64, etfs [][]storage.TagFilter) error {
|
||||
deadline := searchutil.GetDeadlineForQuery(r, startTime)
|
||||
mayCache := !httputil.GetBool(r, "nocache")
|
||||
optimizeRepeatedBinaryOpSubexprs := httputil.GetBool(r, "optimize_repeated_binary_op_subexprs")
|
||||
lookbackDelta, err := getMaxLookback(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate input args.
|
||||
maxLen := searchutil.GetMaxQueryLen()
|
||||
if len(query) > maxLen {
|
||||
return fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(query), maxLen)
|
||||
}
|
||||
if start > end {
|
||||
end = start + defaultStep
|
||||
}
|
||||
@@ -1005,7 +1005,7 @@ func queryRangeHandler(qt *querytracer.Tracer, startTime time.Time, w http.Respo
|
||||
if step < maxStepForPointsAdjustment.Milliseconds() {
|
||||
queryOffset, err := getLatencyOffsetMilliseconds(r)
|
||||
if err != nil {
|
||||
return httpserver.InvalidParamError(err)
|
||||
return err
|
||||
}
|
||||
if ct-queryOffset < end {
|
||||
result = adjustLastPoints(result, ct-queryOffset, ct+step)
|
||||
@@ -1156,13 +1156,13 @@ func QueryStatsHandler(w http.ResponseWriter, r *http.Request) error {
|
||||
if len(topNStr) > 0 {
|
||||
n, err := strconv.Atoi(topNStr)
|
||||
if err != nil {
|
||||
return httpserver.InvalidParamError(fmt.Errorf("cannot parse `topN` arg %q: %w", topNStr, err))
|
||||
return fmt.Errorf("cannot parse `topN` arg %q: %w", topNStr, err)
|
||||
}
|
||||
topN = n
|
||||
}
|
||||
maxLifetimeMsecs, err := httputil.GetDuration(r, "maxLifetime", 10*60*1000)
|
||||
if err != nil {
|
||||
return httpserver.InvalidParamError(fmt.Errorf("cannot parse `maxLifetime` arg: %w", err))
|
||||
return fmt.Errorf("cannot parse `maxLifetime` arg: %w", err)
|
||||
}
|
||||
maxLifetime := time.Duration(maxLifetimeMsecs) * time.Millisecond
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/querystats"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||
@@ -47,15 +46,15 @@ func Exec(qt *querytracer.Tracer, ec *EvalConfig, q string, isFirstPointOnly boo
|
||||
|
||||
e, err := parsePromQLWithCache(q)
|
||||
if err != nil {
|
||||
return nil, httpserver.InvalidParamError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if *disableImplicitConversion || *logImplicitConversion {
|
||||
isInvalid := metricsql.IsLikelyInvalid(e)
|
||||
if isInvalid && *disableImplicitConversion {
|
||||
// we don't add query=%q to err message as it will be added by the caller
|
||||
return nil, httpserver.InvalidParamError(fmt.Errorf("query requires implicit conversion and is rejected according to -search.disableImplicitConversion command-line flag. " +
|
||||
"See https://docs.victoriametrics.com/victoriametrics/metricsql/#implicit-query-conversions for details"))
|
||||
return nil, fmt.Errorf("query requires implicit conversion and is rejected according to -search.disableImplicitConversion command-line flag. " +
|
||||
"See https://docs.victoriametrics.com/victoriametrics/metricsql/#implicit-query-conversions for details")
|
||||
}
|
||||
if isInvalid && *logImplicitConversion {
|
||||
logger.Warnf("query=%q requires implicit conversion, see https://docs.victoriametrics.com/victoriametrics/metricsql/#implicit-query-conversions for details", e.AppendString(nil))
|
||||
|
||||
@@ -135,7 +135,7 @@ func tenantViaURL(addr, prefix, tenant, suffix string) string {
|
||||
}
|
||||
|
||||
// tenantViaHeaders returns path in cluster's URL format where tenant is omitted in URL
|
||||
// Only supported if -enableMultitenancyViaHeaders is enabled
|
||||
// Only supported if -enableMultitenancyViaHeaders is specified
|
||||
func tenantViaHeaders(addr, prefix, suffix string) string {
|
||||
return fmt.Sprintf("http://%s/%s/%s", addr, prefix, suffix)
|
||||
}
|
||||
|
||||
@@ -25,10 +25,12 @@ func TestClusterMultiTenantSelectViaHeaders(t *testing.T) {
|
||||
})
|
||||
vminsert := tc.MustStartVminsert("vminsert", []string{
|
||||
"-storageNode=" + vmstorage.VminsertAddr(),
|
||||
"-enableMultitenancyViaHeaders",
|
||||
})
|
||||
vmselect := tc.MustStartVmselect("vmselect", []string{
|
||||
"-storageNode=" + vmstorage.VmselectAddr(),
|
||||
"-search.tenantCacheExpireDuration=0",
|
||||
"-enableMultitenancyViaHeaders",
|
||||
})
|
||||
|
||||
multitenant := make(http.Header)
|
||||
|
||||
@@ -594,6 +594,7 @@ func TestSingleVMAgentMultitenancy(t *testing.T) {
|
||||
fmt.Sprintf(`-remoteWrite.url=%s/api/v1/write`, remoteWriteSrv.URL),
|
||||
"-remoteWrite.tmpDataPath=" + tc.Dir() + "/vmagent-multitenancy",
|
||||
"-enableMultitenantHandlers",
|
||||
"-enableMultitenancyViaHeaders",
|
||||
})
|
||||
|
||||
vmagent.APIV1ImportPrometheus(t, []string{
|
||||
|
||||
@@ -75,7 +75,7 @@ groups:
|
||||
Consider to limit the ingestion rate, decrease retention or scale the disk space if possible."
|
||||
|
||||
- alert: RequestErrorsToAPI
|
||||
expr: increase(vm_http_request_errors_total[5m]) > 0
|
||||
expr: increase(vm_http_request_errors_total{path=~".+", path!="*"}[5m]) > 0
|
||||
for: 15m
|
||||
labels:
|
||||
severity: warning
|
||||
@@ -83,8 +83,24 @@ groups:
|
||||
annotations:
|
||||
dashboard: "{{ $externalURL }}/d/oS7Bi_0Wz?viewPanel=52&var-instance={{ $labels.instance }}"
|
||||
summary: "Too many errors served for {{ $labels.job }} path {{ $labels.path }} (instance {{ $labels.instance }})"
|
||||
description: "Requests to path {{ $labels.path }} are receiving errors.
|
||||
Please verify if clients are sending correct requests."
|
||||
description: |
|
||||
Requests to path {{ $labels.path }} are receiving errors.
|
||||
Please verify if clients are sending correct requests.
|
||||
|
||||
# Auth errors and unknown paths should be handled by a different alert
|
||||
# See https://github.com/VictoriaMetrics/VictoriaMetrics/blob/fdd9a221df835daa378ae2e6c9f12e4e3be79c76/lib/httpserver/httpserver.go#L589-L591
|
||||
- alert: RequestErrorsToUnknownPaths
|
||||
expr: sum(increase(vm_http_request_errors_total{path=~"^(\*|)$"}[5m])) by(job, instance, reason) > 0
|
||||
for: 15m
|
||||
labels:
|
||||
severity: warning
|
||||
show_at: dashboard
|
||||
annotations:
|
||||
dashboard: "{{ $externalURL }}/d/oS7Bi_0Wz?viewPanel=52&var-instance={{ $labels.instance }}"
|
||||
summary: "Too many errors served for {{ $labels.job }} with reason {{ $labels.reason }} (instance {{ $labels.instance }})"
|
||||
description: |
|
||||
Requests are failing with reason {{ $labels.reason }}.
|
||||
Please verify if clients are sending correct requests.
|
||||
|
||||
- alert: RPCErrors
|
||||
expr: |
|
||||
|
||||
@@ -75,7 +75,7 @@ groups:
|
||||
Consider to limit the ingestion rate, decrease retention or scale the disk space if possible."
|
||||
|
||||
- alert: RequestErrorsToAPI
|
||||
expr: increase(vm_http_request_errors_total[5m]) > 0
|
||||
expr: increase(vm_http_request_errors_total{path=~".+"}[5m]) > 0
|
||||
for: 15m
|
||||
labels:
|
||||
severity: warning
|
||||
@@ -85,6 +85,21 @@ groups:
|
||||
description: "Requests to path {{ $labels.path }} are receiving errors.
|
||||
Please verify if clients are sending correct requests."
|
||||
|
||||
# Auth errors and unknown paths should be handled by a different alert
|
||||
# See https://github.com/VictoriaMetrics/VictoriaMetrics/blob/fdd9a221df835daa378ae2e6c9f12e4e3be79c76/lib/httpserver/httpserver.go#L589-L591
|
||||
- alert: RequestErrorsToUnknownPaths
|
||||
expr: sum(increase(vm_http_request_errors_total{path=~"^(\*|)$"}[5m])) by(job, instance, reason) > 0
|
||||
for: 15m
|
||||
labels:
|
||||
severity: warning
|
||||
show_at: dashboard
|
||||
annotations:
|
||||
dashboard: "{{ $externalURL }}/d/oS7Bi_0Wz?viewPanel=52&var-instance={{ $labels.instance }}"
|
||||
summary: "Too many errors served for {{ $labels.job }} with reason {{ $labels.reason }} (instance {{ $labels.instance }})"
|
||||
description: |
|
||||
Requests are failing with reason {{ $labels.reason }}.
|
||||
Please verify if clients are sending correct requests.
|
||||
|
||||
- alert: TooHighChurnRate
|
||||
expr: |
|
||||
(
|
||||
|
||||
@@ -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.2
|
||||
image: victoriametrics/vmanomaly:v1.30.1
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
schedulers:
|
||||
periodic:
|
||||
infer_every: "1m"
|
||||
fit_every: "1000d" # bootstrap-only schedule; use a finite cadence if accumulated state must be reset
|
||||
fit_every: "100w" # the online model keeps learning during inference
|
||||
fit_window: "2w"
|
||||
|
||||
models:
|
||||
|
||||
@@ -16,23 +16,6 @@ Please find the changelog for VictoriaMetrics Anomaly Detection below.
|
||||
|
||||
{{% collapse name="2026" open=true %}}
|
||||
|
||||
## v1.30.2
|
||||
Released: 2026-08-13
|
||||
|
||||
- UI: Updated [vmanomaly UI](https://docs.victoriametrics.com/anomaly-detection/ui/) from [v1.8.1](https://docs.victoriametrics.com/anomaly-detection/ui/#v181) to [v1.8.2](https://docs.victoriametrics.com/anomaly-detection/ui/#v182), fixing tenant discovery and switching for multitenant VictoriaMetrics datasources.
|
||||
|
||||
- FEATURE: Added **query**-level [`data_range`, `detection_direction`, `min_dev_from_expected`, and `min_rel_dev_from_expected`](https://docs.victoriametrics.com/anomaly-detection/components/reader/#per-query-parameters). Model-level placement is deprecated but remains a compatible fallback.
|
||||
|
||||
- IMPROVEMENT: Added [`reader.workers`](https://docs.victoriametrics.com/anomaly-detection/components/reader/#config-parameters) to cap concurrent datasource requests and disk-streamed query chunks; `0` selects an automatic bound.
|
||||
|
||||
- IMPROVEMENT: Added [`settings.native_threads_per_worker`](https://docs.victoriametrics.com/anomaly-detection/components/settings/#parallelization) to reduce [native-thread oversubscription](https://scikit-learn.org/stable/computing/parallelism.html#oversubscription-spawning-too-many-threads), throttling risk, fit latency, and memory. For example, with 16 CPUs/workers, [Temporal Envelope](https://docs.victoriametrics.com/anomaly-detection/components/models/#temporal-envelope) fit time fell 70.6% for 1,000 univariate models and 11.5% for 100 x 10-channel grouped models; inference was unchanged.
|
||||
|
||||
- IMPROVEMENT: Removed temporary fit-data generations after all dependent models finish and commit, while safely retaining failed or overlapping generations.
|
||||
|
||||
- IMPROVEMENT: Reduced disk-backed grouped multivariate memory and fit latency without model or state migration. For example, 100 x 100-channel four-week fits cut peak PSS/fit time by 63%/56% for [Temporal Envelope](https://docs.victoriametrics.com/anomaly-detection/components/models/#temporal-envelope).
|
||||
|
||||
- BUGFIX: Made [multivariate models](https://docs.victoriametrics.com/anomaly-detection/components/models/#multivariate-models) independent of input channel order when the fitted channel set matches; missing, extra, or duplicate channels remain rejected.
|
||||
|
||||
## v1.30.1
|
||||
Released: 2026-08-06
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ Please see example graph illustrating this logic below:
|
||||
|
||||

|
||||
|
||||
> Additional post-processing logic may be applied to produced anomaly scores when query policies such as [`min_dev_from_expected`](https://docs.victoriametrics.com/anomaly-detection/components/models/#minimal-deviation-from-expected) or [`detection_direction`](https://docs.victoriametrics.com/anomaly-detection/components/models/#detection-direction) are configured. Follow the links for details.
|
||||
> p.s. please note that additional post-processing logic might be applied to produced anomaly scores, if common arguments like [`min_dev_from_expected`](https://docs.victoriametrics.com/anomaly-detection/components/models/#minimal-deviation-from-expected) or [`detection_direction`](https://docs.victoriametrics.com/anomaly-detection/components/models/#detection-direction) are enabled for a particular model. Follow the links above for the explanations.
|
||||
|
||||
|
||||
## How does vmanomaly work?
|
||||
@@ -135,7 +135,7 @@ Still not 100% sure what to use? We are [here to help](https://docs.victoriametr
|
||||
|
||||
## Incorporating domain knowledge
|
||||
|
||||
Anomaly detection models can significantly improve when incorporating business-specific assumptions about the data and what constitutes an anomaly. `vmanomaly` supports [business policies](https://docs.victoriametrics.com/anomaly-detection/components/models/#common-args) across 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:
|
||||
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:
|
||||
|
||||
- **Setting `detection_direction`** - use [`detection_direction`](https://docs.victoriametrics.com/anomaly-detection/components/models/#detection-direction) to specify whether anomalies occur **above or below expectations**:
|
||||
- Set to `above_expected` for metrics like error rates, where spikes indicate anomalies.
|
||||
@@ -163,7 +163,7 @@ Then, the following config may be used to benefit from incorporating domain know
|
||||
schedulers:
|
||||
periodic_http:
|
||||
class: periodic
|
||||
fit_every: 1000d
|
||||
fit_every: 12w
|
||||
fit_window: 1w
|
||||
infer_every: 1m
|
||||
# other schedulers ...
|
||||
@@ -172,19 +172,18 @@ reader:
|
||||
queries:
|
||||
percentage_4xx:
|
||||
expr: respective_metricsQL_expr
|
||||
data_range: [0, 0.05] # query-level business policy from v1.30.2; error rates >5% trigger anomaly score >1
|
||||
detection_direction: 'above_expected' # query-level from v1.30.2; only spikes are anomalous
|
||||
min_dev_from_expected: [0, 0.005] # query-level from v1.30.2; ignore upward deviations below 0.5%
|
||||
min_rel_dev_from_expected: [0, 10] # query-level from v1.30.2; ignore upward deviations below 10%
|
||||
data_range: [0, 0.05] # to automatically trigger anomaly score > 1 for error rates > 5%
|
||||
step: 1m
|
||||
models:
|
||||
# other models ...
|
||||
zscore: # let it be online Z-score, for simplicity
|
||||
class: zscore_online # online model update itself each infer call, resulting in resource-efficient setups
|
||||
z_threshold: 3.0
|
||||
decay: 0.99 # give more weight to recent data while using the bootstrap-only fit schedule
|
||||
schedulers: ['periodic_http']
|
||||
queries: ['percentage_4xx']
|
||||
detection_direction: 'above_expected' # as interested only in spikes, drops are OK
|
||||
min_dev_from_expected: [0, 0.005] # <0.5% deviations vs expected values should be neglected, generating anomaly score == 0
|
||||
min_rel_dev_from_expected: [0, 0.1] # <10% relative deviations vs expected values should be neglected, generating anomaly score == 0
|
||||
# to align predictions to be within [0, 5%] interval, defined in reader.queries.percentage_4xx.data_range
|
||||
clip_predictions: True
|
||||
# specify output series produced by vmanomaly to be written to VictoriaMetrics in `writer`
|
||||
@@ -230,7 +229,7 @@ models:
|
||||
schedulers: ['scheduler_alias'] # if omitted, all the defined schedulers will be attached
|
||||
queries: ['query_alias1'] # if omitted, all the defined queries will be attached
|
||||
# https://docs.victoriametrics.com/anomaly-detection/components/models/#provide-series
|
||||
provide_series: ['anomaly_score']
|
||||
provide_series: ['anomaly_score']
|
||||
# ... other models
|
||||
|
||||
reader:
|
||||
@@ -256,7 +255,6 @@ Configuration above will produce N intervals of full length (`fit_window`=14d +
|
||||
|
||||
`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.
|
||||
|
||||
> [!WARNING]
|
||||
> 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**.
|
||||
|
||||
Here's an example of how to produce forecasts using `vmanomaly` and combine it with the regular model, e.g. to estimate daily outcomes for a disk usage metric:
|
||||
@@ -266,12 +264,12 @@ Here's an example of how to produce forecasts using `vmanomaly` and combine it w
|
||||
schedulers:
|
||||
periodic_5m: # this scheduler will be used to produce anomaly scores each 5 minutes using "regular" simple model
|
||||
class: 'periodic'
|
||||
fit_every: '1000d'
|
||||
fit_every: '100w'
|
||||
fit_window: '3d'
|
||||
infer_every: '5m'
|
||||
periodic_forecast: # this scheduler will be used to produce forecasts each 24h using "daily" model
|
||||
class: 'periodic'
|
||||
fit_every: '1000d'
|
||||
fit_every: '1000w'
|
||||
fit_window: '730d' # to fit the model on 2 years of data to account for seasonality and holidays
|
||||
infer_every: '24h'
|
||||
# https://docs.victoriametrics.com/anomaly-detection/components/reader/#vm-reader
|
||||
@@ -291,7 +289,6 @@ reader:
|
||||
1h
|
||||
)
|
||||
data_range: [0, 1]
|
||||
detection_direction: 'above_expected' # query-level from v1.30.2
|
||||
# step: '1m' # default will be inherited from sampling_period
|
||||
disk_usage_perc_1d:
|
||||
expr: |
|
||||
@@ -303,15 +300,14 @@ reader:
|
||||
)
|
||||
step: '1d' # override default step to 1d, as we want to produce daily forecasts
|
||||
data_range: [0, 1]
|
||||
detection_direction: 'above_expected' # query-level from v1.30.2
|
||||
# https://docs.victoriametrics.com/anomaly-detection/components/models/
|
||||
models:
|
||||
quantile_5m:
|
||||
class: 'quantile_online' # online model, which updates itself each infer call
|
||||
queries: ['disk_usage_perc_5m']
|
||||
schedulers: ['periodic_5m']
|
||||
decay: 0.99 # give more weight to recent data while using the bootstrap-only fit schedule
|
||||
clip_predictions: True
|
||||
detection_direction: 'above_expected' # as we are interested in spikes in capacity planning
|
||||
quantiles: [0.25, 0.5, 0.75] # to produce median and upper quartiles
|
||||
iqr_threshold: 2.0
|
||||
|
||||
@@ -319,9 +315,8 @@ models:
|
||||
class: 'temporal_envelope'
|
||||
queries: ['disk_usage_perc_1d']
|
||||
schedulers: ['periodic_forecast']
|
||||
alpha: 0.005 # capture the changes faster if increased
|
||||
loss_reactivity: 3 # allow new deviations to update the envelope
|
||||
clip_predictions: True
|
||||
detection_direction: 'above_expected' # as we are interested in spikes in capacity planning
|
||||
forecast_at: ['3d', '7d'] # this will produce forecasts for 3 and 7 days ahead
|
||||
provide_series: ['yhat', 'yhat_upper'] # to write forecasts back to VictoriaMetrics, omitting `yhat_lower` as it is not needed in this example
|
||||
seasonalities: [dow_smooth]
|
||||
@@ -430,15 +425,13 @@ For information on migrating between different versions of `vmanomaly`, please r
|
||||
|
||||
> {{% available_from "v1.24.0" anomaly %}} This feature is best used in conjunction with [stateful mode](https://docs.victoriametrics.com/anomaly-detection/components/settings/#state-restoration) to ensure that the model state is preserved across service restarts.
|
||||
|
||||
> {{% available_from "v1.30.2" anomaly %}} Scheduler-managed fit data is **temporary**. It is removed after every dependent univariate or multivariate model completes fitting and commits its state, rather than being retained until the next `fit_every` cycle. Model dumps and state metadata remain available for restoration.
|
||||
|
||||
Here's an example of how to set it up in docker-compose using volumes:
|
||||
```yaml
|
||||
services:
|
||||
# ...
|
||||
vmanomaly:
|
||||
container_name: vmanomaly
|
||||
image: victoriametrics/vmanomaly:v1.30.2
|
||||
image: victoriametrics/vmanomaly:v1.30.1
|
||||
# ...
|
||||
restart: always
|
||||
volumes:
|
||||
@@ -509,7 +502,7 @@ settings:
|
||||
schedulers:
|
||||
periodic:
|
||||
class: 'periodic'
|
||||
fit_every: '1000d'
|
||||
fit_every: '180d' # we need only initial fit to start
|
||||
fit_window: '4h' # reduced window, especially if the data doesn't have strong seasonality
|
||||
infer_every: '1m' # the model will be updated during each infer call
|
||||
# other schedulers ...
|
||||
@@ -517,7 +510,7 @@ models:
|
||||
zscore_example:
|
||||
class: 'zscore_online'
|
||||
min_n_samples_seen: 120 # i.e. minimal relevant seasonality or (initial) fit_window / sampling_period
|
||||
decay: 0.99 # decay factor to control how fast the model adapts to new data, the lower, the faster it adapts
|
||||
decay: 0.999 # decay factor to control how fast the model adapts to new data, the lower, the faster it adapts
|
||||
schedulers: ['periodic']
|
||||
# other model params ...
|
||||
# other config sections ...
|
||||
@@ -531,11 +524,11 @@ As a result, switching from the offline Z-score model to the Online Z-score mode
|
||||
|
||||
**New configuration**:
|
||||
- `fit_window`: 4 hours
|
||||
- `fit_every`: 1000 days ( >1 week)
|
||||
- `fit_every`: 180 days ( >1 week)
|
||||
|
||||
The old configuration would perform 168 (hours in a week) `fit` calls, each using 2 days (48 hours) of data, totaling 168 * 48 = 8064 hours of data for each timeseries returned.
|
||||
|
||||
The new configuration performs only 1 `fit` call in 1000 days, using 4 hours of data initially, totaling 4 hours of data, which is **magnitudes smaller**.
|
||||
The new configuration performs only 1 `fit` call in 180 days, using 4 hours of data initially, totaling 4 hours of data, which is **magnitudes smaller**.
|
||||
|
||||
P.s. `infer` data volume will remain the same for both models, so it does not affect the overall calculations.
|
||||
|
||||
@@ -563,7 +556,9 @@ models:
|
||||
temporal_envelope:
|
||||
class: temporal_envelope
|
||||
# other model args
|
||||
queries: ['sum_alerts']
|
||||
queries: [
|
||||
'sum_alerts',
|
||||
]
|
||||
# other config sections
|
||||
```
|
||||
|
||||
@@ -583,7 +578,9 @@ models:
|
||||
temporal_envelope:
|
||||
class: temporal_envelope
|
||||
# other model args
|
||||
queries: ['sum_alerts']
|
||||
queries: [
|
||||
'sum_alerts',
|
||||
]
|
||||
# other config sections
|
||||
```
|
||||
|
||||
@@ -601,7 +598,10 @@ models:
|
||||
temporal_envelope:
|
||||
class: temporal_envelope
|
||||
# other model args
|
||||
queries: ['sum_alerts_pending', 'sum_alerts_firing']
|
||||
queries: [
|
||||
'sum_alerts_pending',
|
||||
'sum_alerts_firing',
|
||||
]
|
||||
# other config sections
|
||||
```
|
||||
|
||||
@@ -651,12 +651,10 @@ options:
|
||||
Minimum level to log. Default: INFO
|
||||
```
|
||||
|
||||
For a side-by-side comparison of all split modes and their resulting sub-configurations, see [splitting strategies](https://docs.victoriametrics.com/anomaly-detection/scaling-vmanomaly/#splitting-strategies).
|
||||
|
||||
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.2 && docker image tag victoriametrics/vmanomaly:v1.30.2 vmanomaly
|
||||
docker pull victoriametrics/vmanomaly:v1.30.1 && docker image tag victoriametrics/vmanomaly:v1.30.1 vmanomaly
|
||||
```
|
||||
|
||||
```sh
|
||||
@@ -689,11 +687,10 @@ reader:
|
||||
# ...
|
||||
queries:
|
||||
extra_big_query: metricsql_expression_returning_too_many_timeseries
|
||||
extra_filters: [
|
||||
extra_filters:
|
||||
# suppose you have a label `region` with values to deterministically define such subsets
|
||||
'{env="region_name_1"}',
|
||||
- '{env="region_name_1"}'
|
||||
# ...
|
||||
]
|
||||
```
|
||||
|
||||
```yaml
|
||||
@@ -703,11 +700,10 @@ reader:
|
||||
# ...
|
||||
queries:
|
||||
extra_big_query: metricsql_expression_returning_too_many_timeseries
|
||||
extra_filters: [
|
||||
extra_filters:
|
||||
# suppose you have a label `region` with values to deterministically define such subsets
|
||||
'{region="region_name_2"}',
|
||||
- '{region="region_name_2"}'
|
||||
# ...
|
||||
]
|
||||
```
|
||||
|
||||
## Monitoring vmanomaly
|
||||
|
||||
@@ -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.2](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1302) | 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.2 remains compatible with v1.30.1 state and its compatible predecessors; no persisted-state migration is required. |
|
||||
| [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) |
|
||||
|
||||
@@ -137,7 +137,7 @@ 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.2
|
||||
docker pull victoriametrics/vmanomaly:v1.30.1
|
||||
```
|
||||
|
||||
2. Create the license file with your license key.
|
||||
@@ -157,7 +157,7 @@ docker run -it \
|
||||
-v ./license:/license \
|
||||
-v ./config.yaml:/config.yaml \
|
||||
-p 8490:8490 \
|
||||
victoriametrics/vmanomaly:v1.30.2 \
|
||||
victoriametrics/vmanomaly:v1.30.1 \
|
||||
/config.yaml \
|
||||
--licenseFile=/license \
|
||||
--loggerLevel=INFO \
|
||||
@@ -174,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.2 \
|
||||
victoriametrics/vmanomaly:v1.30.1 \
|
||||
/config.yaml \
|
||||
--licenseFile=/license \
|
||||
--loggerLevel=INFO \
|
||||
@@ -187,7 +187,7 @@ services:
|
||||
# ...
|
||||
vmanomaly:
|
||||
container_name: vmanomaly
|
||||
image: victoriametrics/vmanomaly:v1.30.2
|
||||
image: victoriametrics/vmanomaly:v1.30.1
|
||||
# ...
|
||||
restart: always
|
||||
volumes:
|
||||
@@ -250,13 +250,12 @@ Before deploying, check the correctness of your configuration validate config fi
|
||||
|
||||
### Example
|
||||
|
||||
Here is an example of a config file that runs the online [Temporal Envelope](https://docs.victoriametrics.com/anomaly-detection/components/models/#temporal-envelope) model on a CPU metric. The scheduler runs inference every five minutes and uses the fit only for initial bootstrap; between fits the model updates causally from each inference batch. The initial fit uses four weeks of data. The model produces `anomaly_score`, `yhat`, `yhat_lower`, and `yhat_upper` [series](https://docs.victoriametrics.com/anomaly-detection/components/models/#vmanomaly-output) for debugging, and its hour-of-day and day-of-week profiles follow the query timezone and daylight-saving-time changes.
|
||||
Here is an example of a config file that runs the online [Temporal Envelope](https://docs.victoriametrics.com/anomaly-detection/components/models/#temporal-envelope) model on a CPU metric. The scheduler runs inference every five minutes and performs a full refit only every 100 weeks; between refits the model updates causally from each inference batch. The initial fit uses four weeks of data. The model produces `anomaly_score`, `yhat`, `yhat_lower`, and `yhat_upper` [series](https://docs.victoriametrics.com/anomaly-detection/components/models/#vmanomaly-output) for debugging, and its hour-of-day and day-of-week profiles follow the query timezone and daylight-saving-time changes.
|
||||
|
||||
```yaml
|
||||
settings:
|
||||
# https://docs.victoriametrics.com/anomaly-detection/components/settings/
|
||||
n_workers: 2 # number of workers to run workload in parallel, set to 0 or negative number to use all available CPU cores
|
||||
native_threads_per_worker: 0 # automatically divide container-aware CPU capacity across workers
|
||||
anomaly_score_outside_data_range: 5.0 # default anomaly score for anomalies outside expected data range
|
||||
restore_state: true # restore state from previous run, available since v1.24.0
|
||||
# https://docs.victoriametrics.com/anomaly-detection/components/settings/#logger-levels
|
||||
@@ -269,12 +268,13 @@ settings:
|
||||
model.online.temporal_envelope: WARNING
|
||||
|
||||
schedulers:
|
||||
online_5m:
|
||||
100w_5m:
|
||||
# https://docs.victoriametrics.com/anomaly-detection/components/scheduler/#periodic-scheduler
|
||||
class: 'periodic'
|
||||
infer_every: '5m'
|
||||
scatter_infer_jobs: true
|
||||
fit_every: '1000d'
|
||||
# Temporal Envelope learns online between full refits.
|
||||
fit_every: '100w'
|
||||
fit_window: '4w'
|
||||
|
||||
models:
|
||||
@@ -282,7 +282,7 @@ models:
|
||||
temporal_envelope_model:
|
||||
class: 'temporal_envelope'
|
||||
queries: ['cpu_user']
|
||||
schedulers: ['online_5m']
|
||||
schedulers: ['100w_5m']
|
||||
provide_series: ['anomaly_score', 'yhat', 'yhat_lower', 'yhat_upper'] # for debugging
|
||||
seasonalities: ['hod_smooth', 'dow_smooth']
|
||||
alpha: 0.005 # trend reactivity; try 0.0025-0.02
|
||||
@@ -297,15 +297,12 @@ reader:
|
||||
tenant_id: '0:0'
|
||||
sampling_period: "5m"
|
||||
tz: 'UTC' # set the IANA timezone that defines local calendar patterns, e.g. 'America/New_York'
|
||||
workers: 0 # automatically choose bounded datasource concurrency
|
||||
series_processing_batch_size: 8 # number of time series to process together while preparing data for fit or infer stages
|
||||
queries:
|
||||
# define your queries with MetricsQL - https://docs.victoriametrics.com/victoriametrics/metricsql/
|
||||
cpu_user:
|
||||
expr: 'sum(rate(node_cpu_seconds_total{mode=~"user"}[10m])) by (container)'
|
||||
data_range: [0, 'inf'] # query-level business policy from v1.30.2
|
||||
detection_direction: 'above_expected' # query-level from v1.30.2; only spikes are anomalous
|
||||
max_points_per_query: 15000 # to deal with longer queries hitting search.maxPointsPerTimeseries
|
||||
max_datapoints_per_query: 15000 # to deal with longer queries hitting search.MaxPointsPerTimeseries
|
||||
# other queries ...
|
||||
|
||||
writer:
|
||||
|
||||
@@ -32,15 +32,14 @@ schedulers:
|
||||
periodic_1d: # alias
|
||||
class: 'periodic' # scheduler class
|
||||
infer_every: "30s"
|
||||
fit_every: "1000d"
|
||||
fit_every: "1h"
|
||||
fit_window: "24h"
|
||||
|
||||
# https://docs.victoriametrics.com/anomaly-detection/components/models/
|
||||
models:
|
||||
zscore: # we can set up alias for model
|
||||
class: 'zscore_online' # online model class
|
||||
class: 'zscore' # model class
|
||||
z_threshold: 3.5
|
||||
decay: 0.99 # give more weight to recent data while using the bootstrap-only fit schedule
|
||||
queries: ['cpu_seconds_total', 'host_network_receive_errors']
|
||||
|
||||
# https://docs.victoriametrics.com/anomaly-detection/components/reader/#vm-reader
|
||||
@@ -81,7 +80,6 @@ Additionally, a replication factor `R ≥ 1` ensures [high availability](#high-a
|
||||
|
||||
{{% content "vmanomaly-sharding-ha-diagram.md" %}}
|
||||
|
||||
> [!WARNING]
|
||||
> 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/).
|
||||
|
||||
Sharding configuration can be controlled by using the following environment variables:
|
||||
@@ -89,94 +87,7 @@ Sharding configuration can be controlled by using the following environment vari
|
||||
- **`VMANOMALY_MEMBERS_COUNT`**: Defines the total number of shards (i.e., available nodes to distribute [sub-configurations](#sub-configuration) to). <br>Defaults to `1` for backward compatibility.
|
||||
- **`VMANOMALY_MEMBER_NUM`**: Specifies the shard index (`0` to `VMANOMALY_MEMBERS_COUNT - 1`), determining the subset of [sub-configurations](#sub-configuration) to run on a specific node. Defaults to `0`. Supports automatic **pod name discovery** in Kubernetes [StatefulSets](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/) (e.g., if set to `vmanomaly-node-exporter-7`, shard `7` will be extracted).
|
||||
- **`VMANOMALY_REPLICATION_FACTOR`**: If `R > 1`, enables [high availability](#high-availability) by ensuring each [sub-configuration](#sub-configuration) is assigned to exactly `R` shards. Defaults to `1` (no replication).
|
||||
- **`VMANOMALY_SPLIT_BY`**: Defines the logical entity used to split the global config into [sub-configurations](#sub-configuration). The accepted values are `SCHEDULERS`, `MODELS`, `QUERIES`, `EXTRA_FILTERS`, and `COMPLETE` (case-insensitive). It defaults to `COMPLETE`, which usually provides the most granular and balanced distribution.
|
||||
|
||||
The split strategies differ as follows:
|
||||
|
||||
| `VMANOMALY_SPLIT_BY` | Unit of work in each sub-configuration | Recommended use |
|
||||
| --- | --- | --- |
|
||||
| `SCHEDULERS` | One scheduler and the workload attached to it | Separate workloads by fit and inference cadence. The number of sub-configurations is limited by the number of referenced schedulers. |
|
||||
| `MODELS` | One configured model alias with its attached schedulers and queries | Isolate computationally different models or distribute several models that process the same queries. |
|
||||
| `QUERIES` | One query for [univariate models](https://docs.victoriametrics.com/anomaly-detection/components/models/#univariate-models); the complete attached query set for each [multivariate model](https://docs.victoriametrics.com/anomaly-detection/components/models/#multivariate-models) | Distribute independent query workloads. Queries belonging to one multivariate model remain together because the model needs all channels. This option does not split the series returned by one query. |
|
||||
| `EXTRA_FILTERS` | One configured `reader.extra_filters` selector, with the full model/query/scheduler topology retained | Partition the series returned by large queries, for example by region, cluster, another stable label, or by [VictoriaMetrics tenant](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#multitenancy-via-labels) using `vm_account_id` and `vm_project_id` selectors with the multitenant endpoint. The filters must already be defined in the global configuration. |
|
||||
| `COMPLETE` | One valid scheduler/model/query combination; [multivariate](https://docs.victoriametrics.com/anomaly-detection/components/models/#multivariate-models) query sets remain together | Obtain the finest general-purpose split and the default choice for balanced sharding. `reader.extra_filters` are intentionally not expanded by this strategy. |
|
||||
|
||||
After the selected strategy creates the sub-configurations, they are assigned to members in deterministic round-robin order and then replicated according to `VMANOMALY_REPLICATION_FACTOR`.
|
||||
|
||||
### Splitting strategies
|
||||
|
||||
{{% collapse name="Configuration and resulting sub-configurations" %}}
|
||||
|
||||
The following abbreviated global configuration contains two schedulers, two models, four queries, and two data partitions:
|
||||
|
||||
```yaml
|
||||
schedulers:
|
||||
fast:
|
||||
class: periodic
|
||||
infer_every: 1m
|
||||
fit_every: 1000d
|
||||
fit_window: 1d
|
||||
seasonal:
|
||||
class: periodic
|
||||
infer_every: 5m
|
||||
fit_every: 1000d
|
||||
fit_window: 2w
|
||||
|
||||
models:
|
||||
cpu_zscore:
|
||||
class: zscore_online
|
||||
schedulers: [fast]
|
||||
queries: [cpu, error_rate]
|
||||
decay: 0.99
|
||||
gpu_envelope:
|
||||
class: temporal_envelope_multivariate
|
||||
schedulers: [seasonal]
|
||||
queries: [temperature, power]
|
||||
seasonalities: [hod_smooth, dow_smooth]
|
||||
|
||||
reader:
|
||||
class: vm
|
||||
datasource_url: http://victoriametrics:8428/
|
||||
sampling_period: 1m
|
||||
queries:
|
||||
cpu:
|
||||
expr: avg(rate(node_cpu_seconds_total[5m])) by (instance)
|
||||
error_rate:
|
||||
expr: rate(application_errors_total[5m])
|
||||
temperature:
|
||||
expr: avg(gpu_temperature_celsius) by (gpu)
|
||||
power:
|
||||
expr: avg(gpu_power_watts) by (gpu)
|
||||
extra_filters: ['{region="us-east"}', '{region="eu-west"}']
|
||||
|
||||
writer:
|
||||
class: vm
|
||||
datasource_url: http://victoriametrics:8428/
|
||||
```
|
||||
|
||||
For this configuration, each strategy produces the following logical units before they are assigned to shards:
|
||||
|
||||
| Value | Resulting sub-configurations |
|
||||
| --- | --- |
|
||||
| `SCHEDULERS` | `fast`; `seasonal` |
|
||||
| `MODELS` | `cpu_zscore`; `gpu_envelope` |
|
||||
| `QUERIES` | `cpu`; `error_rate`; the multivariate set `power,temperature` |
|
||||
| `EXTRA_FILTERS` | `{region="us-east"}`; `{region="eu-west"}`; each retains all schedulers, models, and queries, while the query context is restricted by its selector |
|
||||
| `COMPLETE` | `fast:cpu_zscore:cpu`; `fast:cpu_zscore:error_rate`; `seasonal:gpu_envelope:power,temperature` |
|
||||
|
||||
For example, choose the query split with:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
VMANOMALY_MEMBERS_COUNT: 3
|
||||
VMANOMALY_MEMBER_NUM: 0
|
||||
VMANOMALY_REPLICATION_FACTOR: 1
|
||||
VMANOMALY_SPLIT_BY: QUERIES
|
||||
```
|
||||
|
||||
To partition the timeseries returned by the same large query instead, define non-overlapping selectors in `reader.extra_filters` and use `VMANOMALY_SPLIT_BY: EXTRA_FILTERS`. Each generated sub-configuration keeps one selector, for example `{region="us-east"}` or `{region="eu-west"}`.
|
||||
|
||||
{{% /collapse %}}
|
||||
- **`VMANOMALY_SPLIT_BY`**: Defines the logical entity used to split the global config into [sub-configurations](#sub-configuration). Defaults to `complete`, which provides the most granular distribution (1 model per [sub-config](#sub-configuration), mapped to 1 query and attached to 1 scheduler) for balanced workloads.
|
||||
|
||||
---
|
||||
|
||||
@@ -219,7 +130,6 @@ When `VMANOMALY_REPLICATION_FACTOR` > 1, each [sub-config](#sub-configuration) `
|
||||
|
||||
{{% content "vmanomaly-sharding-ha-diagram.md" %}}
|
||||
|
||||
> [!WARNING]
|
||||
> 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/).
|
||||
|
||||
### Example
|
||||
@@ -288,11 +198,7 @@ services:
|
||||
user: "1000:1000"
|
||||
restart: always
|
||||
healthcheck:
|
||||
test:
|
||||
- "CMD"
|
||||
- "curl"
|
||||
- "-f"
|
||||
- "http://127.0.0.1:8490/health"
|
||||
test: ["CMD", "curl", "-f", "http://127.0.0.1:8490/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
@@ -312,11 +218,7 @@ services:
|
||||
user: "1000:1000"
|
||||
restart: always
|
||||
healthcheck:
|
||||
test:
|
||||
- "CMD"
|
||||
- "curl"
|
||||
- "-f"
|
||||
- "http://127.0.0.1:8490/health"
|
||||
test: ["CMD", "curl", "-f", "http://127.0.0.1:8490/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
|
||||
@@ -137,13 +137,13 @@ users:
|
||||
password: '<password>'
|
||||
url_map:
|
||||
- src_hosts:
|
||||
- "metrics.local.some-domain.net"
|
||||
- "metrics.local.some-domain.net"
|
||||
url_prefix: "http://victoriametrics:8428"
|
||||
- src_hosts:
|
||||
- "vl.local.some-domain.net"
|
||||
- "vl.local.some-domain.net"
|
||||
url_prefix: "http://victorialogs:9428"
|
||||
- src_hosts:
|
||||
- "vmanomaly.local.some-domain.net"
|
||||
- "vmanomaly.local.some-domain.net"
|
||||
url_prefix: "http://vmanomaly:8490"
|
||||
keep_original_host: true
|
||||
```
|
||||
@@ -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.2 \
|
||||
victoriametrics/vmanomaly:v1.30.1 \
|
||||
vmanomaly_config.yaml
|
||||
```
|
||||
|
||||
@@ -645,13 +645,6 @@ If the **results** look good and the **model configuration should be deployed in
|
||||
|
||||
{{% collapse name="Release history" %}}
|
||||
|
||||
### v1.8.2
|
||||
Released: 2026-08-13
|
||||
|
||||
vmanomaly version: [v1.30.2](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1302)
|
||||
|
||||
- BUGFIX: Fixed tenant discovery for VictoriaMetrics datasource URLs containing `/select/multitenant/prometheus`. The UI now loads available numeric tenants from `/admin/tenants` and can switch the datasource URL from `multitenant` to the selected tenant.
|
||||
|
||||
### v1.8.1
|
||||
Released: 2026-08-06
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ The following minimal configuration demonstrates current many-to-many model, que
|
||||
```yaml
|
||||
settings:
|
||||
n_workers: 4 # number of workers to run models in parallel
|
||||
native_threads_per_worker: 0 # automatically divide container-aware CPU capacity across workers
|
||||
anomaly_score_outside_data_range: 5.0 # default anomaly score for anomalies outside expected data range
|
||||
restore_state: True # restore state from previous run, if available
|
||||
retention: # how long to keep stale models on disk/in memory
|
||||
@@ -51,15 +50,15 @@ schedulers:
|
||||
class: 'periodic' # scheduler class
|
||||
infer_every: "30s" # how often to produce anomaly scores for new data
|
||||
scatter_infer_jobs: true # distribute infer jobs evenly across the infer interval to reduce synchronized bursts
|
||||
fit_every: "1000d" # bootstrap-only schedule; use a finite cadence if accumulated state must be reset
|
||||
fit_every: "365d" # how often to re-fit the models, for online models used effectively once, then they are updated with new data and won't require re-fit
|
||||
fit_window: "3d" # how much historical data to use for fit stage
|
||||
start_from: "00:00" # align the bootstrap fit to midnight in the configured timezone
|
||||
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_online_weekly:
|
||||
class: 'periodic'
|
||||
infer_every: "15m"
|
||||
scatter_infer_jobs: true
|
||||
fit_every: "1000d" # bootstrap-only schedule; use a finite cadence if accumulated state must be reset
|
||||
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
|
||||
|
||||
@@ -73,15 +72,17 @@ models:
|
||||
provide_series: ['anomaly_score', 'y', 'yhat', 'yhat_upper'] # what series to produce as output of the model
|
||||
queries: ['host_network_receive_errors'] # what queries to run particular model on
|
||||
schedulers: ['periodic_online'] # will be fit once, used for infer every 30s
|
||||
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
|
||||
envelope_weekly: # we can set up alias for model
|
||||
class: 'temporal_envelope'
|
||||
alpha: 0.005 # adapt the trend while using the bootstrap-only fit schedule
|
||||
loss_reactivity: 3 # allow new deviations to update the envelope
|
||||
provide_series: ['anomaly_score', 'y', 'yhat', 'yhat_lower', 'yhat_upper']
|
||||
queries: ['cpu_seconds_total']
|
||||
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`
|
||||
seasonalities: ['hod_smooth', 'dow_smooth']
|
||||
|
||||
@@ -92,7 +93,6 @@ reader:
|
||||
datasource_url: "https://play.victoriametrics.com/"
|
||||
tenant_id: "0:0"
|
||||
sampling_period: "30s" # what data resolution to fetch from VictoriaMetrics' /query_range endpoint
|
||||
workers: 0 # automatically choose bounded datasource concurrency
|
||||
latency_offset: '1ms'
|
||||
query_from_last_seen_timestamp: False
|
||||
tz: "UTC" # timezone to use for queries without explicit timezone
|
||||
@@ -101,15 +101,11 @@ reader:
|
||||
cpu_seconds_total:
|
||||
expr: 'avg(rate(node_cpu_seconds_total[5m])) by (mode)'
|
||||
# step: '30s' # if not set, will be equal to reader-level sampling_period
|
||||
data_range: [0, 'inf'] # query-level business policy from v1.30.2
|
||||
detection_direction: 'above_expected' # query-level from v1.30.2; detect spikes only
|
||||
min_dev_from_expected: [0.01, 0.01] # query-level from v1.30.2
|
||||
data_range: [0, 'inf'] # expected value range, anomaly_score = anomaly_score_outside_data_range if y (real value) is outside
|
||||
host_network_receive_errors:
|
||||
expr: 'rate(node_network_receive_errs_total[3m]) / rate(node_network_receive_packets_total[3m])'
|
||||
step: '15m' # here we override per-query `sampling_period` to request way less data from VM TSDB
|
||||
data_range: [0, 'inf'] # query-level business policy from v1.30.2
|
||||
detection_direction: 'above_expected' # query-level from v1.30.2; detect spikes only
|
||||
min_dev_from_expected: 0.0 # query-level from v1.30.2; absolute-deviation filtering is disabled
|
||||
data_range: [0, 'inf']
|
||||
|
||||
# where to write data to
|
||||
# https://docs.victoriametrics.com/anomaly-detection/components/writer/
|
||||
@@ -150,7 +146,7 @@ server:
|
||||
|
||||
{{% available_from "v1.25.0" anomaly %}} The service supports hot reload of configuration files, applying changes without an explicit restart. Enable it with the `--watch` [CLI argument](https://docs.victoriametrics.com/anomaly-detection/quickstart/#command-line-arguments). The `vmanomaly_config_reload_enabled` [self-monitoring metric](https://docs.victoriametrics.com/anomaly-detection/components/monitoring/#startup-metrics) is `1` when hot reload is enabled and `0` otherwise.
|
||||
|
||||
> [!WARNING]
|
||||
> [!NOTE]
|
||||
> {{% deprecated_from "v1.29.5" anomaly %}} File system event-based hot reload has been deprecated in favor of content-based polling with configurable `-configCheckInterval` due to reliability issues with Kubernetes ConfigMap symlink rotations and other filesystems where event delivery can be inconsistent. If you were using file system event-based hot reload, please switch to content-based polling by enabling `--watch` flag and configuring `-configCheckInterval` as needed.
|
||||
|
||||
### How it works
|
||||
@@ -177,7 +173,7 @@ schedulers:
|
||||
periodic:
|
||||
class: 'periodic'
|
||||
infer_every: "30s"
|
||||
fit_every: "1000d" # bootstrap-only schedule; use a finite cadence if accumulated state must be reset
|
||||
fit_every: "365d"
|
||||
fit_window: "24h"
|
||||
|
||||
reader:
|
||||
|
||||
@@ -65,9 +65,6 @@ models:
|
||||
|
||||
Common arguments supported by every model were introduced in [v1.10.0](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1100).
|
||||
|
||||
> [!WARNING]
|
||||
> Configuring `data_range`, `detection_direction`, `min_dev_from_expected`, or `min_rel_dev_from_expected` at model level is deprecated {{% deprecated_from "v1.30.2" anomaly %}}. These stable KPI policies belong under [`reader.queries.<alias>`](https://docs.victoriametrics.com/anomaly-detection/components/reader/#per-query-parameters), where they remain consistent across every [univariate](#univariate-models) or [multivariate](#multivariate-models) model that uses the query. Existing model-level values remain compatible as model-local fallbacks when an attached query does not define the corresponding field; an explicit query value is authoritative.
|
||||
|
||||
<div class="collapse-group">
|
||||
|
||||
{{% collapse name="Queries" %}}
|
||||
@@ -148,46 +145,61 @@ models:
|
||||
{{% 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`. Configure it on the input query; model-level placement is {{% deprecated_from "v1.30.2" anomaly %}}.
|
||||
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`.
|
||||
|
||||
Here's how the three options differ:
|
||||
Here's how default (backward-compatible) behavior looks like - anomalies will be tracked in `both` directions (`y > yhat` or `y < yhat`). This is useful when there is no domain expertise to filter the required direction.
|
||||
|
||||

|
||||

|
||||
|
||||
With the default, backward-compatible `both` value, anomalies are tracked in both directions (`y > yhat` or `y < yhat`). This is useful when there is no domain expertise to filter the required direction.
|
||||
|
||||
When set to `above_expected`, anomalies are tracked only when `y > yhat`.
|
||||
|
||||
*Example metrics*: Error rate, response time, page load time, number of failed transactions - metrics where *lower values are better*, so **higher** values are typically tracked.
|
||||
|
||||

|
||||
|
||||
|
||||
When set to `below_expected`, anomalies are tracked only when `y < yhat`.
|
||||
|
||||
*Example metrics*: Service Level Agreement (SLA) compliance, conversion rate, Customer Satisfaction Score (CSAT) - metrics where *higher values are better*, so **lower** values are typically tracked.
|
||||
|
||||
One model can use multiple queries with different directions because the policy belongs to each query:
|
||||

|
||||
|
||||
|
||||
Config with a split example:
|
||||
|
||||
```yaml
|
||||
models:
|
||||
model_above_expected:
|
||||
class: 'zscore_online'
|
||||
z_threshold: 3.0
|
||||
# track only cases when y > yhat, otherwise anomaly_score would be explicitly set to 0
|
||||
detection_direction: 'above_expected'
|
||||
# for this query we do not need to track lower values, thus, set anomaly detection tracking for y > yhat (above_expected)
|
||||
queries: ['query_values_the_lower_the_better']
|
||||
model_below_expected:
|
||||
class: 'zscore_online'
|
||||
z_threshold: 3.0
|
||||
# track only cases when y < yhat, otherwise anomaly_score would be explicitly set to 0
|
||||
detection_direction: 'below_expected'
|
||||
# for this query we do not need to track higher values, thus, set anomaly detection tracking for y < yhat (above_expected)
|
||||
queries: ['query_values_the_higher_the_better']
|
||||
model_bidirectional_default:
|
||||
class: 'zscore_online'
|
||||
z_threshold: 3.0
|
||||
# track in both direction, same backward-compatible behavior in case this arg is missing
|
||||
detection_direction: 'both'
|
||||
# for this query both directions can be equally important for anomaly detection, thus, setting it bidirectional (both)
|
||||
queries: ['query_values_both_direction_matters']
|
||||
reader:
|
||||
# ...
|
||||
queries:
|
||||
query_values_the_lower_the_better:
|
||||
query_values_the_lower_the_better:
|
||||
expr: metricsql_expression1
|
||||
detection_direction: 'above_expected' # query-level from v1.30.2; only y > yhat can be anomalous
|
||||
query_values_the_higher_the_better:
|
||||
query_values_the_higher_the_better:
|
||||
expr: metricsql_expression2
|
||||
detection_direction: 'below_expected' # query-level from v1.30.2; only y < yhat can be anomalous
|
||||
query_values_both_direction_matters:
|
||||
query_values_both_direction_matters:
|
||||
expr: metricsql_expression3
|
||||
detection_direction: 'both' # query-level from v1.30.2; the default when omitted
|
||||
models:
|
||||
model_all_directions:
|
||||
class: 'zscore_online'
|
||||
z_threshold: 3.0
|
||||
queries: [
|
||||
'query_values_the_lower_the_better',
|
||||
'query_values_the_higher_the_better',
|
||||
'query_values_both_direction_matters',
|
||||
]
|
||||
# other components like writer, schedule, monitoring
|
||||
```
|
||||
|
||||
@@ -197,7 +209,7 @@ models:
|
||||
|
||||
### 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. Configure it on the input query; model-level placement is {{% deprecated_from "v1.30.2" anomaly %}}.
|
||||
`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.
|
||||
|
||||
> [!NOTE]
|
||||
{{% available_from "v1.23.0" anomaly %}} The `min_dev_from_expected` argument can be a list of two float values, allowing separate thresholds for upper and lower deviations. This is useful when the acceptable deviation varies in different directions (e.g., `min_dev_from_expected: [0.01, 0.02]` means that the lower bound is `0.01` when `y` is less than `yhat` and the upper bound is `0.02` when `y` is greater than `yhat`). If only one value is provided, it is broadcasted to both directions, meaning that the same threshold is applied for both upper and lower deviations (e.g., `min_dev_from_expected: 0.01` means that the lower bound is `0.01` when `y` is less than `yhat` and the upper bound is also `0.01` when `y` is greater than `yhat`).
|
||||
@@ -206,9 +218,15 @@ models:
|
||||
|
||||
*Example*: Consider a scenario where CPU utilization in specific mode is low and oscillates around 0.3% (0.003). A sudden spike to 1.3% (0.013) represents a +333% increase in **relative** terms, but only a +1 percentage point (0.01) increase in **absolute** terms, which may be negligible and not warrant an alert. Setting the `min_dev_from_expected` argument to `0.01` (1%) will ensure that all anomaly scores for deviations <= `0.01` are set to 0.
|
||||
|
||||
The visualization below demonstrates this concept. The narrow blue model prediction boundary is nested inside the wider green business protection boundary. Actual values outside the prediction boundary but still within `[yhat - min_dev_from_expected, yhat + min_dev_from_expected]` receive `anomaly_score = 0`; only values outside the green boundary remain anomalous.
|
||||
Visualizations below demonstrate this concept; the green zone defined as the `[yhat - min_dev_from_expected, yhat + min_dev_from_expected]` range excludes actual data points (`y`) from generating anomaly scores if they fall within that range.
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
Example config of how to use this param based on query results:
|
||||
|
||||
@@ -218,17 +236,23 @@ reader:
|
||||
# ...
|
||||
queries:
|
||||
# the usage of min_dev should reduce false positives here
|
||||
need_to_include_min_dev:
|
||||
need_to_include_min_dev:
|
||||
expr: small_abs_values_metricsql_expression
|
||||
min_dev_from_expected: [5.0, 5.0] # query-level from v1.30.2
|
||||
# min_dev is not really needed here
|
||||
normal_behavior:
|
||||
normal_behavior:
|
||||
expr: no_need_to_exclude_small_deviations_metricsql_expression
|
||||
models:
|
||||
zscore:
|
||||
zscore_with_min_dev:
|
||||
class: 'zscore_online'
|
||||
z_threshold: 3
|
||||
queries: ['need_to_include_min_dev', 'normal_behavior']
|
||||
min_dev_from_expected: [5.0, 5.0] # set the same threshold for both directions, meaning that deviations less than 5.0 in absolute values won't be considered anomalous, even if they are relatively significant
|
||||
queries: ['need_to_include_min_dev'] # use such models on queries where domain experience confirm usefulness
|
||||
zscore_wo_min_dev:
|
||||
class: 'zscore_online'
|
||||
z_threshold: 3
|
||||
# if not set, equals to setting min_dev_from_expected == 0 (meaning no filtering is applied)
|
||||
# min_dev_from_expected: [0.0, 0.0]
|
||||
queries: ['normal_behavior'] # use the default where it's not needed
|
||||
```
|
||||
|
||||
{{% /collapse %}}
|
||||
@@ -237,17 +261,13 @@ models:
|
||||
|
||||
### 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. Configure it on the input query; model-level placement is {{% deprecated_from "v1.30.2" anomaly %}}.
|
||||
{{% 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.
|
||||
|
||||
Parameter can be a list of two float values, *allowing separate thresholds for upper and lower relative deviations*. If only one value is provided, it is broadcasted to both directions.
|
||||
|
||||
> [!NOTE]
|
||||
If both `min_dev_from_expected` [arg](#minimal-deviation-from-expected) and `min_rel_dev_from_expected` are set, the model will combine both filters. A data point will be considered anomalous (i.e., have an anomaly score != 0) only if it exceeds **both** the *absolute* deviation threshold defined by `min_dev_from_expected` and the *relative* deviation threshold defined by `min_rel_dev_from_expected`. This allows for more granular control over anomaly detection, ensuring that only significant deviations in both absolute and relative terms are flagged as anomalies.
|
||||
|
||||
The green business protection boundary below scales with `|yhat|`, while the model prediction boundary remains visible inside it. Actual values outside the blue boundary but inside the proportional green boundary receive `anomaly_score = 0`.
|
||||
|
||||

|
||||
|
||||
|
||||
*Example*: Consider a scenario of monitoring incoming traffic to websites that typically receives *unknown in advance* requests per second (from tens to thousands). Setting absolute deviation threshold with `min_dev_from_expected` *may not be effective in reducing false positives*, as even a small increase in traffic (e.g., from 10 to 20 requests per second) can represent a 100% relative increase, which may be significant for that website. Instead, setting `min_rel_dev_from_expected` to smaller relative value - `[20, 40]` (20/40%) - will ensure that traffic drop from 10 to 8 requests per second (20% decrease) and traffic spike from 10 to 14 requests per second (40% increase) won't be considered anomalous, even if they exceed confidence intervals, thus, reducing false positives for small absolute deviations that are relatively significant.
|
||||
|
||||
@@ -259,17 +279,23 @@ reader:
|
||||
# ...
|
||||
queries:
|
||||
# the usage of min_rel_dev should reduce false positives here
|
||||
need_to_include_min_rel_dev:
|
||||
need_to_include_min_rel_dev:
|
||||
expr: small_abs_values_metricsql_expression
|
||||
min_rel_dev_from_expected: [10, 20] # query-level from v1.30.2
|
||||
# min_rel_dev is not really needed here
|
||||
normal_behavior:
|
||||
normal_behavior:
|
||||
expr: no_need_to_exclude_small_deviations_metricsql_expression
|
||||
models:
|
||||
zscore:
|
||||
zscore_with_min_rel_dev:
|
||||
class: 'zscore_online'
|
||||
z_threshold: 3
|
||||
queries: ['need_to_include_min_rel_dev', 'normal_behavior']
|
||||
min_rel_dev_from_expected: [10, 20] # set different thresholds for both directions, meaning that relative deviations less than 10% when y < yhat and less than 20% when y > yhat won't be considered anomalous, even if they exceed confidence intervals, thus, reducing false positives for small absolute deviations that are relatively significant
|
||||
queries: ['need_to_include_min_rel_dev'] # use such models on queries where domain experience confirm usefulness
|
||||
zscore_wo_min_rel_dev:
|
||||
class: 'zscore_online'
|
||||
z_threshold: 3
|
||||
# if not set, equals to setting min_rel_dev_from_expected == 0 (meaning no filtering is applied)
|
||||
# min_rel_dev_from_expected: [0, 0]
|
||||
queries: ['normal_behavior'] # use the default where it's not needed
|
||||
```
|
||||
|
||||
|
||||
@@ -292,29 +318,17 @@ reader:
|
||||
# assume there are M unique hosts identified by the `host` label
|
||||
queries:
|
||||
# return one timeseries for each CPU mode per host, total = N*M timeseries
|
||||
cpu:
|
||||
expr: sum(rate(node_cpu_seconds_total[5m])) by (host, mode)
|
||||
data_range: [0, 'inf']
|
||||
detection_direction: both
|
||||
min_rel_dev_from_expected: [15, 15]
|
||||
cpu: sum(rate(node_cpu_seconds_total[5m])) by (host, mode)
|
||||
# return one timeseries per host, total = 1*M timeseries
|
||||
ram:
|
||||
expr: |
|
||||
100 * (
|
||||
1 - node_memory_MemAvailable_bytes
|
||||
/ node_memory_MemTotal_bytes
|
||||
)
|
||||
data_range: [0, 100]
|
||||
detection_direction: above_expected
|
||||
min_rel_dev_from_expected: [0, 15]
|
||||
ram: |
|
||||
(
|
||||
(node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes)
|
||||
/ node_memory_MemTotal_bytes
|
||||
) * 100 by (host)
|
||||
# return one timeseries per host for both network receive and transmit data, total = 1*M timeseries
|
||||
network:
|
||||
expr: |
|
||||
sum(rate(node_network_receive_bytes_total[5m])) by (host)
|
||||
+ sum(rate(node_network_transmit_bytes_total[5m])) by (host)
|
||||
data_range: [0, 'inf']
|
||||
detection_direction: below_expected
|
||||
min_rel_dev_from_expected: [20, 0]
|
||||
network: |
|
||||
sum(rate(node_network_receive_bytes_total[5m])) by (host)
|
||||
+ sum(rate(node_network_transmit_bytes_total[5m])) by (host)
|
||||
|
||||
models:
|
||||
envelope: # alias for the model
|
||||
@@ -328,9 +342,6 @@ models:
|
||||
groupby: [host]
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> {{% available_from "v1.30.2" anomaly %}} Multivariate Temporal Envelope applies each query's [`data_range`, `detection_direction`, and minimum relative deviation](https://docs.victoriametrics.com/anomaly-detection/components/reader/#per-query-parameters) to every channel returned by that query before aggregating the joint anomaly score. The example detects CPU deviations in either direction, RAM increases of at least 15%, and network drops of at least 20% within each host model.
|
||||
|
||||
{{% /collapse %}}
|
||||
|
||||
{{% collapse name="Scale" %}}
|
||||
@@ -348,10 +359,6 @@ For example, setting `scale: [1.2, 0.75]` for particular model will:
|
||||
- **Increase** the width of the lower confidence interval by **20%**.
|
||||
- **Decrease** the width of the upper confidence boundary by **25%**.
|
||||
|
||||
Alternative visualization:
|
||||
|
||||

|
||||
|
||||
The most common **use case** is when there is a preference to **widen one side** to blacklist smaller false positives (which otherwise would have [anomaly scores](https://docs.victoriametrics.com/anomaly-detection/faq/#how-is-anomaly-score-calculated) **only slightly higher than 1.0**, still making such data points **anomalous**), while **tightening the other side** to avoid missing true positives due to an overly loose margin (leading to [anomaly scores](https://docs.victoriametrics.com/anomaly-detection/faq/#how-is-anomaly-score-calculated) being slightly less than 1.0, making such data points **non-anomalous**).
|
||||
|
||||
```yaml
|
||||
@@ -550,8 +557,6 @@ For a multivariate model, **one shared model instance** is fitted and used acros
|
||||
|
||||
For example, if you have some **multivariate** model to use 3 [MetricQL queries](https://docs.victoriametrics.com/victoriametrics/metricsql/), each returning 5 time series, there will be one shared model created in total. Once fit, this model will expect **exactly 15 time series with exact same labelsets as an input**. This model will produce **one shared [output](#vmanomaly-output)**.
|
||||
|
||||
> {{% available_from "v1.30.2" anomaly %}} Multivariate Temporal Envelope and Isolation Forest accept matching input channels in any order. The channel set must still match the fitted model exactly: missing, extra, and duplicate channels are rejected, while a matching set is restored to learned fit order before inference or online updates.
|
||||
|
||||
> {{% available_from "v1.16.0" anomaly %}} N models — one for each N unique combinations of label values specified in the `groupby` [common argument](#group-by) — can be trained. This allows for context separation (e.g., one model per host, region, or other relevant grouping label), leading to improved accuracy and faster training. See an example [here](#group-by).
|
||||
|
||||
If during an inference, you got a **different amount of series** or some series having a **new labelset** (not present in any of fitted models), the inference will be skipped until you get a model, trained particularly for such labelset during forthcoming re-fit step.
|
||||
@@ -680,7 +685,7 @@ Selecting model [hyperparameters](https://en.wikipedia.org/wiki/Hyperparameter_(
|
||||
- `tuned_class_name` (string) - [Built-in model class](#built-in-models) to wrap, i.e. `zscore_online`
|
||||
- `optimization_params` (dict) - Optimization parameters for *unsupervised* model tuning. Control percentage of found anomalies, as well as a tradeoff between time spent and the accuracy. The higher `timeout` and `n_trials` are, the better model configuration can be found for `tuned_class_name`, but the longer it takes and vice versa. Set `n_jobs` to `-1` to use all the CPUs available, it makes sense if only you have a big dataset to train on during `fit` calls, otherwise overhead isn't worth it.
|
||||
- `anomaly_percentage` (float) - Expected percentage of anomalies that can be seen in training data, from `[0, 0.5)` interval (i.e. 0.01 means it's expected ~ 1% of anomalies to be present in training data). This is a *required* parameter.
|
||||
- `optimized_business_params` (list[string]) - {{% available_from "v1.15.0" anomaly %}} Experimental optimization of model-level business parameters is {{% deprecated_from "v1.30.2" anomaly %}}. Keep this list empty and configure stable `detection_direction`, `min_dev_from_expected`, and `min_rel_dev_from_expected` policies on [`reader.queries.<alias>`](https://docs.victoriametrics.com/anomaly-detection/components/reader/#per-query-parameters) instead.
|
||||
- `optimized_business_params` (list[string]) - {{% available_from "v1.15.0" anomaly %}} this argument allows particular [business-specific parameters](#common-args) such as [`detection_direction`](https://docs.victoriametrics.com/anomaly-detection/components/models/#detection-direction) or [`min_dev_from_expected`](https://docs.victoriametrics.com/anomaly-detection/components/models/#minimal-deviation-from-expected) to remain **unchanged during optimizations, retaining their initial values**. I.e. setting `optimized_business_params` to `['detection_direction']` will allow to optimize only `detection_direction` business-specific arg, while `min_dev_from_expected` will retain its default value of (e.g. [1, 2] if set to that value in model config). By default and if not set, will be equal to `[]` (empty list), meaning no business params will be optimized. **A recommended option is to leave it empty** as this feature is still experimental and may lead to unexpected results.
|
||||
- `seed` (int) - Random seed for reproducibility and deterministic nature of underlying optimizations.
|
||||
- `validation_scheme` (string) - {{% available_from "v1.25.1" anomaly %}} the validation scheme to use for hyperparameter tuning, either `regular` (time-based default) or `leaky` (regular cross-validation with `n_splits` folds, where each fold is a time-based split of the data). The `leaky` scheme is recommended for `anomaly_percentage` ~ 0%, as it allows the model to "see" all the datapoints at least once during the optimization process, which can lead to better results in such cases. Defaults to `regular`.
|
||||
- `n_splits` (int) - How many folds to create for hyperparameter tuning out of your data. The higher, the longer it takes but the better the results can be. Defaults to 3.
|
||||
@@ -804,7 +809,7 @@ For simple profiles without strong trend or seasonality, prefer [Online MAD](#on
|
||||
|
||||
Preset suffixes describe expected profile shape: `smooth` represents gradual recurring curves, `spiky` represents narrow phase peaks, and `plateau` represents sustained calendar levels. Choose only profiles supported by the data. Calendar and holiday features use civil time from the configured query timezone, so hour/day profiles remain aligned across daylight-saving-time transitions.
|
||||
|
||||
Temporal Envelope also supports the [common model arguments](#common-args), including `queries`, `schedulers`, `provide_series`, `scale`, and `clip_predictions`. Configure `data_range`, `detection_direction`, `min_dev_from_expected`, `min_rel_dev_from_expected`, and query timezone under the corresponding [reader query](https://docs.victoriametrics.com/anomaly-detection/components/reader/#per-query-parameters). The multivariate variant applies these business policies independently to each input channel {{% available_from "v1.30.2" anomaly %}}, so one model can represent combinations such as temperature above expected, power above expected, and clock below expected.
|
||||
Temporal Envelope also supports the [common model arguments](#common-args), including `queries`, `schedulers`, `provide_series`, `detection_direction`, `scale`, `clip_predictions`, `min_dev_from_expected`, and `min_rel_dev_from_expected`. Input `data_range` and query timezone are configured on the [reader](https://docs.victoriametrics.com/anomaly-detection/components/reader/#config-parameters).
|
||||
|
||||
The multivariate variant uses `class: temporal_envelope_multivariate` or `model.online.TemporalEnvelopeMultivariateModel` and adds:
|
||||
|
||||
@@ -895,13 +900,10 @@ models:
|
||||
# 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,
|
||||
# ]
|
||||
# Configure detection_direction and minimum-deviation policies under reader.queries.<alias> (query-level from v1.30.2).
|
||||
# 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
|
||||
@@ -965,13 +967,10 @@ models:
|
||||
# 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,
|
||||
# ]
|
||||
# Configure detection_direction and minimum-deviation policies under reader.queries.<alias> (query-level from v1.30.2).
|
||||
# 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
|
||||
@@ -1015,13 +1014,10 @@ models:
|
||||
# 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,
|
||||
# ]
|
||||
# Configure detection_direction and minimum-deviation policies under reader.queries.<alias> (query-level from v1.30.2).
|
||||
# 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
|
||||
@@ -1064,13 +1060,10 @@ models:
|
||||
# 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,
|
||||
# ]
|
||||
# Configure detection_direction and minimum-deviation policies under reader.queries.<alias> (query-level from v1.30.2).
|
||||
# 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
|
||||
@@ -1106,7 +1099,6 @@ Resulting metrics of the model are described [here](#vmanomaly-output).
|
||||
- `tz_use_cyclical_encoding`{{% available_from "v1.18.0" anomaly %}} (bool): If set to `True`, applies [cyclical encoding technique](https://www.kaggle.com/code/avanwyk/encoding-cyclical-features-for-deep-learning) to timezone-aware seasonalities. Should be used with `tz_aware=True` and `tz_seasonalities`.
|
||||
- `forecast_at`{{% available_from "v1.25.3" anomaly %}} (list[str]): Specifies future relative offsets for which forecasts should be generated (e.g., `['1h', '1d']`). Works similarly to [predict_linear](https://docs.victoriametrics.com/victoriametrics/metricsql/#predict_linear) in MetricQL, but with more flexibility and seasonality support - produced series will have *the same timestamp* as the other [output](#vmanomaly-output) series, but with the forecasted value for the *future timestamp*. Defaults to `[]` (empty list, meaning no future forecasts are produced). If set, `provide_series` must include at least `yhat` for point-wise forecasts (and `yhat_lower` or/and `yhat_upper` for respective confidence intervals). For example, if `forecast_at` is set to `['1h', '1d']`, the model will produce forecasts for both the next hour and the next day, and these series can be accessed by `yhat_1h`, `yhat_lower_1h`, `yhat_upper_1h`, `yhat_1d`, `yhat_lower_1d`, and `yhat_upper_1d` in the output, respectively. See [FAQ](https://docs.victoriametrics.com/anomaly-detection/faq/#forecasting) for more details.
|
||||
|
||||
> [!WARNING]
|
||||
> `forecast_at` parameter can lead to **significant increase in active timeseries** if you have a lot of time series returned by your queries, as it will produce additional series for each of the future timestamps specified in `forecast_at` (optionally multiplied by 1-3 if interval forecasts are included). For example, if you have 1000 time series returned by your query and set `forecast_at` to `[1h, 1d, 1w]`, and `provide_series` includes `yhat_lower` and `yhat_upper`, it will produce 1000 (series) * 3 (intervals) * 3 (predictions, point + interval) = 9000 additional timeseries. Consider using it only on small subset of metrics (e.g. grouped by `host` or `region`) to avoid this issue, as it also **proportionally (to the number of `forecast_at` elements) increases the timings of inference calls**.
|
||||
|
||||
- `compression` {{% available_from "v1.28.1" anomaly %}} (dict, optional): Configuration for downsampling input data before fitting the model. Useful for high-frequency data to reduce CPU and RAM/disk load and improve model performance. The `compression` block supports the following parameters:
|
||||
@@ -1129,13 +1121,10 @@ models:
|
||||
# See https://docs.victoriametrics.com/anomaly-detection/components/models/#common-args
|
||||
#
|
||||
# provide_series: ['anomaly_score', 'yhat', 'yhat_lower', 'yhat_upper', 'trend']
|
||||
# schedulers: [
|
||||
# all scheduler aliases defined in `scheduler` section,
|
||||
# ]
|
||||
# queries: [
|
||||
# all query aliases defined in `reader.queries` section,
|
||||
# ]
|
||||
# Configure detection_direction and minimum-deviation policies under reader.queries.<alias> (query-level from v1.30.2).
|
||||
# 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
|
||||
@@ -1166,13 +1155,10 @@ models:
|
||||
# See https://docs.victoriametrics.com/anomaly-detection/components/models/#common-args
|
||||
#
|
||||
# provide_series: ['anomaly_score', 'yhat', 'yhat_lower', 'yhat_upper', 'trend']
|
||||
# schedulers: [
|
||||
# all scheduler aliases defined in `scheduler` section,
|
||||
# ]
|
||||
# queries: [
|
||||
# all query aliases defined in `reader.queries` section,
|
||||
# ]
|
||||
# Configure detection_direction and minimum-deviation policies under reader.queries.<alias> (query-level from v1.30.2).
|
||||
# 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
|
||||
@@ -1268,12 +1254,8 @@ models:
|
||||
# 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,
|
||||
# ]
|
||||
# schedulers: [all scheduler aliases defined in `scheduler` section]
|
||||
# queries: [all query aliases defined in `reader.queries` section]
|
||||
# anomaly_score_outside_data_range: 1.01 # auto anomaly score (1.01) if `y` (real value) is outside of data_range, if set
|
||||
```
|
||||
|
||||
@@ -1334,13 +1316,10 @@ models:
|
||||
# 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,
|
||||
# ]
|
||||
# Configure detection_direction and minimum-deviation policies under reader.queries.<alias> (query-level from v1.30.2).
|
||||
# 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
|
||||
@@ -1380,13 +1359,10 @@ models:
|
||||
# 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,
|
||||
# ]
|
||||
# Configure detection_direction and minimum-deviation policies under reader.queries.<alias> (query-level from v1.30.2).
|
||||
# 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
|
||||
@@ -1457,7 +1433,7 @@ Create `custom_model.py` with a `CustomModel` class derived from `Model`. A conc
|
||||
- `serialize`, which returns `bytes` suitable for on-disk storage;
|
||||
- `deserialize`, which restores the same model from bytes or a file path.
|
||||
|
||||
Model-specific configuration is passed through the `args` mapping. The example below learns a stationary normal interval. It emits the standard forecast columns and uses the base-class anomaly-score calculation, so query policies such as `detection_direction`, `data_range`, and minimum deviations, together with model settings such as `scale`, continue to work.
|
||||
Model-specific configuration is passed through the `args` mapping. The example below learns a stationary normal interval. It emits the standard forecast columns and uses the base-class anomaly-score calculation, so common settings such as `detection_direction`, `data_range`, `scale`, and minimum deviations continue to work.
|
||||
|
||||
```python
|
||||
from pickle import dumps
|
||||
@@ -1585,7 +1561,7 @@ See the [component configuration reference](https://docs.victoriametrics.com/ano
|
||||
Pull the `vmanomaly` image:
|
||||
|
||||
```sh
|
||||
docker pull victoriametrics/vmanomaly:v1.30.2
|
||||
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.
|
||||
@@ -1595,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.2 \
|
||||
victoriametrics/vmanomaly:v1.30.1 \
|
||||
/config.yaml \
|
||||
--licenseFile=/license \
|
||||
--dryRun
|
||||
@@ -1691,13 +1667,10 @@ models:
|
||||
# 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,
|
||||
# ]
|
||||
# Configure detection_direction and minimum-deviation policies under reader.queries.<alias> (query-level from v1.30.2).
|
||||
# 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
|
||||
@@ -1736,13 +1709,10 @@ models:
|
||||
# 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,
|
||||
# ]
|
||||
# Configure detection_direction and minimum-deviation policies under reader.queries.<alias> (query-level from v1.30.2).
|
||||
# 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
|
||||
|
||||
@@ -333,14 +333,6 @@ For detailed guidance on configuring mTLS parameters such as `verify_tls`, `tls_
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`vmanomaly_native_threads_per_worker`</span>
|
||||
</td>
|
||||
<td>Gauge</td>
|
||||
<td>Effective maximum native numerical-library threads per model worker{{% available_from "v1.30.2" anomaly %}} after resolving [`settings.native_threads_per_worker`](https://docs.victoriametrics.com/anomaly-detection/components/settings/#parallelization) against the effective worker count and container-aware CPU capacity.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`vmanomaly_config_entities`</span>
|
||||
</td>
|
||||
<td>Gauge</td>
|
||||
|
||||
@@ -59,7 +59,7 @@ reader:
|
||||
step: '10s' # individual step for this query, will be filled with `sampling_period` from the root level
|
||||
data_range: ['-inf', 'inf'] # by default, no constraints applied on data range
|
||||
tz: 'UTC' # by default, tz-free data is used throughout the model lifecycle
|
||||
# from v1.30.2, explicitly add detection_direction and minimum-deviation policies here when needed
|
||||
# new query-level arguments will be added in backward-compatible way in future releases
|
||||
```
|
||||
{{% /collapse %}}
|
||||
|
||||
@@ -85,16 +85,6 @@ There is change {{% available_from "v1.13.0" anomaly %}} of [`queries`](https://
|
||||
|
||||
> If not set explicitly (or if older config style prior to [v1.13.0](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1130)) is used, then it is set to reader-level `data_range` arg{{% available_from "v1.18.1" anomaly %}}
|
||||
|
||||
> Configuring `data_range` in a model is {{% deprecated_from "v1.30.2" anomaly %}}. Configure it under `reader.queries.<alias>` so the KPI domain remains the same when the query is attached to different models. Existing model-level values remain compatible as model-local fallbacks when the query does not define an explicit value.
|
||||
|
||||
- `detection_direction`{{% available_from "v1.30.2" anomaly %}} (`both`, `above_expected`, or `below_expected`): controls whether deviations on both sides, only above the expected value, or only below it can produce anomaly scores. The default is `both`. See [detection direction](https://docs.victoriametrics.com/anomaly-detection/components/models/#detection-direction) for behavior details.
|
||||
|
||||
- `min_dev_from_expected`{{% available_from "v1.30.2" anomaly %}} (float or one/two-element list[float]): ignores deviations smaller than the configured absolute threshold. A scalar or one-element list applies to both directions; a two-element list configures lower and upper deviations separately. See [minimal deviation from expected](https://docs.victoriametrics.com/anomaly-detection/components/models/#minimal-deviation-from-expected).
|
||||
|
||||
- `min_rel_dev_from_expected`{{% available_from "v1.30.2" anomaly %}} (float or one/two-element list[float]): ignores deviations smaller than the configured percentage of the absolute expected value. A scalar or one-element list applies to both directions; a two-element list configures lower and upper percentages separately. See [minimal relative deviation from expected](https://docs.victoriametrics.com/anomaly-detection/components/models/#minimal-relative-deviation-from-expected).
|
||||
|
||||
> Configuring `detection_direction`, `min_dev_from_expected`, or `min_rel_dev_from_expected` in a model is {{% deprecated_from "v1.30.2" anomaly %}}. Query-level values are authoritative. Existing model-level values remain compatible only as model-local fallbacks for attached queries that do not define the corresponding policy.
|
||||
|
||||
- `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., [`TemporalEnvelopeModel`](https://docs.victoriametrics.com/anomaly-detection/components/models/#temporal-envelope) or [`OnlineQuantileModel`](https://docs.victoriametrics.com/anomaly-detection/components/models/#online-seasonal-quantile)).
|
||||
@@ -125,10 +115,7 @@ reader:
|
||||
ingestion_rate_t1:
|
||||
expr: 'sum(rate(vm_rows_inserted_total[5m])) by (type) > 0'
|
||||
step: '2m' # overrides global `sampling_period` of 1m
|
||||
data_range: [10, 'inf'] # query-level business policy from v1.30.2; y < 10 triggers anomaly score > 1
|
||||
detection_direction: 'above_expected' # query-level from v1.30.2; only spikes can be anomalous
|
||||
min_dev_from_expected: [0, 5] # query-level from v1.30.2; ignore upward deviations smaller than 5
|
||||
min_rel_dev_from_expected: [0, 15] # query-level from v1.30.2; ignore upward deviations below 15%
|
||||
data_range: [10, 'inf'] # meaning only positive values > 10 are expected, i.e. a value `y` < 10 will trigger anomaly score > 1
|
||||
max_points_per_query: 5000 # overrides reader-level value of 10000 for `ingestion_rate` query
|
||||
tz: 'America/New_York' # to override reader-wise `tz`
|
||||
tenant_id: '1:0' # overriding tenant_id to isolate data
|
||||
@@ -315,19 +302,6 @@ Optional timeout {{% available_from "v1.30.0" anomaly %}} for post-fetch process
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`workers`</span>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
`0`
|
||||
</td>
|
||||
<td>
|
||||
Maximum concurrent datasource fetch threads {{% available_from "v1.30.2" anomaly %}}. `0` selects a bounded value automatically from the number of queries and available CPUs. A positive value sets an explicit cap for queries and disk-streamed split-query chunks.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`verify_tls`</span>
|
||||
</td>
|
||||
<td>
|
||||
@@ -536,7 +510,6 @@ reader:
|
||||
timeout: '30s' # backward-compatible default for both phases
|
||||
fetch_timeout: '30s' # timeout for each datasource request, overrides `timeout` if set
|
||||
processing_timeout: '1m' # timeout for preparing fetched series for fit/infer, overrides `timeout` if set
|
||||
workers: 0 # automatic bounded datasource concurrency; set a positive value for an explicit cap
|
||||
query_from_last_seen_timestamp: True # false by default
|
||||
latency_offset: '1ms'
|
||||
series_processing_batch_size: 8
|
||||
@@ -923,19 +896,6 @@ Optional timeout {{% available_from "v1.30.0" anomaly %}} for post-fetch process
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`workers`</span>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
`0`
|
||||
</td>
|
||||
<td>
|
||||
Maximum concurrent datasource fetch threads {{% available_from "v1.30.2" anomaly %}}. `0` selects a bounded value automatically from the number of queries and available CPUs. A positive value sets an explicit cap for queries and disk-streamed split-query chunks.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<span style="white-space: nowrap;">`verify_tls`</span>
|
||||
</td>
|
||||
<td>
|
||||
@@ -1077,15 +1037,12 @@ reader:
|
||||
timeout: '30s' # backward-compatible default for both phases
|
||||
fetch_timeout: '30s' # timeout for each datasource request, overrides `timeout` if set
|
||||
processing_timeout: '1m' # timeout for preparing fetched series for fit/infer, overrides `timeout` if set
|
||||
workers: 0 # automatic bounded datasource concurrency; set a positive value for an explicit cap
|
||||
queries:
|
||||
# one query returning 1 result fields (avg_duration), it will have __name__ label (series name) as `duration_30m__avg`
|
||||
duration_avg_30m:
|
||||
expr: "* | stats avg(duration) as avg" # initial LogsQL expression
|
||||
step: '2m' # overrides global `sampling_period` of 1m
|
||||
data_range: [0, 'inf'] # query-level business policy from v1.30.2; y < 0 triggers anomaly score > 1
|
||||
detection_direction: 'above_expected' # query-level from v1.30.2
|
||||
min_rel_dev_from_expected: [0, 20] # query-level from v1.30.2; ignore upward deviations below 20%
|
||||
data_range: [0, 'inf'] # meaning only positive values > 0 are expected, i.e. a value `y` < 0 will trigger anomaly score > 1
|
||||
tz: 'America/New_York' # to override reader-wise `tz`
|
||||
# tenant_id: '1:0' # overriding tenant_id to isolate data
|
||||
# offset: '-15s' # to override reader-wise `offset` and query data 15 seconds earlier to account for data collection delays
|
||||
|
||||
@@ -70,7 +70,6 @@ options={`"scheduler.periodic.PeriodicScheduler"`, `"scheduler.oneoff.OneoffSche
|
||||
|
||||
## Periodic scheduler
|
||||
|
||||
> [!WARNING]
|
||||
> If `start_from` [parameter](#parameters-1) is used, it's suggested to also set `restore_state: true` in the [Settings section](https://docs.victoriametrics.com/anomaly-detection/components/settings/#state-restoration) of a config, so that the scheduler can restore its state from the previous run **if terminated or restarted in between scheduled runs** and continue producing anomaly scores without interruptions, otherwise the service will be idle until future `start_from` time is reached. E.g. if `start_from` is set to `20:00` and the service is started and then terminated and restarted at `20:30`, it will not produce any anomaly scores until the next day's `20:00` is reached (+23:30 of being idle), which introduces inconvenience for the users.
|
||||
|
||||
> {{% 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.
|
||||
@@ -197,7 +196,6 @@ This configuration specifies that `vmanomaly` will calculate a 14-day time windo
|
||||
|
||||
## Oneoff scheduler
|
||||
|
||||
> [!WARNING]
|
||||
> As of latest version, the Oneoff scheduler can't be explicitly used with a combination of [stateful service](https://docs.victoriametrics.com/anomaly-detection/components/settings/#state-restoration). It is designed to run once and exit, so it does not maintain state across runs. A warning will be raised in logs and internal state for such scheduler will not be saved and restored upon restart. If you need to run the scheduler periodically and/or maintain state, consider using the [Periodic scheduler](#periodic-scheduler) instead.
|
||||
|
||||
### Parameters
|
||||
@@ -369,7 +367,6 @@ schedulers:
|
||||
|
||||
> {{% available_from "v1.26.0" anomaly %}} `BacktestingScheduler` in [inference-only](https://docs.victoriametrics.com/anomaly-detection/components/scheduler/#inference-only-mode) mode is used in UI for backtesting configurations on historical data to verify that it works as expected before it goes live. See [vmanomaly UI](https://docs.victoriametrics.com/anomaly-detection/ui/) on how to access and use the UI.
|
||||
|
||||
> [!WARNING]
|
||||
> As of latest version, the Backtesting scheduler can't be explicitly used with a combination of [state restoration](https://docs.victoriametrics.com/anomaly-detection/components/settings/#state-restoration). It is designed to run once and exit, so it does not maintain state across runs. A warning will be raised in logs and internal state for such scheduler will not be saved and restored upon restart. If you need to run the scheduler periodically and/or maintain state, consider using the [Periodic scheduler](#periodic-scheduler) instead.
|
||||
|
||||
> A new, more intuitive backtesting mode is available {{% available_from "v1.22.1" anomaly %}}. In **Inference only** mode, the window you specify via `[from, to]` (or `[from_iso, to_iso]`) is used *solely for inference*, and the corresponding training (“fit”) windows are determined automatically. To enable this behavior, set:
|
||||
|
||||
|
Before Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 63 KiB |
@@ -16,7 +16,7 @@ aliases:
|
||||
Through the **Settings** section of a config, you can configure the following parameters of the anomaly detection service:
|
||||
|
||||
- [Anomaly score outside data range](#anomaly-score-outside-data-range) - specific anomaly score fo values outside the expected data range of particular query
|
||||
- [Parallelization](#parallelization) - process workers and native numerical-library threads used by each worker
|
||||
- [Parallelization](#parallelization) - number of workers to run workloads in parallel
|
||||
- [State restoration](#state-restoration) - whether to restore models' state in between runs if the service is restarted or stopped
|
||||
|
||||
## Anomaly Score Outside Data Range
|
||||
@@ -36,7 +36,7 @@ settings:
|
||||
schedulers:
|
||||
periodic:
|
||||
class: periodic
|
||||
fit_every: 1000d # bootstrap-only schedule; use a finite cadence if accumulated state must be reset
|
||||
fit_every: 5m
|
||||
fit_window: 3h
|
||||
infer_every: 30s
|
||||
# other schedulers
|
||||
@@ -45,14 +45,12 @@ models:
|
||||
zscore_online_inherited:
|
||||
class: zscore_online
|
||||
z_threshold: 3.5
|
||||
decay: 0.99 # give more weight to recent data while using the bootstrap-only fit schedule
|
||||
clip_predictions: True
|
||||
# will be inherited from settings.anomaly_score_outside_data_range
|
||||
# anomaly_score_outside_data_range: 5.0
|
||||
zscore_online_override:
|
||||
class: zscore_online
|
||||
z_threshold: 3.5
|
||||
decay: 0.99 # give more weight to recent data while using the bootstrap-only fit schedule
|
||||
clip_predictions: True
|
||||
anomaly_score_outside_data_range: 1.5 # will override settings.anomaly_score_outside_data_range
|
||||
# other models
|
||||
@@ -88,29 +86,24 @@ monitoring:
|
||||
# other monitoring settings
|
||||
```
|
||||
|
||||
The examples on this page use `fit_every: 1000d` as an effectively bootstrap-only schedule. This is appropriate when an online model has a suitable forgetting or reactivity mechanism, such as `zscore_online` with `decay < 1`. If outdated history must be discarded explicitly, choose a finite fit cadence instead; each fit resets the online model state from the configured `fit_window`.
|
||||
|
||||
## Parallelization
|
||||
|
||||
The `n_workers` argument allows you to explicitly specify the number of process workers for internal parallelization of the service. This can help improve performance on multicore systems by allowing the service to process multiple tasks in parallel. For backward compatibility, it is set to `1` by default. It should be an integer greater than or equal to `-1`; values `-1` and `0` use the number of CPU cores available to the service, including container CPU limits.
|
||||
The `n_workers` argument allows you to explicitly specify the number of workers for internal parallelization of the service. This can help improve performance on multicore systems by allowing the service to process multiple tasks in parallel. For backward compatibility, it's set to `1` by default, meaning that the service will run in a single-threaded mode. It should be an integer greater than or equal to `-1`, where `-1` and `0` means that the service will automatically inherit the number of workers based on the number of available CPU cores.
|
||||
|
||||
The `native_threads_per_worker` argument {{% available_from "v1.30.2" anomaly %}} limits [native numerical-library threads](https://scikit-learn.org/stable/computing/parallelism.html#oversubscription-spawning-too-many-threads), such as OpenBLAS threads, inside each model worker. Its default `0` divides the CPU capacity available to the service across effective workers automatically. A positive integer requests an explicit per-worker limit, capped by the CPU share available to that worker. This avoids oversubscription and CPU throttling when every process would otherwise start its own multi-threaded numerical workload. Both `n_workers` and `native_threads_per_worker` are startup settings and require a service restart to change.
|
||||
|
||||
- **Increasing** the number can be particularly useful when dealing with a high volume of queries returning many (long) timeseries.
|
||||
- **Decreasing** the number can be useful when running the service on a system with limited resources or when you want to reduce the load on the system.
|
||||
Increasing the number can be particularly useful when dealing with a high volume of queries returning many (long) timeseries.
|
||||
Decreasing the number can be useful when running the service on a system with limited resources or when you want to reduce the load on the system.
|
||||
|
||||
Here's an example configuration that uses 4 workers for service's internal parallelization:
|
||||
|
||||
```yaml
|
||||
settings:
|
||||
n_workers: 4
|
||||
native_threads_per_worker: 0 # automatically divide available CPU capacity across workers
|
||||
restore_state: False # do not restore state from previous run
|
||||
|
||||
schedulers:
|
||||
periodic:
|
||||
class: periodic
|
||||
fit_every: 1000d # bootstrap-only schedule; use a finite cadence if accumulated state must be reset
|
||||
fit_every: 5m
|
||||
fit_window: 3h
|
||||
infer_every: 30s
|
||||
# other schedulers
|
||||
@@ -119,7 +112,6 @@ models:
|
||||
zscore_online_override:
|
||||
class: zscore_online
|
||||
z_threshold: 3.5
|
||||
decay: 0.99 # give more weight to recent data while using the bootstrap-only fit schedule
|
||||
clip_predictions: True
|
||||
# other models
|
||||
|
||||
@@ -157,11 +149,10 @@ monitoring:
|
||||
|
||||
> This feature is best used with config [hot-reloading](https://docs.victoriametrics.com/anomaly-detection/components/#hot-reload) {{% available_from "v1.25.0" anomaly %}} for increased deployment flexibility.
|
||||
|
||||
The `restore_state` argument {{% available_from "v1.24.0" anomaly %}} makes `vmanomaly` service **stateful** by persisting and restoring service metadata and fitted model state between runs, allowing seamless continuation after service restarts.
|
||||
The `restore_state` argument {{% available_from "v1.24.0" anomaly %}} makes `vmanomaly` service **stateful** by persisting and restoring state between runs. If enabled, the service will save the state of anomaly detection models and their training data to local filesystem, allowing for seamless continuation of operations after service restarts.
|
||||
|
||||
By default, `restore_state` is set to `false`, meaning the service will start fresh on each restart, to maintain backward compatibility.
|
||||
|
||||
> [!WARNING]
|
||||
> This feature requires enabling [on-disk mode](https://docs.victoriametrics.com/anomaly-detection/faq/#on-disk-mode) for the models and data. If not enabled, the service will exit with an error when `restore_state` is set to `true`.
|
||||
|
||||
### Benefits
|
||||
@@ -173,17 +164,15 @@ This feature improves the experience of using the anomaly detection service in s
|
||||
|
||||
### How it works
|
||||
|
||||
**Storage**: The service dumps its state into a database file located at `$VMANOMALY_MODEL_DUMPS_DIR/vmanomaly.db`. This database contains metadata about model configurations and schedulers, together with references to trained model artifacts. Scheduler-managed Parquet data is temporary fit input rather than durable model state.
|
||||
**Storage**: The service dumps its state into a database file located at `$VMANOMALY_MODEL_DUMPS_DIR/vmanomaly.db`. This database contains metadata about model configurations, schedulers and references to the trained model instances and their respective data.
|
||||
|
||||
**State restoration**: When the service starts with `restore_state` set to `true`, it will:
|
||||
1. Check for the existence of the database file in the specified directory.
|
||||
2. If the file does not exist, it will create a new database file and initialize the state with the current configuration, training models as needed. If the file exists, then it compares the loaded state with the current configuration to determine what can be reused and what needs to be retrained (for example, a changed model class, hyperparameter, scheduler, or reader query invalidates the affected state). Compatible model configurations and trained model instances are restored.
|
||||
3. Subsequently, it checks model "staleness" and retrains models if necessary, based on the current configuration and the last training time stored in the database versus the next scheduled training time. If the model is **actual**, it continues to use the previously trained model instance. If the model is **stale** (for example, `fit_every` has passed since the last training), it reads the latest `fit_window` from VictoriaMetrics and retrains the model.
|
||||
2. If the file does not exist, it will create a new database file and initialize the state with the current configuration, training models as needed. If the file exists, then it compares the loaded state with the current configuration to ensure compatibility - what can be reused and what needs to be retrained (e.g., if the model class or hyperparameters have changed, it will not restore the state for that model, same for schedulers or reader queries). For reusable components, previously saved state, including model configurations, trained model instances, and their training data, will be restored.
|
||||
3. Subsequently, it will check for model "staleness" and retrain models if necessary, based on the current configuration and the last training time stored in the database vs next scheduled training time. If the model is **actual**, it will continue to use the previously trained model instances or its training data. If the model is **stale** (e.g. `fit_every` time has passed since the last training), it will retrain the model using the latest data of `fit_window` length from VictoriaMetrics TSDB.
|
||||
|
||||
**State update**: The service periodically saves the updated state after each "atomic" operations, such as (model_alias, query_alias)-based training or inference. This ensures that the state is always up-to-date and can be restored in case of a service restart. [Online models](https://docs.victoriametrics.com/anomaly-detection/components/models/#online-models) are also updated after each inference, while [offline models](https://docs.victoriametrics.com/anomaly-detection/components/models/#offline-models) are only saved after each training operation as they do not change the state during consecutive fit calls.
|
||||
|
||||
**Fit-data cleanup**: {{% available_from "v1.30.2" anomaly %}} Each scheduler-managed Parquet generation is removed after all dependent univariate or multivariate models finish fitting and commit their state. Failed or overlapping fits retain their own generation until it is safe to clean up. This keeps the initial bootstrap window available while it is in use without retaining it for the full `fit_every` interval.
|
||||
|
||||
**Cleanup behavior**: When `restore_state` is switched from `true` to `false`, the database file is automatically removed on the next service startup to prevent inconsistent behavior. All the artifacts (such as model dumps and data dumps) will be removed as well, so the service will start fresh without any previous state.
|
||||
|
||||
Here's an example configuration that enables state restoration:
|
||||
@@ -196,7 +185,7 @@ settings:
|
||||
schedulers:
|
||||
periodic:
|
||||
class: periodic
|
||||
fit_every: 1000d # bootstrap-only schedule; use a finite cadence if accumulated state must be reset
|
||||
fit_every: 5m
|
||||
fit_window: 3h
|
||||
infer_every: 30s
|
||||
# other schedulers
|
||||
@@ -205,7 +194,6 @@ models:
|
||||
zscore_online:
|
||||
class: zscore_online
|
||||
z_threshold: 3.5
|
||||
decay: 0.99 # give more weight to recent data while using the bootstrap-only fit schedule
|
||||
clip_predictions: True
|
||||
# other models
|
||||
|
||||
@@ -254,19 +242,16 @@ settings:
|
||||
schedulers:
|
||||
periodic_1d:
|
||||
class: periodic
|
||||
fit_every: 1000d # bootstrap-only schedule; use a finite cadence if accumulated state must be reset
|
||||
fit_every: 1h
|
||||
infer_every: 30s
|
||||
fit_window: 24h
|
||||
models:
|
||||
zscore_online:
|
||||
class: zscore_online
|
||||
z_threshold: 3.5
|
||||
decay: 0.99 # give more weight to recent data while using the bootstrap-only fit schedule
|
||||
schedulers: ['periodic_1d']
|
||||
temporal_envelope:
|
||||
class: temporal_envelope
|
||||
alpha: 0.005 # adapt the trend while using the bootstrap-only fit schedule
|
||||
loss_reactivity: 5 # allow new deviations to update the envelope
|
||||
schedulers: ['periodic_1d']
|
||||
queries: ['q1', 'q2']
|
||||
seasonalities: ['hod_smooth', 'dow_smooth']
|
||||
@@ -283,7 +268,7 @@ reader:
|
||||
# other components like writer, monitoring, etc.
|
||||
```
|
||||
|
||||
if the service is restarted before the next scheduled fit, it will restore the state of the `zscore_online` and `temporal_envelope` models if their signature (class, hyperparameters, schedulers, etc.) has not changed. It loads trained model instances from disk and continues 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:
|
||||
@@ -292,19 +277,16 @@ settings:
|
||||
schedulers:
|
||||
periodic_1d: # can be fully reused, no changes
|
||||
class: periodic
|
||||
fit_every: 1000d # unchanged bootstrap-only schedule
|
||||
fit_every: 1h # unchanged, still fits every hour
|
||||
infer_every: 30s # unchanged, still infers every 30 seconds
|
||||
fit_window: 24h # unchanged, still fits on the last 24 hours of data
|
||||
models:
|
||||
zscore_online: # can't be reused, because its `z_threshold` has changed
|
||||
class: zscore_online # unchanged, still the same model class
|
||||
z_threshold: 3.0 # changed, needs retraining!
|
||||
decay: 0.99 # unchanged forgetting factor
|
||||
schedulers: ['periodic_1d'] # unchanged, still attached to the same scheduler
|
||||
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
|
||||
alpha: 0.005 # unchanged trend reactivity
|
||||
loss_reactivity: 5 # unchanged envelope reactivity
|
||||
schedulers: ['periodic_1d'] # unchanged, still attached to the same scheduler
|
||||
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
|
||||
@@ -332,31 +314,31 @@ This means that the service upon restart:
|
||||
|
||||
## Retention
|
||||
|
||||
{{% available_from "v1.28.1" anomaly %}} The `retention` argument sets a [time to live](https://en.wikipedia.org/wiki/Time_to_live) (TTL) for stored model instances. At each `check_interval`, the service removes instances that have not been used for inference or refitting within `ttl`. This bounds stale resource usage in long-running deployments. Temporary scheduler-managed fit data follows the [fit-data cleanup lifecycle](#how-it-works) independently.
|
||||
{{% available_from "v1.28.1" anomaly %}} The `retention` argument sets a [time to live](https://en.wikipedia.org/wiki/Time_to_live) (TTL) for service artifacts such as stored model instances and training data. At each `check_interval`, the service removes artifacts that have not been used for inference or refitting within `ttl`. This bounds stale resource usage in long-running deployments.
|
||||
|
||||
### Use Cases
|
||||
- With **[online models](https://docs.victoriametrics.com/anomaly-detection/components/models/#online-models)** as they continuously create model instances for new timeseries over time during inference calls, especially when combined with [periodic schedulers](https://docs.victoriametrics.com/anomaly-detection/components/scheduler/#periodic-scheduler) with infrequent `fit_every` (say, `90d`).
|
||||
- In deployments where **the set of monitored timeseries changes frequently**, leading to accumulation of unused model instances due to high churn rate or relabeling of metrics.
|
||||
- When using **[state restoration](https://docs.victoriametrics.com/anomaly-detection/components/settings/#state-restoration)**, which improves fault tolerance but can retain inactive model instances unless retention is configured.
|
||||
- In deployments where **the set of monitored timeseries changes frequently**, leading to accumulation of unused model instances and training data over time, due to high churn rate or relabeling of metrics.
|
||||
- When using **[state restoration](https://docs.victoriametrics.com/anomaly-detection/components/settings/#state-restoration) feature** which improves fault tolerance, but may retain all model instances and their training data for considerable time, potentially leading to high disk or RAM usage.
|
||||
|
||||
### Configuration
|
||||
|
||||
The section is **backward-compatible and disabled by default**, meaning that model instances are retained unless:
|
||||
The section is **backward-compatible and disabled by default**, meaning that all model instances and their training data are retained unless:
|
||||
- The service is restarted with `restore_state` set to `false`, which triggers a cleanup of all stored artifacts.
|
||||
- The models are marked as outdated once scheduled re-fitting is due, leading to retraining and replacement of previous artifacts.
|
||||
|
||||
`ttl` defines the time-to-live period for model instances. It should be a valid period string (e.g., `7d` for 7 days or `30d` for 30 days). If a model instance has not been used for inference or refitting within this period, it is considered stale and eligible for cleanup.
|
||||
`ttl` argument defines the time-to-live period for model instances and their training data. It should be a valid period string (e.g., `7d` for 7 days, `30d` for 30 days, etc.). If a model instance or its training data has not been used for inference or refitting within this period, it will be considered stale and eligible for cleanup.
|
||||
|
||||
> If `ttl` is greater than a scheduler's `fit_every`, the model is refitted before it becomes stale and the TTL has no effect.
|
||||
|
||||
`check_interval` defines how often the service should check for stale artifacts. It should be a valid period string (e.g., `1h` for 1 hour or `24h` for 24 hours). During each check, the service evaluates stored model instances against the defined `ttl` and removes those that are stale.
|
||||
`check_interval` argument defines how often the service should check for stale artifacts. It should be a valid period string (e.g., `1h` for 1 hour, `24h` for 24 hours, etc.). During each check, the service will evaluate all stored model instances and their training data against the defined `ttl` and remove those that are stale.
|
||||
|
||||
> Check interval should be set to a value smaller than `ttl` and smaller than the smallest `fit_every` period among all schedulers used in the config to ensure timely cleanup of stale artifacts, otherwise stale artifacts may persist longer than intended.
|
||||
|
||||
### Example
|
||||
|
||||
Here's an example configuration that enables retention with a TTL of 1 day and a check interval of 30 minutes, where inference is performed every 15 minutes.
|
||||
- Model instances that have not been used for inference or refitting within the last day will be cleaned up every 30 minutes (m2 example on a diagram)
|
||||
- Model instances and their training data that have not been used for inference or refitting within the last day will be cleaned up every 30 minutes (m2 example on a diagram)
|
||||
- While model instances used for inference within the last day at least 1 time will be retained (m1 example on a diagram)
|
||||
|
||||

|
||||
@@ -400,7 +382,7 @@ settings:
|
||||
# other settings
|
||||
restore_state: True # enables state restoration
|
||||
retention:
|
||||
ttl: 24h # time-to-live for inactive model instances
|
||||
ttl: 24h # time-to-live for model instances and their training data
|
||||
check_interval: 30m # interval to check for stale artifacts
|
||||
```
|
||||
|
||||
|
||||
@@ -124,12 +124,12 @@ Detailed parameters in each section:
|
||||
|
||||
* `schedulers` ([PeriodicScheduler](https://docs.victoriametrics.com/anomaly-detection/components/scheduler/#periodic-scheduler) is used here)
|
||||
* `infer_every` - Specifies the frequency at which the trained models perform inferences on new data, essentially determining how often new anomaly score data points are generated. Format examples: 30s, 4m, 2h, 1d (time units: 's' for seconds, 'm' for minutes, 'h' for hours, 'd' for days). This parameter essentially asks, at regular intervals (e.g., every 1 minute), whether the latest data points appear abnormal based on historical data.
|
||||
* `fit_every` - Sets the frequency for retraining the models. [Online models](https://docs.victoriametrics.com/anomaly-detection/components/models/#online-models) learn from every inference batch, so set a large value such as `1000d` to make fitting effectively bootstrap-only. For evolving behavior, configure the model's forgetting or reactivity mechanism, or choose a finite fit cadence to reset accumulated state. Format is similar to `infer_every`.
|
||||
* `fit_every` - Sets the frequency for retraining the models. A higher frequency ensures more updated models but requires more CPU resources. If omitted, models are retrained in each `infer_every` cycle. Format is similar to `infer_every`.
|
||||
* `fit_window` - Defines the data interval for training the models. Longer intervals allow for capturing extensive historical behavior and better seasonal pattern detection but may slow down the model's response to permanent metric changes and increase resource consumption. A minimum of two full seasonal cycles is recommended. Example format: 3h for three hours of data.
|
||||
|
||||
* `models`
|
||||
* `class` - Specifies the model to be used. Options include custom models ([guide here](https://docs.victoriametrics.com/anomaly-detection/components/models/#custom-model-guide)) or a selection from [built-in models](https://docs.victoriametrics.com/anomaly-detection/components/models/#built-in-models). For operational metrics with calendar behavior, use the online [Temporal Envelope](https://docs.victoriametrics.com/anomaly-detection/components/models/#temporal-envelope).
|
||||
* Model-specific parameters are configured directly below the model alias, as shown in the example.
|
||||
* `class` - Specifies the model to be used. Options include custom models ([guide here](https://docs.victoriametrics.com/anomaly-detection/components/models/#custom-model-guide)) or a selection from [built-in models](https://docs.victoriametrics.com/anomaly-detection/components/models/#built-in-models), such as the [Facebook Prophet](https://docs.victoriametrics.com/anomaly-detection/components/models/#prophet) (`model.prophet.ProphetModel`).
|
||||
* `args` - Model-specific parameters, formatted as a YAML dictionary in the `key: value` structure. Parameters available in [FB Prophet](https://facebook.github.io/prophet/docs/quick_start) can be used as an example.
|
||||
|
||||
* `reader`
|
||||
* `datasource_url` - The URL for the data source, typically an HTTP endpoint serving `/api/v1/query_range`.
|
||||
@@ -145,16 +145,16 @@ Below is an illustrative example of a `vmanomaly_config.yml` configuration file.
|
||||
schedulers:
|
||||
periodic:
|
||||
infer_every: "1m"
|
||||
fit_every: "1000d" # bootstrap-only schedule; use a finite cadence if accumulated state must be reset
|
||||
fit_window: "14d" # two weekly cycles for initial bootstrap
|
||||
fit_every: "1h"
|
||||
fit_window: "2d" # 2d-14d based on the presence of weekly seasonality in your data
|
||||
|
||||
models:
|
||||
temporal_envelope:
|
||||
class: "temporal_envelope"
|
||||
alpha: 0.005 # adapt the trend while using the bootstrap-only fit schedule
|
||||
loss_reactivity: 5 # allow new deviations to update the envelope
|
||||
seasonalities: ["hod_smooth", "dow_smooth"]
|
||||
provide_series: ["anomaly_score", "y", "yhat", "yhat_lower", "yhat_upper"]
|
||||
prophet:
|
||||
class: "prophet"
|
||||
args:
|
||||
interval_width: 0.98
|
||||
weekly_seasonality: False # comment it if your data has weekly seasonality
|
||||
yearly_seasonality: False
|
||||
|
||||
reader:
|
||||
datasource_url: "http://victoriametrics:8428/"
|
||||
@@ -279,24 +279,19 @@ global:
|
||||
scrape_configs:
|
||||
- job_name: 'vmagent'
|
||||
static_configs:
|
||||
- targets:
|
||||
- 'vmagent:8429'
|
||||
- targets: ['vmagent:8429']
|
||||
- job_name: 'vmalert'
|
||||
static_configs:
|
||||
- targets:
|
||||
- 'vmalert:8880'
|
||||
- targets: ['vmalert:8880']
|
||||
- job_name: 'victoriametrics'
|
||||
static_configs:
|
||||
- targets:
|
||||
- 'victoriametrics:8428'
|
||||
- targets: ['victoriametrics:8428']
|
||||
- job_name: 'node-exporter'
|
||||
static_configs:
|
||||
- targets:
|
||||
- 'node-exporter:9100'
|
||||
- targets: ['node-exporter:9100']
|
||||
- job_name: 'vmanomaly'
|
||||
static_configs:
|
||||
- targets:
|
||||
- 'vmanomaly:8490'
|
||||
- targets: [ 'vmanomaly:8490' ]
|
||||
```
|
||||
|
||||
|
||||
@@ -392,7 +387,7 @@ services:
|
||||
- "--notifier.url=http://alertmanager:9093/"
|
||||
- "--rule=/etc/alerts/*.yml"
|
||||
# display source of alerts in grafana
|
||||
- "--external.url=http://127.0.0.1:3000" # grafana outside container
|
||||
- "--external.url=http://127.0.0.1:3000" #grafana outside container
|
||||
# when copypaste the line be aware of '$$' for escaping in '$expr'
|
||||
- '--external.alert.source=explore?orgId=1&left=["now-1h","now","VictoriaMetrics",{"expr": },{"mode":"Metrics"},{"ui":[true,true,true,"none"]}]'
|
||||
networks:
|
||||
@@ -400,7 +395,7 @@ services:
|
||||
restart: always
|
||||
vmanomaly:
|
||||
container_name: vmanomaly
|
||||
image: victoriametrics/vmanomaly:v1.30.2
|
||||
image: victoriametrics/vmanomaly:v1.30.1
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
|
||||
@@ -95,15 +95,16 @@ See also multitenancy [via headers](#multitenancy-via-headers) and [via labels](
|
||||
|
||||
### Multitenancy via headers
|
||||
|
||||
With `--enableMultitenancyViaHeaders` {{% available_from "v1.143.0" %}} command-line flag enabled (enabled by default {{% available_from "#" %}})
|
||||
tenant ID can be specified via HTTP headers `AccountID` and `ProjectID`. This flag needs to be enabled on vminserts and vmselects.
|
||||
By default, VictoriaMetrics allows specifying `accountID` and `projectID` only in the request URL.
|
||||
|
||||
With `--enableMultitenancyViaHeaders` enabled [URL format](#url-format) can be simplified to the following:
|
||||
Set `--enableMultitenancyViaHeaders` {{% available_from "v1.143.0" %}} command-line flag to support
|
||||
specifying `accountID` and `projectID` via HTTP headers `AccountID` and `ProjectID` respectively.
|
||||
This flag needs to be specified separately for vminserts and vmselects.
|
||||
|
||||
When `--enableMultitenancyViaHeaders` is enabled, [URL format](#url-format) can be simplified to the following:
|
||||
- `http://<vminsert>:8480/insert/<suffix>` for writes
|
||||
- `http://<vmselect>:8481/select/prometheus/<suffix>` for reads
|
||||
|
||||
> Set --enableMultitenancyViaHeaders=false to disable simplified URL format.
|
||||
|
||||
For example, the following query will only select metric `up` from `accountID=2` and `projectID=3`:
|
||||
```
|
||||
curl 'https://<vmselect>:8481/select/prometheus/api/v1/query' \
|
||||
|
||||
@@ -26,24 +26,11 @@ See also [LTS releases](https://docs.victoriametrics.com/victoriametrics/lts-rel
|
||||
|
||||
## tip
|
||||
|
||||
**Update Note 1:** `vmselect` and `vminsert` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/), and `vmagent`: default value of `-enableMultitenancyViaHeaders` command-line flag has changed from `false` to `true`. This change enables support of [multitenancy via headers for cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#multitenancy-via-headers) and [for vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/#multitenancy-via-headers) by default. With this change, mentioned components will start supporting URLs with omitted tenant ID in the path: `https://<vmselect>:8481/select/prometheus/api/v1/query` will become a valid URL. To disable multitenancy via headers and simplified URLs set `--enableMultitenancyViaHeaders=false` on vmagent, vminsert and vmselect.
|
||||
|
||||
* FEATURE: [relabeling](https://docs.victoriametrics.com/victoriametrics/relabeling/): reduce CPU usage up to 30% when matching relabeling rules with multiple `if` expressions containing exact metric names. Expressions for other metric names are now skipped before evaluating their remaining label filters. See [#11341](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11341). Thanks to @nevgeny for contribution.
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/) and [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/): add support for [linode_sd_configs](https://docs.victoriametrics.com/victoriametrics/sd_configs/#linode_sd_configs) for discovering scrape targets from Linode instances. See [#9118](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9118). Thanks to @cxdy for contribution.
|
||||
* FEATURE: [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/): extend `-replay.continueWithExecutionErr` to also handle the `400 Bad Request` response code, since it is used for Prometheus querying API requests when request parameters are missing or incorrect. See [#11352](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11352).
|
||||
* FEATURE: `vmselect` and `vminsert` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/), and `vmagent`: set default value of `-enableMultitenancyViaHeaders` to `true`. This change enables support of [multitenancy via headers for cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#multitenancy-via-headers) and [for vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/#multitenancy-via-headers) by default, aligning VictoriaMetrics multitenancy behavior with [multitenancy in VictoriaLogs](https://docs.victoriametrics.com/victorialogs/#multitenancy). See related ticket [#11365](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11365).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui): add an option to customize the favicon color. This makes it easier to distinguish between different installations opened in multiple browser tabs. See [#11329](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11329).
|
||||
* FEATURE: [alerts](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/rules): limit the `RequestErrorsToAPI` alert to known API paths. Introduce a new `RequestErrorsToUnknownPaths` alert for authentication failures and requests to unknown paths. See [#11200](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11200).
|
||||
|
||||
* BUGFIX: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/) and [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/): avoid suggesting the unrelated `-enableTCP6` command-line flag when scraping a target over a Unix domain socket fails. See [#11320](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11320). Thanks to @lwmacct for contribution.
|
||||
* BUGFIX: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) and `vminsert` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): skip labels with empty name at [/api/v1/import](https://docs.victoriametrics.com/victoriametrics/#how-to-import-data-in-json-line-format). Previously such a label replaced the metric name, so a series sent with `"metric":{"__name__":"foo","":"bar"}` was stored under the name `bar`. Other ingestion protocols already skip such labels. See [#4962](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4962). Thanks to @Vandit1604 for contribution.
|
||||
* BUGFIX: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/), `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/) and [vmctl](https://docs.victoriametrics.com/victoriametrics/vmctl/): properly parse small fractional Unix timestamps in timestamp args such as `start` and `end` in `/api/v1/query_range` and `--vm-native-filter-time-start` and `--vm-native-filter-time-end` in `vmctl`. Previously, fractional Unix timestamps with the integer part below `9223372` were interpreted with the wrong unit, for example `12.0` was parsed as `12000` seconds instead of `12` seconds. See [#11324](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11324).
|
||||
* BUGFIX: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/), `vmstorage` and `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): persist the previous working set cache during graceful shutdown when it is likely to contain the active working set. This prevents saving an empty or cold current cache right after split-mode cache rotation, which could otherwise slow down ingestion or queries after restart until the cache warms up again. See [#11299](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11299).
|
||||
* BUGFIX: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) and `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): change the HTTP response code for [Prometheus querying API](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#prometheus-querying-api-usage) requests from `422 Unprocessable Entity` to `400 Bad Request` when request parameters are missing or incorrect. See [#11330](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11330).
|
||||
* BUGFIX: [vmui](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui): respect the custom query step specified via `g0.step_input` when opening a URL. Previously, it could be reset to the automatically calculated step and potentially cause dashboards to freeze. See [#11137](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11137).
|
||||
* BUGFIX: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/) and [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/): properly assign scrape target IP address at IPv6-only networks for [docker_sd_configs](https://docs.victoriametrics.com/victoriametrics/sd_configs/#docker_sd_configs). See [#10965](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10965).
|
||||
* BUGFIX: [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/): rename `vmalert_rule_group_results_limit` back to `vmalert_group_rule_results_limit`. The metric was introduced in [v1.147.0](https://docs.victoriametrics.com/victoriametrics/changelog/#v11470) but was accidentally given the wrong name. See [#11179](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11179).
|
||||
* BUGFIX: [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/): properly update group-level `eval_delay` and `eval_alignment` for existing groups during runtime when config reload is triggered periodically or manually via `/-/reload`. Previously, these settings weren't updated after config reload during runtime. See [#11374](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11374).
|
||||
* BUGFIX: `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): scale the default `-search.maxConcurrentRequests` with the number of available CPU cores instead of capping it at 16. See [#11191](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11191). Thanks to @Dhru1Tanna for contribution.
|
||||
|
||||
## [v1.149.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.149.0)
|
||||
|
||||
@@ -458,6 +445,8 @@ Released at 2026-07-03
|
||||
All these fixes are also included in [the latest community release](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest).
|
||||
The v1.136.x line will be supported for at least 12 months since [v1.136.0](https://docs.victoriametrics.com/victoriametrics/changelog/#v11360) release**
|
||||
|
||||
**Update Note 1:** [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/): vmalert updates VictoriaLogs LogsQL query parser to [v1.51.0](https://docs.victoriametrics.com/victorialogs/changelog/#v1510), which contains a breaking change in LogsQL filter pipes handling. If you used vmalert with `vlogs` query type and query expressions contained deprecated syntax - these rules will fail the validation on vmalert restart. Please review the [VictoriaLogs v1.51.0 changelog](https://docs.victoriametrics.com/victorialogs/changelog/#v1510) and update your alerting rules accordingly before upgrading.
|
||||
|
||||
* SECURITY: upgrade base docker image (Alpine) from 3.23.4 to 3.24.1. See [Alpine 3.24.1 release notes](https://www.alpinelinux.org/posts/Alpine-3.24.1-released.html).
|
||||
|
||||
* BUGFIX: `vminsert` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/) and [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/): properly check values range for the limits configured with flags `-maxLabelsPerTimeseries`, `-maxLabelNameLen` and `-maxLabelValueLen`. It must be in range `1..65535`. See [#11128](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11128).
|
||||
@@ -868,6 +857,8 @@ Released at 2026-07-03
|
||||
All these fixes are also included in [the latest community release](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest).
|
||||
The v1.122.x line will be supported for at least 12 months since [v1.122.0](https://docs.victoriametrics.com/victoriametrics/changelog/#v11220) release**
|
||||
|
||||
**Update Note 1:** [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/): vmalert updates VictoriaLogs LogsQL query parser to [v1.51.0](https://docs.victoriametrics.com/victorialogs/changelog/#v1510), which contains a breaking change in LogsQL filter pipes handling. If you used vmalert with `vlogs` query type and query expressions contained deprecated syntax - these rules will fail the validation on vmalert restart. Please review the [VictoriaLogs v1.51.0 changelog](https://docs.victoriametrics.com/victorialogs/changelog/#v1510) and update your alerting rules accordingly before upgrading.
|
||||
|
||||
* SECURITY: upgrade base docker image (Alpine) from 3.23.4 to 3.24.1. See [Alpine 3.24.1 release notes](https://www.alpinelinux.org/posts/Alpine-3.24.1-released.html).
|
||||
|
||||
* BUGFIX: `vminsert` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/) and [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/): properly check values range for the limits configured with flags `-maxLabelsPerTimeseries`, `-maxLabelNameLen` and `-maxLabelValueLen`. It must be in range `1..65535`. See [#11128](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11128).
|
||||
|
||||
@@ -6,16 +6,15 @@ build:
|
||||
sitemap:
|
||||
disable: true
|
||||
---
|
||||
|
||||
## Data model
|
||||
|
||||
### What is a metric
|
||||
|
||||
Simply put, `metric` is a numeric measure or observation of something.
|
||||
|
||||
The most common use cases for metrics are:
|
||||
The most common use-cases for metrics are:
|
||||
|
||||
- check how the system behaves at a particular time period;
|
||||
- check how the system behaves at the particular time period;
|
||||
- correlate behavior changes to other measurements;
|
||||
- observe or forecast trends;
|
||||
- trigger events (alerts) if the metric exceeds a threshold.
|
||||
@@ -26,7 +25,7 @@ Let's start with an example. To track how many requests our application serves,
|
||||
name `requests_total`.
|
||||
|
||||
You can be more specific here by saying `requests_success_total` (for only successful requests)
|
||||
or `request_errors_total` (for requests which failed). Choosing a metric name is very important and is supposed to clarify
|
||||
or `request_errors_total` (for requests which failed). Choosing a metric name is very important and supposed to clarify
|
||||
what is actually measured to every person who reads it, just like **variable names** in programming.
|
||||
|
||||
#### Labels
|
||||
@@ -55,14 +54,14 @@ requests_total{path="/", code="200"}
|
||||
Labels can be automatically attached to the [time series](#time-series)
|
||||
written via [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/#adding-labels-to-metrics)
|
||||
or [Prometheus](https://docs.victoriametrics.com/victoriametrics/integrations/prometheus/).
|
||||
VictoriaMetrics supports enforcing label filters for the [query API](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#prometheus-querying-api-enhancements)
|
||||
VictoriaMetrics supports enforcing of label filters for [query API](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#prometheus-querying-api-enhancements)
|
||||
to emulate data isolation. However, the real data isolation can be achieved via [multi-tenancy](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#multitenancy).
|
||||
|
||||
#### Time series
|
||||
|
||||
A combination of a metric name and its labels defines a `time series`. For example,
|
||||
`requests_total{path="/", code="200"}` and `requests_total{path="/", code="403"}`
|
||||
are two different time series because they have different values for the `code` label.
|
||||
are two different time series because they have different values for `code` label.
|
||||
|
||||
The number of unique time series has an impact on database resource usage.
|
||||
See [what is an active time series](https://docs.victoriametrics.com/victoriametrics/faq/#what-is-an-active-time-series) and
|
||||
@@ -70,8 +69,9 @@ See [what is an active time series](https://docs.victoriametrics.com/victoriamet
|
||||
|
||||
#### Cardinality
|
||||
|
||||
The number of unique [time series](#time-series) is named `cardinality`. Having too many unique time series is named `high cardinality`.
|
||||
[High cardinality](https://docs.victoriametrics.com/victoriametrics/faq/#what-is-high-cardinality) may result in increased resource usage in VictoriaMetrics.
|
||||
The number of unique [time series](#time-series) is named `cardinality`. Too big number of unique time series is named `high cardinality`.
|
||||
High cardinality may result in increased resource usage at VictoriaMetrics.
|
||||
See [these docs](https://docs.victoriametrics.com/victoriametrics/faq/#what-is-high-cardinality) for more details.
|
||||
|
||||
#### Raw samples
|
||||
|
||||
@@ -108,13 +108,13 @@ of the [time series](https://docs.victoriametrics.com/victoriametrics/keyconcept
|
||||
| requests_total{path="/health", code="200"} | 4 | 1676297730 |
|
||||
....
|
||||
```
|
||||
Here we have a time series `requests_total{path="/health", code="200"}` which has a value updated every `30s`.
|
||||
This means its resolution is also `30s`.
|
||||
Here we have a time series `requests_total{path="/health", code="200"}` which has a value update each `30s`.
|
||||
This means, its resolution is also a `30s`.
|
||||
|
||||
> In terms of [pull model](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#pull-model), resolution is equal
|
||||
> to `scrape_interval` and is controlled by the monitoring system (server).
|
||||
> For [push model](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#push-model), resolution is an interval between
|
||||
> sample timestamps and is controlled by a client (metrics collector).
|
||||
> samples timestamps and is controlled by a client (metrics collector).
|
||||
|
||||
Try to keep time series resolution consistent, since some [MetricsQL](#metricsql) functions may expect it to be so.
|
||||
|
||||
@@ -126,10 +126,10 @@ type exists specifically to help users to understand how the metric was measured
|
||||
|
||||
#### Counter
|
||||
|
||||
A counter is a metric that counts an event. Its value increases or stays the same over time.
|
||||
It cannot decrease in the general case. The only exception is, e.g., `counter reset`,
|
||||
Counter is a metric, which counts some events. Its value increases or stays the same over time.
|
||||
It cannot decrease in general case. The only exception is e.g. `counter reset`,
|
||||
when the metric resets to zero. The `counter reset` can occur when the service, which exposes the counter, restarts.
|
||||
So, the `counter` metric shows the number of observed events since the service started.
|
||||
So, the `counter` metric shows the number of observed events since the service start.
|
||||
|
||||
In programming, `counter` is a variable that you **increment** each time something happens.
|
||||
|
||||
@@ -139,7 +139,7 @@ In programming, `counter` is a variable that you **increment** each time somethi
|
||||
above is that time series `vm_http_requests_total{instance="localhost:8428", job="victoriametrics", path="api/v1/query_range"}`
|
||||
was rapidly changing from 1:38 pm to 1:39 pm, then there were no changes until 1:41 pm.
|
||||
|
||||
A counter is used for measuring the number of events, like the number of requests, errors, logs, messages, etc.
|
||||
Counter is used for measuring the number of events, like the number of requests, errors, logs, messages, etc.
|
||||
The most common [MetricsQL](#metricsql) functions used with counters are:
|
||||
|
||||
* [rate](https://docs.victoriametrics.com/victoriametrics/metricsql/#rate) - calculates the average per-second speed of metric change.
|
||||
@@ -148,7 +148,7 @@ The most common [MetricsQL](#metricsql) functions used with counters are:
|
||||
time period specified in square brackets.
|
||||
For example, `increase(requests_total[1h])` shows the number of requests served over the last hour.
|
||||
|
||||
It is OK to have fractional counters. For example, the `request_duration_seconds_sum` counter may sum the durations of all the requests.
|
||||
It is OK to have fractional counters. For example, `request_duration_seconds_sum` counter may sum the durations of all the requests.
|
||||
Every duration may have a fractional value in seconds, e.g. `0.5` of a second. So the cumulative sum of all the request durations
|
||||
may be fractional too.
|
||||
|
||||
@@ -162,12 +162,12 @@ Gauge is used for measuring a value that can go up and down:
|
||||

|
||||
|
||||
The metric `process_resident_memory_anon_bytes` on the graph shows the memory usage of the application at every given time.
|
||||
It is changing frequently, going up and down, showing how the process allocates and frees the memory.
|
||||
It is changing frequently, going up and down showing how the process allocates and frees the memory.
|
||||
In programming, `gauge` is a variable to which you **set** a specific value as it changes.
|
||||
|
||||
Gauge is used in the following scenarios:
|
||||
|
||||
* measuring temperature, memory usage, disk usage, etc;
|
||||
* measuring temperature, memory usage, disk usage etc;
|
||||
* storing the state of some process. For example, gauge `config_reloaded_successful` can be set to `1` if everything is
|
||||
good, and to `0` if configuration failed to reload;
|
||||
* storing the timestamp when the event happened. For example, `config_last_reload_success_timestamp_seconds`
|
||||
@@ -178,11 +178,11 @@ and [rollup functions](https://docs.victoriametrics.com/victoriametrics/metricsq
|
||||
|
||||
#### Histogram
|
||||
|
||||
A histogram is a set of [counter](#counter) metrics with different `vmrange` or `le` labels.
|
||||
Histogram is a set of [counter](#counter) metrics with different `vmrange` or `le` labels.
|
||||
The `vmrange` or `le` labels define measurement boundaries of a particular bucket.
|
||||
When the observed measurement hits a particular bucket, then the corresponding counter is incremented.
|
||||
|
||||
Histogram buckets usually have a `_bucket` suffix in their names.
|
||||
Histogram buckets usually have `_bucket` suffix in their names.
|
||||
For example, VictoriaMetrics tracks the distribution of rows processed per query with the `vm_rows_read_per_query` histogram.
|
||||
The exposition format for this histogram has the following form:
|
||||
|
||||
@@ -200,10 +200,10 @@ The `vm_rows_read_per_query_bucket{vmrange="4.084e+02...4.642e+02"} 2` line mean
|
||||
that there were 2 queries with the number of rows in the range `(408.4 - 464.2]`
|
||||
since the last VictoriaMetrics start.
|
||||
|
||||
The counters ending with the `_bucket` suffix allow estimating arbitrary percentiles
|
||||
The counters ending with `_bucket` suffix allow estimating arbitrary percentile
|
||||
for the observed measurement with the help of [histogram_quantile](https://docs.victoriametrics.com/victoriametrics/metricsql/#histogram_quantile)
|
||||
function. For example, the following query returns the estimated 99th percentile
|
||||
on the number of rows read per query during the last hour (see `1h` in square brackets):
|
||||
on the number of rows read per each query during the last hour (see `1h` in square brackets):
|
||||
|
||||
```metricsql
|
||||
histogram_quantile(0.99, sum(increase(vm_rows_read_per_query_bucket[1h])) by (vmrange))
|
||||
@@ -215,15 +215,15 @@ This query works in the following way:
|
||||
number of events over the last hour.
|
||||
1. The `sum(...) by (vmrange)` calculates per-bucket events by summing per-instance buckets
|
||||
with the same `vmrange` values.
|
||||
1. The `histogram_quantile(0.99, ...)` calculates the 99th percentile over `vmrange` buckets returned at step 2.
|
||||
1. The `histogram_quantile(0.99, ...)` calculates 99th percentile over `vmrange` buckets returned at step 2.
|
||||
|
||||
Histogram metric type exposes two additional counters ending with `_sum` and `_count` suffixes:
|
||||
|
||||
- the `vm_rows_read_per_query_sum` is a sum of all the observed measurements,
|
||||
e.g., the sum of rows served by all the queries since the last VictoriaMetrics start.
|
||||
e.g. the sum of rows served by all the queries since the last VictoriaMetrics start.
|
||||
|
||||
- the `vm_rows_read_per_query_count` is the total number of observed events,
|
||||
e.g., the total number of observed queries since the last VictoriaMetrics start.
|
||||
e.g. the total number of observed queries since the last VictoriaMetrics start.
|
||||
|
||||
These counters allow calculating the average measurement value on a particular lookbehind window.
|
||||
For example, the following query calculates the average number of rows read per query
|
||||
@@ -233,7 +233,7 @@ during the last 5 minutes (see `5m` in square brackets):
|
||||
increase(vm_rows_read_per_query_sum[5m]) / increase(vm_rows_read_per_query_count[5m])
|
||||
```
|
||||
|
||||
The `vm_rows_read_per_query` histogram may be used in a Go application in the following way
|
||||
The `vm_rows_read_per_query` histogram may be used in Go application in the following way
|
||||
by using the [github.com/VictoriaMetrics/metrics](https://github.com/VictoriaMetrics/metrics) package:
|
||||
|
||||
```go
|
||||
@@ -246,7 +246,7 @@ for _, query := range queries {
|
||||
}
|
||||
```
|
||||
|
||||
Now let's see what happens each time `rowsReadPerQuery.Update` is called:
|
||||
Now let's see what happens each time when `rowsReadPerQuery.Update` is called:
|
||||
|
||||
* counter `vm_rows_read_per_query_sum` is incremented by value of `len(query.Rows)` expression;
|
||||
* counter `vm_rows_read_per_query_count` increments by 1;
|
||||
@@ -262,7 +262,7 @@ and calculating [quantiles](https://prometheus.io/docs/practices/histograms/#qua
|
||||
Grafana doesn't understand buckets with `vmrange` labels, so the [prometheus_buckets](https://docs.victoriametrics.com/victoriametrics/metricsql/#prometheus_buckets)
|
||||
function must be used for converting buckets with `vmrange` labels to buckets with `le` labels before building heatmaps in Grafana.
|
||||
|
||||
Histograms are usually used for measuring the distribution of latency, sizes of elements (batch size, for example), etc. There are two
|
||||
Histograms are usually used for measuring the distribution of latency, sizes of elements (batch size, for example) etc. There are two
|
||||
implementations of a histogram supported by VictoriaMetrics:
|
||||
|
||||
1. [Prometheus histogram](https://prometheus.io/docs/practices/histograms/). The canonical histogram implementation is
|
||||
@@ -271,7 +271,7 @@ implementations of a histogram supported by VictoriaMetrics:
|
||||
histogram requires a user to define ranges (`buckets`) statically.
|
||||
1. [VictoriaMetrics histogram](https://valyala.medium.com/improving-histogram-usability-for-prometheus-and-grafana-bc7e5df0e350)
|
||||
supported by [VictoriaMetrics/metrics](https://github.com/VictoriaMetrics/metrics) instrumentation library.
|
||||
VictoriaMetrics histogram automatically handles bucket boundaries, so users don't need to think about them.
|
||||
Victoriametrics histogram automatically handles bucket boundaries, so users don't need to think about them.
|
||||
|
||||
We recommend reading the following articles before you start using histograms:
|
||||
|
||||
@@ -303,27 +303,27 @@ The visualization of summaries is pretty straightforward:
|
||||
|
||||
Such an approach makes summaries easier to use but also puts significant limitations compared to [histograms](#histogram):
|
||||
|
||||
- It is impossible to calculate a quantile over multiple summary metrics, e.g. `sum(go_gc_duration_seconds{quantile="0.75"})`,
|
||||
- It is impossible to calculate quantile over multiple summary metrics, e.g. `sum(go_gc_duration_seconds{quantile="0.75"})`,
|
||||
`avg(go_gc_duration_seconds{quantile="0.75"})` or `max(go_gc_duration_seconds{quantile="0.75"})`
|
||||
won't return the expected 75th percentile over `go_gc_duration_seconds` metrics collected from multiple instances
|
||||
of the application. See [Latency Tip of the Day: You Can't Average Percentiles](https://latencytipoftheday.blogspot.de/2014/06/latencytipoftheday-you-cant-average.html) for details.
|
||||
of the application. See [this article](https://latencytipoftheday.blogspot.de/2014/06/latencytipoftheday-you-cant-average.html) for details.
|
||||
|
||||
- It is impossible to calculate quantiles other than the already pre-calculated quantiles.
|
||||
|
||||
- It is impossible to calculate quantiles for measurements collected over an arbitrary time range. Usually, `summary`
|
||||
quantiles are calculated over a fixed time range such as the last 5 minutes.
|
||||
|
||||
Summaries are usually used for tracking the pre-defined percentiles for latency, sizes of elements (batch size, for example), etc.
|
||||
Summaries are usually used for tracking the pre-defined percentiles for latency, sizes of elements (batch size, for example) etc.
|
||||
|
||||
### Instrumenting application with metrics
|
||||
|
||||
As was said at the beginning of the [types of metrics](#types-of-metrics) section, metric type defines how it was
|
||||
measured. VictoriaMetrics TSDB doesn't know about metric types. All it sees are metric names, labels, values, and timestamps.
|
||||
What these metrics are, what they measure, and how - all these depend on the application which emits them.
|
||||
What are these metrics, what do they measure, and how - all this depends on the application which emits them.
|
||||
|
||||
To instrument your application with metrics compatible with VictoriaMetrics we recommend
|
||||
using the [github.com/VictoriaMetrics/metrics](https://github.com/VictoriaMetrics/metrics) package.
|
||||
See [How to monitor Go applications with VictoriaMetrics](https://victoriametrics.medium.com/how-to-monitor-go-applications-with-victoriametrics-c04703110870).
|
||||
using [github.com/VictoriaMetrics/metrics](https://github.com/VictoriaMetrics/metrics) package.
|
||||
See more details on how to use it in [this article](https://victoriametrics.medium.com/how-to-monitor-go-applications-with-victoriametrics-c04703110870).
|
||||
|
||||
VictoriaMetrics is also compatible with [Prometheus client libraries for metrics instrumentation](https://prometheus.io/docs/instrumenting/clientlibs/).
|
||||
|
||||
@@ -331,20 +331,20 @@ VictoriaMetrics is also compatible with [Prometheus client libraries for metrics
|
||||
|
||||
We recommend following [Prometheus naming convention for metrics](https://prometheus.io/docs/practices/naming/). There
|
||||
are no strict restrictions, so any metric name and labels are accepted by VictoriaMetrics.
|
||||
But this convention helps to keep names meaningful, descriptive, and clear to other people.
|
||||
Following the convention is a good practice.
|
||||
But the convention helps to keep names meaningful, descriptive, and clear to other people.
|
||||
Following convention is a good practice.
|
||||
|
||||
#### Labels
|
||||
|
||||
Every measurement can contain an arbitrary number of `key="value"` labels. The good practice is to keep this number limited.
|
||||
Otherwise, it would be difficult to deal with measurements containing a large number of labels.
|
||||
Otherwise, it would be difficult to deal with measurements containing a big number of labels.
|
||||
By default, VictoriaMetrics limits the number of labels per measurement to `40` and drops other labels.
|
||||
This limit can be changed via the `-maxLabelsPerTimeseries` command-line flag if necessary (but this isn't recommended).
|
||||
This limit can be changed via `-maxLabelsPerTimeseries` command-line flag if necessary (but this isn't recommended).
|
||||
|
||||
Every label value can contain an arbitrary string value. The good practice is to use short and meaningful label values to
|
||||
describe the attribute of the metric, not to tell the story about it. For example, label-value pair
|
||||
`environment="prod"` is OK, but `log_message="long log message with a lot of details..."` is not OK. By default,
|
||||
VictoriaMetrics limits label values to 4KiB. This limit can be changed via the `-maxLabelValueLen` command-line flag.
|
||||
`environment="prod"` is ok, but `log_message="long log message with a lot of details..."` is not ok. By default,
|
||||
VictoriaMetrics limits label's value size with 4KiB. This limit can be changed via `-maxLabelValueLen` command-line flag.
|
||||
|
||||
It is very important to keep under control the number of unique label values, since every unique label value
|
||||
leads to a new [time series](#time-series). Try to avoid using volatile label values such as session ID or query ID in order to
|
||||
@@ -356,7 +356,7 @@ avoid excessive resource usage and database slowdown.
|
||||
supports [multi-tenancy](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#multitenancy)
|
||||
for data isolation.
|
||||
|
||||
Multi-tenancy can be emulated for the [single-server](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/)
|
||||
Multi-tenancy can be emulated for [single-server](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/)
|
||||
version of VictoriaMetrics by adding [labels](#labels) on [write path](#write-data)
|
||||
and enforcing [labels filtering](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#prometheus-querying-api-enhancements)
|
||||
on [read path](#query-data).
|
||||
@@ -391,10 +391,10 @@ It is allowed to push/write metrics to [single-node VictoriaMetrics](https://doc
|
||||
to [cluster component vminsert](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#architecture-overview)
|
||||
and to [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/).
|
||||
|
||||
The pros of the push model:
|
||||
The pros of push model:
|
||||
|
||||
* Simpler configuration at VictoriaMetrics side - there is no need to configure VictoriaMetrics with locations of the monitored applications.
|
||||
There is no need for complex [service discovery schemes](https://docs.victoriametrics.com/victoriametrics/sd_configs/).
|
||||
There is no need in complex [service discovery schemes](https://docs.victoriametrics.com/victoriametrics/sd_configs/).
|
||||
* Simpler security setup - there is no need to set up access from VictoriaMetrics to each monitored application.
|
||||
|
||||
See [Foiled by the Firewall: A Tale of Transition From Prometheus to VictoriaMetrics](https://www.percona.com/blog/2020/12/01/foiled-by-the-firewall-a-tale-of-transition-from-prometheus-to-victoriametrics/)
|
||||
@@ -406,22 +406,22 @@ The cons of push protocol:
|
||||
Every application needs to be individually configured with the address of the monitoring system
|
||||
for metrics delivery. It also needs to be configured with the interval between metric pushes
|
||||
and the strategy in case of metric delivery failure.
|
||||
* Non-trivial setup for metrics delivery into multiple monitoring systems.
|
||||
* Non-trivial setup for metrics' delivery into multiple monitoring systems.
|
||||
* It may be hard to tell whether the application went down or just stopped sending metrics for a different reason.
|
||||
* Applications can overload the monitoring system by pushing metrics at too short intervals.
|
||||
|
||||
### Pull model
|
||||
|
||||
The pull model is an approach popularized by [Prometheus](https://prometheus.io/), where the monitoring system decides when
|
||||
Pull model is an approach popularized by [Prometheus](https://prometheus.io/), where the monitoring system decides when
|
||||
and where to pull metrics from:
|
||||
|
||||

|
||||
|
||||
In the pull model, the monitoring system needs to be aware of all the applications it needs to monitor. The metrics are
|
||||
In pull model, the monitoring system needs to be aware of all the applications it needs to monitor. The metrics are
|
||||
scraped (pulled) from the known applications (aka `scrape targets`) via HTTP protocol on a regular basis (aka `scrape_interval`).
|
||||
|
||||
VictoriaMetrics supports discovering Prometheus-compatible targets and scraping metrics from them in the same way as Prometheus does -
|
||||
see [how to scrape Prometheus exporters in VictoriaMetrics](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#how-to-scrape-prometheus-exporters-such-as-node-exporter).
|
||||
see [these docs](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#how-to-scrape-prometheus-exporters-such-as-node-exporter).
|
||||
|
||||
Metrics scraping is supported by [single-node VictoriaMetrics](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#how-to-scrape-prometheus-exporters-such-as-node-exporter)
|
||||
and by [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/).
|
||||
@@ -431,7 +431,7 @@ The pros of the pull model:
|
||||
* Easier to debug - VictoriaMetrics knows about all the monitored applications (aka `scrape targets`).
|
||||
The `up == 0` query instantly shows unavailable scrape targets.
|
||||
The actual information about scrape targets is available at `http://victoriametrics:8428/targets` and `http://vmagent:8429/targets`.
|
||||
* The monitoring system controls the frequency of metrics scraping, so it is easier to control its load.
|
||||
* Monitoring system controls the frequency of metrics' scrape, so it is easier to control its load.
|
||||
* Applications aren't aware of the monitoring system and don't need to implement the logic for metrics delivery.
|
||||
|
||||
The cons of the pull model:
|
||||
@@ -448,13 +448,13 @@ The most common approach for data collection is using both models:
|
||||
|
||||

|
||||
|
||||
In this approach, the additional component is used - [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/). Vmagent is
|
||||
a lightweight agent whose main purpose is to collect, filter, relabel, and deliver metrics to VictoriaMetrics.
|
||||
In this approach the additional component is used - [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/). Vmagent is
|
||||
a lightweight agent whose main purpose is to collect, filter, relabel and deliver metrics to VictoriaMetrics.
|
||||
It supports all [push](#push-model) and [pull](#pull-model) protocols mentioned above.
|
||||
|
||||
The basic monitoring setup of VictoriaMetrics and vmagent is described
|
||||
in the [example docker-compose manifest](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker#readme).
|
||||
In this example, vmagent [scrapes a list of targets](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/prometheus-vm-single.yml)
|
||||
In this example vmagent [scrapes a list of targets](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/prometheus-vm-single.yml)
|
||||
and [forwards collected data to VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/9751ea10983d42068487624849cac7ad6fd7e1d8/deployment/docker/compose-vm-single.yml#L16).
|
||||
VictoriaMetrics is then used as a [datasource for Grafana](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/provisioning/datasources/prometheus/single.yml)
|
||||
installation for querying collected data.
|
||||
@@ -480,7 +480,7 @@ The API consists of two main handlers for serving [instant queries](#instant-que
|
||||
|
||||
### Instant query
|
||||
|
||||
An instant query executes the `query` expression at the given `time`:
|
||||
Instant query executes the `query` expression at the given `time`:
|
||||
|
||||
```
|
||||
GET | POST /api/v1/query?query=...&time=...&step=...&timeout=...
|
||||
@@ -497,13 +497,13 @@ Params:
|
||||
For example, the request `/api/v1/query?query=up&step=1m` looks for the last written raw sample for the metric `up`
|
||||
in the `(now()-1m, now()]` interval (the first millisecond is not included). If omitted, `step` is set to `5m` (5 minutes)
|
||||
by default.
|
||||
* `timeout` - optional query timeout. For example, `timeout=5s`. The query is canceled when the timeout is reached.
|
||||
By default, the timeout is set to the value of the `-search.maxQueryDuration` command-line flag passed to the single-node VictoriaMetrics
|
||||
or to the `vmselect` component of the VictoriaMetrics cluster.
|
||||
* `timeout` - optional query timeout. For example, `timeout=5s`. Query is canceled when the timeout is reached.
|
||||
By default the timeout is set to the value of `-search.maxQueryDuration` command-line flag passed to single-node VictoriaMetrics
|
||||
or to `vmselect` component of VictoriaMetrics cluster.
|
||||
|
||||
The result of an Instant query is a list of [time series](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#time-series)
|
||||
matching the filter in the `query` expression. Each returned series contains exactly one `(timestamp, value)` entry,
|
||||
where `timestamp` equals the `time` query arg, while the `value` contains the `query` result at the requested `time`.
|
||||
The result of Instant query is a list of [time series](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#time-series)
|
||||
matching the filter in `query` expression. Each returned series contains exactly one `(timestamp, value)` entry,
|
||||
where `timestamp` equals to the `time` query arg, while the `value` contains `query` result at the requested `time`.
|
||||
|
||||
To understand how instant queries work, let's begin with a data sample:
|
||||
|
||||
@@ -530,7 +530,7 @@ ranging from 1m to 3m. If we plot this data sample on the graph, it will have th
|
||||
{width="500"}
|
||||
|
||||
To get the value of the `foo_bar` series at some specific moment of time, for example `2022-05-10T08:03:00Z`, in
|
||||
VictoriaMetrics, we need to issue an **instant query**:
|
||||
VictoriaMetrics we need to issue an **instant query**:
|
||||
|
||||
```sh
|
||||
curl "http://<victoria-metrics-addr>/api/v1/query?query=foo_bar&time=2022-05-10T08:03:00.000Z"
|
||||
@@ -595,13 +595,13 @@ Params:
|
||||
The `query` is executed at `start`, `start+step`, `start+2*step`, ..., `start+N*step` timestamps,
|
||||
where `N` is the whole number of steps that fit between `start` and `end`.
|
||||
`end` is included only when it equals to `start+N*step`.
|
||||
If the `step` isn't set, then it defaults to `5m` (5 minutes).
|
||||
* `timeout` - optional query timeout. For example, `timeout=5s`. The query is canceled when the timeout is reached.
|
||||
By default, the timeout is set to the value of the `-search.maxQueryDuration` command-line flag passed to the single-node VictoriaMetrics
|
||||
or to the `vmselect` component in a VictoriaMetrics cluster.
|
||||
If the `step` isn't set, then it default to `5m` (5 minutes).
|
||||
* `timeout` - optional query timeout. For example, `timeout=5s`. Query is canceled when the timeout is reached.
|
||||
By default the timeout is set to the value of `-search.maxQueryDuration` command-line flag passed to single-node VictoriaMetrics
|
||||
or to `vmselect` component in VictoriaMetrics cluster.
|
||||
|
||||
The result of a Range query is a list of [time series](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#time-series)
|
||||
matching the filter in the `query` expression. Each returned series contains `(timestamp, value)` results for the `query` executed
|
||||
The result of Range query is a list of [time series](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#time-series)
|
||||
matching the filter in `query` expression. Each returned series contains `(timestamp, value)` results for the `query` executed
|
||||
at `start`, `start+step`, `start+2*step`, ..., `start+N*step` timestamps. In other words, Range query is an [Instant query](#instant-query)
|
||||
executed independently at `start`, `start+step`, ..., `start+N*step` timestamps with the only difference that an instant query
|
||||
does not return `ephemeral` samples (see below). Instead, if the database does not contain any samples for the requested time and step,
|
||||
@@ -705,7 +705,7 @@ In response, VictoriaMetrics returns `17` sample-timestamp pairs for the series
|
||||
from `2022-05-10T07:59:00Z` to `2022-05-10T08:17:00Z`. But, if we take a look at the original data sample again, we'll
|
||||
see that it contains only 13 raw samples. What happens here is that the range query is actually
|
||||
an [instant query](#instant-query) executed `1 + (start-end)/step` times on the time range from `start` to `end`. If we plot
|
||||
this request in VictoriaMetrics, the graph will be shown as follows:
|
||||
this request in VictoriaMetrics the graph will be shown as the following:
|
||||
|
||||

|
||||
{width="500"}
|
||||
@@ -720,13 +720,13 @@ This behavior of adding ephemeral data points comes from the specifics of the [p
|
||||
* Scrape may be skipped if the monitoring system is overloaded.
|
||||
* Scrape may fail due to network issues.
|
||||
|
||||
According to these specifics, the range query assumes that if there is a missing raw sample, then it is likely a missed
|
||||
According to these specifics, the range query assumes that if there is a missing raw sample then it is likely a missed
|
||||
scrape, so it fills it with the previous raw sample. The same will work for cases when `step` is lower than the actual
|
||||
interval between samples. In fact, if we set `step=1s` for the same request, we'll get about 1 thousand data points in
|
||||
response, where most of them are `ephemeral`.
|
||||
|
||||
Sometimes, the lookbehind window for locating the datapoint isn't big enough and the graph will contain a gap. For range
|
||||
queries, the lookbehind window isn't equal to the `step` parameter. It is calculated as the median of the intervals between
|
||||
queries, lookbehind window isn't equal to the `step` parameter. It is calculated as the median of the intervals between
|
||||
the last 20 raw samples in the requested time range. In this way, VictoriaMetrics automatically adjusts the lookbehind
|
||||
window to fill gaps and detect stale series at the same time.
|
||||
|
||||
@@ -734,7 +734,7 @@ Range queries are mostly used for plotting time series data over specified time
|
||||
useful in the following scenarios:
|
||||
|
||||
* Track the state of a metric on the given time interval;
|
||||
* Correlate changes between multiple metrics over the time interval;
|
||||
* Correlate changes between multiple metrics on the time interval;
|
||||
* Observe trends and dynamics of the metric change.
|
||||
|
||||
If you need to export raw samples from VictoriaMetrics, then take a look at [export APIs](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#how-to-export-time-series).
|
||||
@@ -745,7 +745,7 @@ By default, Victoria Metrics does not immediately return the recently written sa
|
||||
written prior to the time specified by the `-search.latencyOffset` command-line flag, which has a default offset of 30 seconds.
|
||||
This is true for both `query` and `query_range` and may give the impression that data is written to the VM with a 30-second delay.
|
||||
|
||||
This flag prevents inconsistent results due to the fact that only part of the values are scraped in the last scrape interval.
|
||||
This flag prevents from non-consistent results due to the fact that only part of the values are scraped in the last scrape interval.
|
||||
|
||||
Here is an illustration of a potential problem when `-search.latencyOffset` is set to zero:
|
||||
|
||||
@@ -758,12 +758,12 @@ duration throughout the `-search.latencyOffset` duration:
|
||||

|
||||
{width="1000"}
|
||||
|
||||
It can be overridden on a per-query basis via the `latency_offset` query arg.
|
||||
It can be overridden on per-query basis via `latency_offset` query arg.
|
||||
|
||||
VictoriaMetrics buffers recently ingested samples in memory for up to a few seconds and then periodically flushes these samples to disk.
|
||||
This buffering improves data ingestion performance. The buffered samples are invisible in query results, even if the `-search.latencyOffset` command-line flag is set to 0,
|
||||
This buffering improves data ingestion performance. The buffered samples are invisible in query results, even if `-search.latencyOffset` command-line flag is set to 0,
|
||||
or if `latency_offset` query arg is set to 0.
|
||||
You can send a GET request to the `/internal/force_flush` HTTP handler at a single-node VictoriaMetrics
|
||||
You can send GET request to `/internal/force_flush` http handler at single-node VictoriaMetrics
|
||||
or to `vmstorage` at [cluster version of VictoriaMetrics](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/)
|
||||
in order to forcibly flush the buffered samples to disk, so they become visible for querying. The `/internal/force_flush` handler
|
||||
is provided for debugging and testing purposes only. Do not call it in production, since this may significantly slow down data ingestion
|
||||
@@ -771,15 +771,15 @@ performance and increase resource usage.
|
||||
|
||||
### MetricsQL
|
||||
|
||||
VictoriaMetrics provides a special query language for executing read queries - [MetricsQL](https://docs.victoriametrics.com/victoriametrics/metricsql/).
|
||||
VictoriaMetrics provide a special query language for executing read queries - [MetricsQL](https://docs.victoriametrics.com/victoriametrics/metricsql/).
|
||||
It is a [PromQL](https://prometheus.io/docs/prometheus/latest/querying/basics)-like query language with a powerful set of
|
||||
functions and features for working specifically with time series data. MetricsQL is backward-compatible with PromQL,
|
||||
so it shares most of the query concepts. The basic concepts for PromQL and MetricsQL are
|
||||
described in this [PromQL tutorial for beginners](https://valyala.medium.com/promql-tutorial-for-beginners-9ab455142085).
|
||||
described [here](https://valyala.medium.com/promql-tutorial-for-beginners-9ab455142085).
|
||||
|
||||
#### Filtering
|
||||
|
||||
In sections [instant query](#instant-query) and [range query](#range-query), we've already used MetricsQL to get data for
|
||||
In sections [instant query](#instant-query) and [range query](#range-query) we've already used MetricsQL to get data for
|
||||
metric `foo_bar`. It is as simple as just writing a metric name in the query:
|
||||
|
||||
```metricsql
|
||||
@@ -793,14 +793,14 @@ requests_total{path="/", code="200"}
|
||||
requests_total{path="/", code="403"}
|
||||
```
|
||||
|
||||
To select only time series with a specific label value, specify the matching filter in curly braces:
|
||||
To select only time series with specific label value specify the matching filter in curly braces:
|
||||
|
||||
```metricsql
|
||||
requests_total{code="200"}
|
||||
```
|
||||
|
||||
The query above returns all time series with the name `requests_total` and label `code="200"`. We use the operator `=` to
|
||||
match the label value. For negative matches, use the `!=` operator. Filters also support positive regex matching via `=~`
|
||||
match label value. For negative match use `!=` operator. Filters also support positive regex matching via `=~`
|
||||
and negative regex matching via `!~`:
|
||||
|
||||
```metricsql
|
||||
@@ -813,7 +813,7 @@ Filters can also be combined:
|
||||
requests_total{code=~"200", path="/home"}
|
||||
```
|
||||
|
||||
The query above returns all time series with the `requests_total` name, which simultaneously have labels `code="200"` and `path="/home"`.
|
||||
The query above returns all time series with `requests_total` name, which simultaneously have labels `code="200"` and `path="/home"`.
|
||||
|
||||
#### Filtering by name
|
||||
|
||||
@@ -829,7 +829,7 @@ The query above returns series for two metrics: `requests_error_total` and `requ
|
||||
|
||||
#### Filtering by multiple "or" filters
|
||||
|
||||
[MetricsQL](https://docs.victoriametrics.com/victoriametrics/metricsql/) supports selecting time series that match at least one of multiple "or" filters.
|
||||
[MetricsQL](https://docs.victoriametrics.com/victoriametrics/metricsql/) supports selecting time series, which match at least one of multiple "or" filters.
|
||||
Such filters must be delimited by `or` inside curly braces. For example, the following query selects time series with
|
||||
`{job="app1",env="prod"}` or `{job="app2",env="dev"}` labels:
|
||||
|
||||
@@ -838,7 +838,7 @@ Such filters must be delimited by `or` inside curly braces. For example, the fol
|
||||
```
|
||||
|
||||
The number of `or` groups can be arbitrary. The number of `,`-delimited label filters per each `or` group can be arbitrary.
|
||||
Per-group filters are applied with the `and` operation, e.g., they select series simultaneously matching all the filters in the group.
|
||||
Per-group filters are applied with `and` operation, e.g. they select series simultaneously matching all the filters in the group.
|
||||
|
||||
This functionality allows passing the selected series to [rollup functions](https://docs.victoriametrics.com/victoriametrics/metricsql/#rollup-functions)
|
||||
such as [rate()](https://docs.victoriametrics.com/victoriametrics/metricsql/#rate)
|
||||
@@ -849,7 +849,7 @@ rate({job="app1",env="prod" or job="app2",env="dev"}[5m])
|
||||
|
||||
```
|
||||
|
||||
If you need to select series matching multiple filters for the same label, then it is better from a performance PoV
|
||||
If you need to select series matching multiple filters for the same label, then it is better from performance PoV
|
||||
to use regexp filter `{label=~"value1|...|valueN"}` instead of `{label="value1" or ... or label="valueN"}`.
|
||||
|
||||
|
||||
@@ -878,11 +878,11 @@ query may break or may lead to incorrect results. The basics of the matching rul
|
||||
|
||||
* MetricsQL engine strips metric names from all the time series on the left and right side of the arithmetic operation
|
||||
without touching labels.
|
||||
* For each time series on the left side, the MetricsQL engine searches for the corresponding time series on the right side
|
||||
with the same set of labels, applies the operation for each data point, and returns the resulting time series with the
|
||||
* For each time series on the left side MetricsQL engine searches for the corresponding time series on the right side
|
||||
with the same set of labels, applies the operation for each data point and returns the resulting time series with the
|
||||
same set of labels. If there are no matches, then the time series is dropped from the result.
|
||||
* The matching rules may be augmented with `ignoring`, `on`, `group_left` and `group_right` modifiers.
|
||||
See [Prometheus's vector matching documentation](https://prometheus.io/docs/prometheus/latest/querying/operators/#vector-matching) for details.
|
||||
See [these docs](https://prometheus.io/docs/prometheus/latest/querying/operators/#vector-matching) for details.
|
||||
|
||||
#### Comparison operations
|
||||
|
||||
@@ -896,7 +896,7 @@ MetricsQL supports the following comparison operators:
|
||||
* less-or-equal - `<=`
|
||||
|
||||
These operators may be applied to arbitrary MetricsQL expressions as with arithmetic operators. The result of the
|
||||
comparison operation is a time series with only matching data points. For instance, the following query would return
|
||||
comparison operation is time series with only matching data points. For instance, the following query would return
|
||||
series only for processes where memory usage exceeds `100MB`:
|
||||
|
||||
```metricsql
|
||||
@@ -906,7 +906,7 @@ process_resident_memory_bytes > 100*1024*1024
|
||||
#### Aggregation and grouping functions
|
||||
|
||||
MetricsQL allows aggregating and grouping of time series. Time series are grouped by the given set of labels and then the
|
||||
given aggregation function is applied individually to each group. For instance, the following query returns
|
||||
given aggregation function is applied individually per each group. For instance, the following query returns
|
||||
summary memory usage for each `job`:
|
||||
|
||||
```metricsql
|
||||
@@ -919,14 +919,14 @@ See [docs for aggregate functions in MetricsQL](https://docs.victoriametrics.com
|
||||
|
||||
One of the most widely used functions for [counters](#counter)
|
||||
is [rate](https://docs.victoriametrics.com/victoriametrics/metricsql/#rate). It calculates the average per-second increase rate individually
|
||||
for each matching time series. For example, the following query shows the average per-second data receive speed
|
||||
for each monitored `node_exporter` instance, which exposes the `node_network_receive_bytes_total` metric:
|
||||
per each matching time series. For example, the following query shows the average per-second data receive speed
|
||||
per each monitored `node_exporter` instance, which exposes the `node_network_receive_bytes_total` metric:
|
||||
|
||||
```metricsql
|
||||
rate(node_network_receive_bytes_total)
|
||||
```
|
||||
|
||||
By default, VictoriaMetrics calculates the `rate` over [raw samples](#raw-samples) on the lookbehind window specified in the `step` parameter
|
||||
By default, VictoriaMetrics calculates the `rate` over [raw samples](#raw-samples) on the lookbehind window specified in the `step` param
|
||||
passed either to [instant query](#instant-query) or to [range query](#range-query).
|
||||
The interval on which `rate` needs to be calculated can be specified explicitly
|
||||
as [duration](https://prometheus.io/docs/prometheus/latest/querying/basics/#float-literals-and-time-durations) in square brackets:
|
||||
@@ -935,10 +935,10 @@ as [duration](https://prometheus.io/docs/prometheus/latest/querying/basics/#floa
|
||||
rate(node_network_receive_bytes_total[5m])
|
||||
```
|
||||
|
||||
In this case, VictoriaMetrics uses the specified lookbehind window - `5m` (5 minutes) - for calculating the average per-second increase rate.
|
||||
In this case VictoriaMetrics uses the specified lookbehind window - `5m` (5 minutes) - for calculating the average per-second increase rate.
|
||||
Bigger lookbehind windows usually lead to smoother graphs.
|
||||
|
||||
`rate` strips the metric name while leaving all the labels for the inner time series. If you need to keep the metric name,
|
||||
`rate` strips metric name while leaving all the labels for the inner time series. If you need to keep the metric name,
|
||||
then add [keep_metric_names](https://docs.victoriametrics.com/victoriametrics/metricsql/#keep_metric_names) modifier
|
||||
after the `rate(..)`. For example, the following query leaves metric names after calculating the `rate()`:
|
||||
|
||||
@@ -952,7 +952,7 @@ rate(node_network_receive_bytes_total) keep_metric_names
|
||||
|
||||
VictoriaMetrics has a built-in graphical User Interface for querying and visualizing metrics -
|
||||
[VMUI](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui).
|
||||
Open the `http://victoriametrics:8428/vmui` page, type the query, and see the results:
|
||||
Open `http://victoriametrics:8428/vmui` page, type the query and see the results:
|
||||
|
||||

|
||||
|
||||
@@ -963,8 +963,8 @@ in the same way as Grafana queries Prometheus.
|
||||
## Modify data
|
||||
|
||||
VictoriaMetrics stores time series data in [MergeTree](https://en.wikipedia.org/wiki/Log-structured_merge-tree)-like
|
||||
data structures. While this approach is very efficient for write-heavy databases, it imposes some limitations on data
|
||||
updates. In short, modifying already written [time series](#time-series) requires rewriting the whole data block where
|
||||
data structures. While this approach is very efficient for write-heavy databases, it applies some limitations on data
|
||||
updates. In short, modifying already written [time series](#time-series) requires re-writing the whole data block where
|
||||
it is stored. Due to this limitation, VictoriaMetrics does not support direct data modification.
|
||||
|
||||
### Deletion
|
||||
@@ -974,14 +974,15 @@ See [How to delete time series](https://docs.victoriametrics.com/victoriametrics
|
||||
|
||||
### Relabeling
|
||||
|
||||
[Relabeling](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#relabeling) is a powerful mechanism for modifying time series before they have been written to the database. Relabeling
|
||||
may be applied for both [push](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#push-model) and [pull](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#pull-model) models.
|
||||
Relabeling is a powerful mechanism for modifying time series before they have been written to the database. Relabeling
|
||||
may be applied for both [push](#push-model) and [pull](#pull-model) models. See more
|
||||
details [here](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#relabeling).
|
||||
|
||||
### Deduplication
|
||||
|
||||
VictoriaMetrics supports data [deduplication](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#deduplication).
|
||||
VictoriaMetrics supports data deduplication. See [these docs](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#deduplication).
|
||||
|
||||
|
||||
### Downsampling
|
||||
|
||||
VictoriaMetrics Enterprise supports data [downsampling](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#downsampling). Downsampling can reduce disk space usage and improve query performance by reducing the number samples in a time series.
|
||||
VictoriaMetrics supports data downsampling. See [these docs](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#downsampling).
|
||||
|
||||
@@ -32,7 +32,6 @@ supports the following Prometheus-compatible service discovery options for Prome
|
||||
* `http_sd_configs` is for discovering and scraping targets provided by external http-based service discovery. See [these docs](#http_sd_configs).
|
||||
* `kubernetes_sd_configs` is for discovering and scraping [Kubernetes](https://kubernetes.io/) targets. See [these docs](#kubernetes_sd_configs).
|
||||
* `kuma_sd_configs` is for discovering and scraping [Kuma](https://kuma.io) targets. See [these docs](#kuma_sd_configs).
|
||||
* `linode_sd_configs` is for discovering and scraping [Linode](https://www.linode.com/) instances. See [these docs](#linode_sd_configs).
|
||||
* `marathon_sd_configs` is for discovering and scraping [Marathon](https://github.com/d2iq-archive/marathon) targets. See [these docs](#marathon_sd_configs).
|
||||
* `nomad_sd_configs` is for discovering and scraping targets registered in [HashiCorp Nomad](https://www.nomadproject.io/). See [these docs](#nomad_sd_configs).
|
||||
* `openstack_sd_configs` is for discovering and scraping OpenStack targets. See [these docs](#openstack_sd_configs).
|
||||
@@ -1314,81 +1313,6 @@ The following meta labels are available on discovered targets during [relabeling
|
||||
|
||||
The list of discovered Kuma targets is refreshed at the interval, which can be configured via `-promscrape.kumaSDCheckInterval` command-line flag.
|
||||
|
||||
## linode_sd_configs
|
||||
|
||||
Linode SD configuration {{% available_from "#" %}} allows retrieving scrape targets from [Linode](https://www.linode.com/) instances.
|
||||
The following [Linode API](https://www.linode.com/docs/api/) token scopes are required: `linodes:read_only` and `ips:read_only`.
|
||||
|
||||
Configuration example:
|
||||
|
||||
```yaml
|
||||
scrape_configs:
|
||||
- job_name: linode
|
||||
linode_sd_configs:
|
||||
|
||||
# server is an optional Linode API server to query.
|
||||
# By default, https://api.linode.com is used.
|
||||
#
|
||||
# server: "https://api.linode.com"
|
||||
|
||||
# port is an optional port to scrape metrics from. By default, port 80 is used.
|
||||
#
|
||||
# port: ...
|
||||
|
||||
# tag_separator is an optional string used to join multi-value labels such as tags and extra IPs.
|
||||
# By default, "," is used.
|
||||
#
|
||||
# tag_separator: ","
|
||||
|
||||
# region is an optional Linode region to filter instances by.
|
||||
# By default, instances from all regions are returned.
|
||||
#
|
||||
# region: "..."
|
||||
|
||||
# Required credentials for Linode API authentication.
|
||||
#
|
||||
authorization:
|
||||
credentials: "..."
|
||||
# type: "..." # default: Bearer
|
||||
# credentials_file: "..." # is mutually-exclusive with credentials
|
||||
|
||||
# Additional HTTP API client options can be specified here.
|
||||
# See https://docs.victoriametrics.com/victoriametrics/sd_configs/#http-api-client-options
|
||||
```
|
||||
|
||||
Each discovered target has an [`__address__`](https://docs.victoriametrics.com/victoriametrics/relabeling/#how-to-modify-scrape-urls-in-targets) label set
|
||||
to `<ip>:<port>`, where `<ip>` is the public IPv4 of the Linode instance when available, otherwise the private IPv4, and `<port>` is the port specified in the `linode_sd_configs`.
|
||||
Instances without a usable IPv4 address are skipped.
|
||||
|
||||
The following meta labels are available on discovered targets during [relabeling](https://docs.victoriametrics.com/victoriametrics/relabeling/):
|
||||
|
||||
* `__meta_linode_instance_id`: the id of the linode instance
|
||||
* `__meta_linode_instance_label`: the label of the linode instance
|
||||
* `__meta_linode_image`: the slug of the linode instance's image
|
||||
* `__meta_linode_private_ipv4`: the private IPv4 of the linode instance
|
||||
* `__meta_linode_public_ipv4`: the public IPv4 of the linode instance
|
||||
* `__meta_linode_public_ipv6`: the public IPv6 of the linode instance
|
||||
* `__meta_linode_private_ipv4_rdns`: the reverse DNS for the first private IPv4 of the linode instance
|
||||
* `__meta_linode_public_ipv4_rdns`: the reverse DNS for the first public IPv4 of the linode instance
|
||||
* `__meta_linode_public_ipv6_rdns`: the reverse DNS for the first public IPv6 of the linode instance
|
||||
* `__meta_linode_region`: the region of the linode instance
|
||||
* `__meta_linode_type`: the type of the linode instance
|
||||
* `__meta_linode_status`: the status of the linode instance
|
||||
* `__meta_linode_tags`: a list of tags of the linode instance joined by the tag separator
|
||||
* `__meta_linode_group`: the display group a linode instance is a member of
|
||||
* `__meta_linode_gpus`: the number of GPUs of the linode instance
|
||||
* `__meta_linode_hypervisor`: the virtualization software powering the linode instance
|
||||
* `__meta_linode_backups`: the backup service status of the linode instance
|
||||
* `__meta_linode_specs_disk_bytes`: the amount of storage space the linode instance has access to
|
||||
* `__meta_linode_specs_memory_bytes`: the amount of RAM the linode instance has access to
|
||||
* `__meta_linode_specs_vcpus`: the number of VCPUs this linode has access to
|
||||
* `__meta_linode_specs_transfer_bytes`: the amount of network transfer the linode instance is allotted each month
|
||||
* `__meta_linode_extra_ips`: a list of all extra IPv4 addresses assigned to the linode instance joined by the tag separator
|
||||
* `__meta_linode_ipv6_ranges`: a list of IPv6 ranges with mask assigned to the linode instance joined by the tag separator
|
||||
|
||||
The list of discovered Linode targets is refreshed at the interval, which can be configured via `-promscrape.linodeSDCheckInterval` command-line flag.
|
||||
Discovery failures are tracked in the `vm_promscrape_discovery_linode_failures_total` metric.
|
||||
|
||||
## marathon_sd_configs
|
||||
|
||||
Marathon SD configuration {{% available_from "v1.109.0" %}} allows retrieving scrape targets from [Marathon](https://github.com/d2iq-archive/marathon) REST API.
|
||||
|
||||
@@ -642,7 +642,7 @@ specified via `-remoteWrite.relabelConfig` and `-remoteWrite.urlRelabelConfig` c
|
||||
|
||||
vmagent can write data to multiple distinct tenants if:
|
||||
* its `-remoteWrite.url` points to the [VictoriaMetrics cluster multitenant URL](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#multitenancy-via-labels)
|
||||
* its `-enableMultitenantHandlers` and `-enableMultitenancyViaHeaders` (enabled by default {{% available_from "#" %}}) command-line flags are both set
|
||||
* its `-enableMultitenantHandlers` and `-enableMultitenancyViaHeaders` command-line flags are both set
|
||||
* clients ingest data into vmagent with the tenants specified [via headers](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#multitenancy-via-headers) {{% available_from "v1.143.0" %}}
|
||||
|
||||
```mermaid
|
||||
|
||||
@@ -405,7 +405,7 @@ See the docs at https://docs.victoriametrics.com/victoriametrics/vmalert/ .
|
||||
-remoteWrite.url string
|
||||
Optional URL to persist alerts state and recording rules results in form of timeseries. It must support either VictoriaMetrics remote write protocol or Prometheus remote_write protocol. Supports address in the form of IP address with a port (e.g., http://127.0.0.1:8428) or DNS SRV record. For example, if -remoteWrite.url=http://127.0.0.1:8428 is specified, then the alerts state will be written to http://127.0.0.1:8428/api/v1/write . See also -remoteWrite.disablePathAppend, '-remoteWrite.showURL'.
|
||||
-replay.continueWithExecutionErr
|
||||
Whether to continue replaying other rules if a rule execution fails with a 400 or 422 response code, which can happen due to an expression syntax error or a resource limit being hit.
|
||||
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.
|
||||
-replay.disableProgressBar
|
||||
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.
|
||||
-replay.maxDatapointsPerQuery int
|
||||
|
||||
@@ -5,18 +5,9 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// InvalidParamError sets HTTP status code to 400 Bad Request for Prometheus querying APIs when parameters are missing or incorrect,
|
||||
// see https://prometheus.io/docs/prometheus/latest/querying/api/#format-overview.
|
||||
func InvalidParamError(err error) *ErrorWithStatusCode {
|
||||
return &ErrorWithStatusCode{
|
||||
Err: err,
|
||||
StatusCode: http.StatusBadRequest,
|
||||
}
|
||||
}
|
||||
|
||||
// SendPrometheusError sends err to w in Prometheus querying API response format,
|
||||
// and sets HTTP status code to 422 Unprocessable Entity when code is not set,
|
||||
// see https://prometheus.io/docs/prometheus/latest/querying/api/#format-overview for more details.
|
||||
// SendPrometheusError sends err to w in Prometheus querying API response format.
|
||||
//
|
||||
// See https://prometheus.io/docs/prometheus/latest/querying/api/#format-overview for more details
|
||||
func SendPrometheusError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
errStr := err.Error()
|
||||
logHTTPError(r, errStr)
|
||||
|
||||
@@ -2,7 +2,6 @@ package netutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
@@ -42,7 +41,7 @@ func newStatDialFunc(metricPrefix string, dialFunc func(ctx context.Context, net
|
||||
sc.dialsTotal.Inc()
|
||||
if err != nil {
|
||||
sc.dialErrors.Inc()
|
||||
if !TCP6Enabled() && !isTCPv4Addr(addr) && !isUnixSocketDialError(err) {
|
||||
if !TCP6Enabled() && !isTCPv4Addr(addr) {
|
||||
err = fmt.Errorf("%w; try -enableTCP6 command-line flag for dialing ipv6 addresses", err)
|
||||
}
|
||||
return nil, err
|
||||
@@ -53,14 +52,6 @@ func newStatDialFunc(metricPrefix string, dialFunc func(ctx context.Context, net
|
||||
}
|
||||
}
|
||||
|
||||
func isUnixSocketDialError(err error) bool {
|
||||
var opErr *net.OpError
|
||||
if !errors.As(err, &opErr) || opErr.Addr == nil {
|
||||
return false
|
||||
}
|
||||
return opErr.Addr.Network() == "unix"
|
||||
}
|
||||
|
||||
type statDialConn struct {
|
||||
closed atomic.Int32
|
||||
net.Conn
|
||||
|
||||
@@ -1,38 +1,9 @@
|
||||
package netutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsUnixSocketDialError(t *testing.T) {
|
||||
f := func(err error, want bool) {
|
||||
t.Helper()
|
||||
|
||||
got := isUnixSocketDialError(err)
|
||||
if got != want {
|
||||
t.Fatalf("unexpected result; got %v; want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
errDial := errors.New("dial error")
|
||||
f(nil, false)
|
||||
f(errDial, false)
|
||||
f(&net.OpError{Op: "dial", Net: "unix", Err: errDial}, false)
|
||||
f(&net.OpError{Op: "dial", Net: "tcp", Addr: &net.TCPAddr{}, Err: errDial}, false)
|
||||
|
||||
errUnix := &net.OpError{
|
||||
Op: "dial",
|
||||
Net: "unix",
|
||||
Addr: &net.UnixAddr{Name: "exporter.sock", Net: "unix"},
|
||||
Err: errDial,
|
||||
}
|
||||
f(errUnix, true)
|
||||
f(fmt.Errorf("wrapped error: %w", errUnix), true)
|
||||
}
|
||||
|
||||
func TestIsTCPv4Addr(t *testing.T) {
|
||||
f := func(addr string, resultExpected bool) {
|
||||
t.Helper()
|
||||
|
||||
@@ -32,22 +32,7 @@ func (ie *IfExpression) Match(labels []prompb.Label) bool {
|
||||
if ie == nil || len(ie.ies) == 0 {
|
||||
return true
|
||||
}
|
||||
if len(ie.ies) == 1 {
|
||||
return ie.ies[0].Match(labels)
|
||||
}
|
||||
|
||||
metricName := ""
|
||||
metricNameInitialized := false
|
||||
for _, ie := range ie.ies {
|
||||
if ie.metricName != "" {
|
||||
if !metricNameInitialized {
|
||||
metricName = getLabelValue(labels, "__name__")
|
||||
metricNameInitialized = true
|
||||
}
|
||||
if ie.metricName != metricName {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ie.Match(labels) {
|
||||
return true
|
||||
}
|
||||
@@ -181,12 +166,6 @@ func (ie *IfExpression) String() string {
|
||||
type ifExpression struct {
|
||||
s string
|
||||
lfss [][]*labelFilter
|
||||
|
||||
// metricName is the metric name, which must be present in labels in order to match ie.
|
||||
//
|
||||
// It is non empty if ie has a single clause, which matches a particular metric name
|
||||
// and empty otherwise - see getCommonMetricName.
|
||||
metricName string
|
||||
}
|
||||
|
||||
func (ie *ifExpression) String() string {
|
||||
@@ -211,7 +190,6 @@ func (ie *ifExpression) Parse(s string) error {
|
||||
}
|
||||
ie.s = s
|
||||
ie.lfss = lfss
|
||||
ie.metricName = getCommonMetricName(lfss)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -222,7 +200,6 @@ func (ie *ifExpression) parseFromMetricExpr(me *metricsql.MetricExpr) error {
|
||||
}
|
||||
ie.s = string(me.AppendString(nil))
|
||||
ie.lfss = lfss
|
||||
ie.metricName = getCommonMetricName(lfss)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -295,24 +272,6 @@ func metricExprToLabelFilterss(me *metricsql.MetricExpr) ([][]*labelFilter, erro
|
||||
return lfssNew, nil
|
||||
}
|
||||
|
||||
// getCommonMetricName returns the metric name, which is required by lfss.
|
||||
//
|
||||
// Labels with a metric name distinct from the returned one cannot match lfss,
|
||||
// so the returned metric name may be used for fast filtering of non-matching labels.
|
||||
func getCommonMetricName(lfss [][]*labelFilter) string {
|
||||
if len(lfss) != 1 {
|
||||
// Do not extract the metric name from `or` groups, since every group
|
||||
// may require its own metric name.
|
||||
return ""
|
||||
}
|
||||
for _, lf := range lfss[0] {
|
||||
if lf.label == "" && lf.op == "=" && lf.value != "" {
|
||||
return lf.value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// labelFilter contains PromQL filter for `{label op "value"}`
|
||||
type labelFilter struct {
|
||||
label string
|
||||
|
||||
@@ -231,44 +231,3 @@ func TestIfExpressionMismatch(t *testing.T) {
|
||||
f(`'{foo!~"bar|"}'`, `abc`)
|
||||
f(`'{foo!~"bar|"}'`, `abc{foo="bar"}`)
|
||||
}
|
||||
|
||||
func TestIfExpressionParseMetricName(t *testing.T) {
|
||||
f := func(s, metricNameExpected string) {
|
||||
t.Helper()
|
||||
|
||||
var ie ifExpression
|
||||
if err := ie.Parse(s); err != nil {
|
||||
t.Fatalf("cannot parse ifExpression %q: %s", s, err)
|
||||
}
|
||||
if ie.metricName != metricNameExpected {
|
||||
t.Fatalf("unexpected metricName for %q; got %q; want %q", s, ie.metricName, metricNameExpected)
|
||||
}
|
||||
}
|
||||
|
||||
// the metric name is known
|
||||
f(`foo`, "foo")
|
||||
f(`foo{bar="baz"}`, "foo")
|
||||
f(`{__name__="foo"}`, "foo")
|
||||
f(`{__name__="foo",bar="baz"}`, "foo")
|
||||
// the metric name filter isn't at the first position
|
||||
f(`{bar="baz",__name__="foo"}`, "foo")
|
||||
|
||||
// the metric name is unknown
|
||||
f(`{}`, "")
|
||||
f(`{bar="baz"}`, "")
|
||||
// metricsql prepends the common metric name to or-groups where `__name__` is missing entirely,
|
||||
// so both groups below require `foo`, but or-groups are skipped anyway
|
||||
f(`{__name__="foo" or bar="baz"}`, "")
|
||||
f(`{bar="baz" or __name__="foo"}`, "")
|
||||
// the metric name is matched via regexp
|
||||
f(`{__name__=~"foo"}`, "")
|
||||
// the metric name is negated
|
||||
f(`{__name__!="foo"}`, "")
|
||||
f(`{__name__!~"foo"}`, "")
|
||||
// an empty metric name is indistinguishable from the unknown one
|
||||
f(`{__name__=""}`, "")
|
||||
// distinct or-groups require distinct metric names
|
||||
f(`{__name__="foo" or __name__="bar"}`, "")
|
||||
f(`{__name__="foo" or __name__=~"bar.+"}`, "")
|
||||
f(`{__name__=~"foo" or bar="baz"}`, "")
|
||||
}
|
||||
|
||||
@@ -76,91 +76,3 @@ func benchIfExpr(b *testing.B, expr string, labels []prompb.Label) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkIfExpressionAlternatives(b *testing.B) {
|
||||
type benchmarkCase struct {
|
||||
name string
|
||||
ifExprs []string
|
||||
labels []prompb.Label
|
||||
result bool
|
||||
}
|
||||
var testCases []benchmarkCase
|
||||
for _, labelsCount := range []int{16, 48} {
|
||||
testCases = append(testCases, benchmarkCase{
|
||||
name: fmt.Sprintf("single_exact_match/labels_%d", labelsCount),
|
||||
ifExprs: []string{"metric_0"},
|
||||
labels: newIfExpressionBenchmarkLabels("metric_0", labelsCount),
|
||||
result: true,
|
||||
})
|
||||
}
|
||||
for _, alternativesCount := range []int{6, 32} {
|
||||
exact := newIfExpressionBenchmarkExpressions(alternativesCount, "metric_%d")
|
||||
mixedExactCount := alternativesCount * 7 / 8
|
||||
mixed := append([]string{}, newIfExpressionBenchmarkExpressions(mixedExactCount, "metric_%d")...)
|
||||
mixed = append(mixed, newIfExpressionBenchmarkExpressions(alternativesCount-mixedExactCount, `{missing_%d="yes"}`)...)
|
||||
generic := newIfExpressionBenchmarkExpressions(alternativesCount, `{missing_%d="yes"}`)
|
||||
for _, labelsCount := range []int{16, 48} {
|
||||
testCases = append(testCases,
|
||||
benchmarkCase{
|
||||
name: fmt.Sprintf("distinct_exact_%d_miss/labels_%d", alternativesCount, labelsCount),
|
||||
ifExprs: exact,
|
||||
labels: newIfExpressionBenchmarkLabels("other", labelsCount),
|
||||
},
|
||||
benchmarkCase{
|
||||
name: fmt.Sprintf("generic_%d_miss/labels_%d", alternativesCount, labelsCount),
|
||||
ifExprs: generic,
|
||||
labels: newIfExpressionBenchmarkLabels("other", labelsCount),
|
||||
},
|
||||
benchmarkCase{
|
||||
name: fmt.Sprintf("mixed_%d_miss/labels_%d", alternativesCount, labelsCount),
|
||||
ifExprs: mixed,
|
||||
labels: newIfExpressionBenchmarkLabels("other", labelsCount),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
b.Run(tc.name, func(b *testing.B) {
|
||||
ie := mustNewIfExpressionForBenchmark(b, tc.ifExprs)
|
||||
for b.Loop() {
|
||||
if result := ie.Match(tc.labels); result != tc.result {
|
||||
b.Fatalf("unexpected match result; got %v; want %v", result, tc.result)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mustNewIfExpressionForBenchmark(b *testing.B, ifExprs []string) *IfExpression {
|
||||
b.Helper()
|
||||
v := make([]any, len(ifExprs))
|
||||
for i, ifExpr := range ifExprs {
|
||||
v[i] = ifExpr
|
||||
}
|
||||
var ie IfExpression
|
||||
if err := ie.unmarshalFromInterface(v); err != nil {
|
||||
b.Fatalf("cannot unmarshal if expressions: %s", err)
|
||||
}
|
||||
return &ie
|
||||
}
|
||||
|
||||
func newIfExpressionBenchmarkExpressions(n int, format string) []string {
|
||||
ifExprs := make([]string, n)
|
||||
for i := range ifExprs {
|
||||
ifExprs[i] = fmt.Sprintf(format, i)
|
||||
}
|
||||
return ifExprs
|
||||
}
|
||||
|
||||
func newIfExpressionBenchmarkLabels(metricName string, labelsCount int) []prompb.Label {
|
||||
labels := make([]prompb.Label, 0, labelsCount)
|
||||
labels = append(labels, prompb.Label{Name: "__name__", Value: metricName})
|
||||
for i := range labelsCount - 1 {
|
||||
labels = append(labels, prompb.Label{
|
||||
Name: fmt.Sprintf("label_%d", i),
|
||||
Value: fmt.Sprintf("value_%d", i),
|
||||
})
|
||||
}
|
||||
return labels
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/http"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/kubernetes"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/kuma"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/linode"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/marathon"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/nomad"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/openstack"
|
||||
@@ -332,7 +331,6 @@ type ScrapeConfig struct {
|
||||
HTTPSDConfigs []http.SDConfig `yaml:"http_sd_configs,omitempty"`
|
||||
KubernetesSDConfigs []kubernetes.SDConfig `yaml:"kubernetes_sd_configs,omitempty"`
|
||||
KumaSDConfigs []kuma.SDConfig `yaml:"kuma_sd_configs,omitempty"`
|
||||
LinodeSDConfigs []linode.SDConfig `yaml:"linode_sd_configs,omitempty"`
|
||||
MarathonSDConfigs []marathon.SDConfig `yaml:"marathon_sd_configs,omitempty"`
|
||||
NomadSDConfigs []nomad.SDConfig `yaml:"nomad_sd_configs,omitempty"`
|
||||
OpenStackSDConfigs []openstack.SDConfig `yaml:"openstack_sd_configs,omitempty"`
|
||||
@@ -417,9 +415,6 @@ func (sc *ScrapeConfig) mustStop() {
|
||||
for i := range sc.KumaSDConfigs {
|
||||
sc.KumaSDConfigs[i].MustStop()
|
||||
}
|
||||
for i := range sc.LinodeSDConfigs {
|
||||
sc.LinodeSDConfigs[i].MustStop()
|
||||
}
|
||||
for i := range sc.NomadSDConfigs {
|
||||
sc.NomadSDConfigs[i].MustStop()
|
||||
}
|
||||
@@ -768,16 +763,6 @@ func (cfg *Config) getKumaSDScrapeWork(prev []*ScrapeWork) []*ScrapeWork {
|
||||
return cfg.getScrapeWorkGeneric(visitConfigs, "kuma_sd_config", prev)
|
||||
}
|
||||
|
||||
// getLinodeSDScrapeWork returns `linode_sd_configs` ScrapeWork from cfg.
|
||||
func (cfg *Config) getLinodeSDScrapeWork(prev []*ScrapeWork) []*ScrapeWork {
|
||||
visitConfigs := func(sc *ScrapeConfig, visitor func(sdc targetLabelsGetter)) {
|
||||
for i := range sc.LinodeSDConfigs {
|
||||
visitor(&sc.LinodeSDConfigs[i])
|
||||
}
|
||||
}
|
||||
return cfg.getScrapeWorkGeneric(visitConfigs, "linode_sd_config", prev)
|
||||
}
|
||||
|
||||
// getMarathonSDScrapeWork returns `marathon_sd_configs` ScrapeWork from cfg.
|
||||
func (cfg *Config) getMarathonSDScrapeWork(prev []*ScrapeWork) []*ScrapeWork {
|
||||
visitConfigs := func(sc *ScrapeConfig, visitor func(sdc targetLabelsGetter)) {
|
||||
|
||||
@@ -37,9 +37,8 @@ type containerNetworkSettings struct {
|
||||
}
|
||||
|
||||
type containerNetwork struct {
|
||||
GlobalIPv6Address string
|
||||
IPAddress string
|
||||
NetworkID string
|
||||
IPAddress string
|
||||
NetworkID string
|
||||
}
|
||||
|
||||
func getContainersLabels(cfg *apiConfig) ([]*promutil.Labels, error) {
|
||||
@@ -119,18 +118,14 @@ func addContainersLabels(containers []container, networkLabels map[string]*promu
|
||||
networks = map[string]containerNetwork{firstNetworkMode: firstNetwork}
|
||||
}
|
||||
for _, n := range networks {
|
||||
ipAddress := n.IPAddress
|
||||
if len(ipAddress) == 0 {
|
||||
ipAddress = n.GlobalIPv6Address
|
||||
}
|
||||
var added bool
|
||||
for _, p := range c.Ports {
|
||||
if p.Type != "tcp" {
|
||||
continue
|
||||
}
|
||||
m := promutil.NewLabels(16)
|
||||
m.Add("__address__", discoveryutil.JoinHostPort(ipAddress, p.PrivatePort))
|
||||
m.Add("__meta_docker_network_ip", ipAddress)
|
||||
m.Add("__address__", discoveryutil.JoinHostPort(n.IPAddress, p.PrivatePort))
|
||||
m.Add("__meta_docker_network_ip", n.IPAddress)
|
||||
m.Add("__meta_docker_port_private", strconv.Itoa(p.PrivatePort))
|
||||
if p.PublicPort > 0 {
|
||||
m.Add("__meta_docker_port_public", strconv.Itoa(p.PublicPort))
|
||||
@@ -146,11 +141,11 @@ func addContainersLabels(containers []container, networkLabels map[string]*promu
|
||||
// Use fallback port when no exposed ports are available or if all are non-TCP
|
||||
addr := hostNetworkingHost
|
||||
if c.HostConfig.NetworkMode != "host" {
|
||||
addr = discoveryutil.JoinHostPort(ipAddress, defaultPort)
|
||||
addr = discoveryutil.JoinHostPort(n.IPAddress, defaultPort)
|
||||
}
|
||||
m := promutil.NewLabels(16)
|
||||
m.Add("__address__", addr)
|
||||
m.Add("__meta_docker_network_ip", ipAddress)
|
||||
m.Add("__meta_docker_network_ip", n.IPAddress)
|
||||
addCommonLabels(m, c, networkLabels[n.NetworkID])
|
||||
// Remove possible duplicate labels, which can appear after addCommonLabels() call
|
||||
m.RemoveDuplicates()
|
||||
|
||||
@@ -431,96 +431,6 @@ func TestAddContainerLabels(t *testing.T) {
|
||||
}),
|
||||
}
|
||||
f(c, networkLabels, labelssExpected)
|
||||
|
||||
data = []byte(`[
|
||||
{
|
||||
"Name": "bridge",
|
||||
"Id": "1dd8d1a8bef59943345c7231d7ce8268333ff5a8c5b3c94881e6b4742b447634",
|
||||
"Created": "2021-03-18T14:36:04.290821903+08:00",
|
||||
"Scope": "local",
|
||||
"Driver": "bridge",
|
||||
"EnableIPv6": true,
|
||||
"IPAM": {
|
||||
"Driver": "default",
|
||||
"Options": null,
|
||||
"Config": [
|
||||
{
|
||||
"Subnet": "fc00::/64",
|
||||
"Gateway": "fc00::1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Internal": false,
|
||||
"Attachable": false,
|
||||
"Ingress": false,
|
||||
"ConfigFrom": {
|
||||
"Network": ""
|
||||
},
|
||||
"ConfigOnly": false,
|
||||
"Containers": {},
|
||||
"Options": {
|
||||
"com.docker.network.bridge.default_bridge": "true",
|
||||
"com.docker.network.bridge.enable_icc": "true",
|
||||
"com.docker.network.bridge.enable_ip_masquerade": "true",
|
||||
"com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
|
||||
"com.docker.network.bridge.name": "docker0",
|
||||
"com.docker.network.driver.mtu": "1500"
|
||||
},
|
||||
"Labels": {}
|
||||
}
|
||||
]`)
|
||||
networks, err = parseNetworks(data)
|
||||
if err != nil {
|
||||
t.Fatalf("fail to parse networks: %v", err)
|
||||
}
|
||||
networkLabels = getNetworkLabelsByNetworkID(networks)
|
||||
|
||||
// NetworkMode != host
|
||||
c = container{
|
||||
ID: "90bc3b31aa13da5c0b11af2e228d54b38428a84e25d4e249ae9e9c95e51a0700",
|
||||
Names: []string{"/crow-server"},
|
||||
Labels: map[string]string{
|
||||
"com.docker.compose.config-hash": "c9f0bd5bb31921f94cff367d819a30a0cc08d4399080897a6c5cd74b983156ec",
|
||||
"com.docker.compose.container-number": "1",
|
||||
"com.docker.compose.oneoff": "False",
|
||||
"com.docker.compose.project": "crowserver",
|
||||
"com.docker.compose.service": "crow-server",
|
||||
"com.docker.compose.version": "1.11.2",
|
||||
},
|
||||
HostConfig: containerHostConfig{
|
||||
NetworkMode: "bridge",
|
||||
},
|
||||
NetworkSettings: containerNetworkSettings{
|
||||
Networks: map[string]containerNetwork{
|
||||
"bridge": {
|
||||
GlobalIPv6Address: "fc00::2",
|
||||
NetworkID: "1dd8d1a8bef59943345c7231d7ce8268333ff5a8c5b3c94881e6b4742b447634",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
labelssExpected = []*promutil.Labels{
|
||||
promutil.NewLabelsFromMap(map[string]string{
|
||||
"__address__": "[fc00::2]:8012",
|
||||
"__meta_docker_container_id": "90bc3b31aa13da5c0b11af2e228d54b38428a84e25d4e249ae9e9c95e51a0700",
|
||||
"__meta_docker_container_label_com_docker_compose_config_hash": "c9f0bd5bb31921f94cff367d819a30a0cc08d4399080897a6c5cd74b983156ec",
|
||||
"__meta_docker_container_label_com_docker_compose_container_number": "1",
|
||||
"__meta_docker_container_label_com_docker_compose_oneoff": "False",
|
||||
"__meta_docker_container_label_com_docker_compose_project": "crowserver",
|
||||
"__meta_docker_container_label_com_docker_compose_service": "crow-server",
|
||||
"__meta_docker_container_label_com_docker_compose_version": "1.11.2",
|
||||
"__meta_docker_container_name": "/crow-server",
|
||||
"__meta_docker_container_network_mode": "bridge",
|
||||
"__meta_docker_network_id": "1dd8d1a8bef59943345c7231d7ce8268333ff5a8c5b3c94881e6b4742b447634",
|
||||
"__meta_docker_network_ingress": "false",
|
||||
"__meta_docker_network_internal": "false",
|
||||
"__meta_docker_network_ip": "fc00::2",
|
||||
"__meta_docker_network_name": "bridge",
|
||||
"__meta_docker_network_scope": "local",
|
||||
}),
|
||||
}
|
||||
f(c, networkLabels, labelssExpected)
|
||||
|
||||
}
|
||||
|
||||
func TestDockerMultiNetworkLabels(t *testing.T) {
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
package linode
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutil"
|
||||
)
|
||||
|
||||
var configMap = discoveryutil.NewConfigMap()
|
||||
|
||||
type apiConfig struct {
|
||||
client *discoveryutil.Client
|
||||
port int
|
||||
tagSeparator string
|
||||
region string
|
||||
}
|
||||
|
||||
func newAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) {
|
||||
ac, err := sdc.HTTPClientConfig.NewConfig(baseDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse auth config: %w", err)
|
||||
}
|
||||
|
||||
apiServer := sdc.Server
|
||||
if apiServer == "" {
|
||||
apiServer = "https://api.linode.com"
|
||||
}
|
||||
if !strings.Contains(apiServer, "://") {
|
||||
scheme := "http"
|
||||
if sdc.HTTPClientConfig.TLSConfig != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
apiServer = scheme + "://" + apiServer
|
||||
}
|
||||
proxyAC, err := sdc.ProxyClientConfig.NewConfig(baseDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse proxy auth config: %w", err)
|
||||
}
|
||||
client, err := discoveryutil.NewClient(apiServer, ac, sdc.ProxyURL, proxyAC, &sdc.HTTPClientConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create HTTP client for %q: %w", apiServer, err)
|
||||
}
|
||||
|
||||
port := sdc.Port
|
||||
if port == 0 {
|
||||
port = 80
|
||||
}
|
||||
tagSeparator := sdc.TagSeparator
|
||||
if tagSeparator == "" {
|
||||
tagSeparator = ","
|
||||
}
|
||||
return &apiConfig{
|
||||
client: client,
|
||||
port: port,
|
||||
tagSeparator: tagSeparator,
|
||||
region: sdc.Region,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) {
|
||||
v, err := configMap.Get(sdc, func() (any, error) { return newAPIConfig(sdc, baseDir) })
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v.(*apiConfig), nil
|
||||
}
|
||||
|
||||
// Linode list API types. See https://www.linode.com/docs/api/
|
||||
|
||||
type instance struct {
|
||||
ID int `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Group string `json:"group"`
|
||||
Status string `json:"status"`
|
||||
Type string `json:"type"`
|
||||
IPv4 []string `json:"ipv4"`
|
||||
IPv6 string `json:"ipv6"`
|
||||
Image string `json:"image"`
|
||||
Region string `json:"region"`
|
||||
Specs specs `json:"specs"`
|
||||
Backups backups `json:"backups"`
|
||||
Hypervisor string `json:"hypervisor"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
type specs struct {
|
||||
Disk int `json:"disk"`
|
||||
Memory int `json:"memory"`
|
||||
VCPUs int `json:"vcpus"`
|
||||
GPUs int `json:"gpus"`
|
||||
Transfer int `json:"transfer"`
|
||||
}
|
||||
|
||||
type backups struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type ipAddress struct {
|
||||
Address string `json:"address"`
|
||||
Public bool `json:"public"`
|
||||
RDNS string `json:"rdns"`
|
||||
}
|
||||
|
||||
type ipv6Range struct {
|
||||
Range string `json:"range"`
|
||||
Prefix int `json:"prefix"`
|
||||
RouteTarget string `json:"route_target"`
|
||||
}
|
||||
|
||||
type listResponse struct {
|
||||
Data json.RawMessage `json:"data"`
|
||||
Page int `json:"page"`
|
||||
Pages int `json:"pages"`
|
||||
}
|
||||
|
||||
const (
|
||||
instancesAPIPath = "/v4/linode/instances"
|
||||
ipAddressesPath = "/v4/networking/ips"
|
||||
ipv6RangesAPIPath = "/v4/networking/ipv6/ranges"
|
||||
pageSize = 500
|
||||
)
|
||||
|
||||
func getInstances(cfg *apiConfig) ([]instance, error) {
|
||||
var instances []instance
|
||||
err := listAllPages(cfg, instancesAPIPath, func(data json.RawMessage) error {
|
||||
var page []instance
|
||||
if err := json.Unmarshal(data, &page); err != nil {
|
||||
return fmt.Errorf("cannot parse linode instances: %w", err)
|
||||
}
|
||||
instances = append(instances, page...)
|
||||
return nil
|
||||
})
|
||||
return instances, err
|
||||
}
|
||||
|
||||
func getIPAddresses(cfg *apiConfig) ([]ipAddress, error) {
|
||||
var ips []ipAddress
|
||||
err := listAllPages(cfg, ipAddressesPath, func(data json.RawMessage) error {
|
||||
var page []ipAddress
|
||||
if err := json.Unmarshal(data, &page); err != nil {
|
||||
return fmt.Errorf("cannot parse linode ip addresses: %w", err)
|
||||
}
|
||||
ips = append(ips, page...)
|
||||
return nil
|
||||
})
|
||||
return ips, err
|
||||
}
|
||||
|
||||
func getIPv6Ranges(cfg *apiConfig) ([]ipv6Range, error) {
|
||||
var ranges []ipv6Range
|
||||
err := listAllPages(cfg, ipv6RangesAPIPath, func(data json.RawMessage) error {
|
||||
var page []ipv6Range
|
||||
if err := json.Unmarshal(data, &page); err != nil {
|
||||
return fmt.Errorf("cannot parse linode ipv6 ranges: %w", err)
|
||||
}
|
||||
ranges = append(ranges, page...)
|
||||
return nil
|
||||
})
|
||||
return ranges, err
|
||||
}
|
||||
|
||||
func listAllPages(cfg *apiConfig, apiPath string, consume func(json.RawMessage) error) error {
|
||||
page := 1
|
||||
for {
|
||||
path := fmt.Sprintf("%s?page=%d&page_size=%d", apiPath, page, pageSize)
|
||||
data, err := cfg.client.GetAPIResponseWithReqParams(path, cfg.regionFilterHeader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query linode api %q: %w", path, err)
|
||||
}
|
||||
var resp listResponse
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return fmt.Errorf("cannot parse linode api response from %q: %w; data=%q", path, err, data)
|
||||
}
|
||||
if err := consume(resp.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Pages == 0 || page >= resp.Pages {
|
||||
return nil
|
||||
}
|
||||
page++
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *apiConfig) regionFilterHeader(req *http.Request) {
|
||||
if cfg.region == "" {
|
||||
return
|
||||
}
|
||||
// Same filter as Prometheus linode_sd: https://www.linode.com/docs/api/#filtering-and-sorting
|
||||
req.Header.Set("X-Filter", fmt.Sprintf(`{"region": "%s"}`, cfg.region))
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package linode
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseListInstances(t *testing.T) {
|
||||
data := []byte(`{
|
||||
"data": [
|
||||
{
|
||||
"id": 26838044,
|
||||
"label": "prometheus-linode-sd-exporter-1",
|
||||
"group": "",
|
||||
"status": "running",
|
||||
"type": "g6-standard-2",
|
||||
"ipv4": ["45.33.82.151", "96.126.108.16"],
|
||||
"ipv6": "2600:3c03::f03c:92ff:fe1a:1382/128",
|
||||
"image": "linode/arch",
|
||||
"region": "us-east",
|
||||
"specs": {"disk": 81920, "memory": 4096, "vcpus": 2, "gpus": 0, "transfer": 4000},
|
||||
"backups": {"enabled": false},
|
||||
"hypervisor": "kvm",
|
||||
"tags": ["monitoring"]
|
||||
}
|
||||
],
|
||||
"page": 1,
|
||||
"pages": 1,
|
||||
"results": 1
|
||||
}`)
|
||||
var resp listResponse
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
t.Fatalf("cannot unmarshal list response: %s", err)
|
||||
}
|
||||
if resp.Page != 1 || resp.Pages != 1 {
|
||||
t.Fatalf("unexpected pagination: page=%d pages=%d", resp.Page, resp.Pages)
|
||||
}
|
||||
var instances []instance
|
||||
if err := json.Unmarshal(resp.Data, &instances); err != nil {
|
||||
t.Fatalf("cannot unmarshal instances: %s", err)
|
||||
}
|
||||
if len(instances) != 1 {
|
||||
t.Fatalf("unexpected instances len: %d", len(instances))
|
||||
}
|
||||
inst := instances[0]
|
||||
if inst.ID != 26838044 || inst.Label != "prometheus-linode-sd-exporter-1" {
|
||||
t.Fatalf("unexpected instance: %+v", inst)
|
||||
}
|
||||
if len(inst.IPv4) != 2 || inst.IPv4[0] != "45.33.82.151" {
|
||||
t.Fatalf("unexpected ipv4: %v", inst.IPv4)
|
||||
}
|
||||
if inst.Specs.Disk != 81920 || inst.Specs.Memory != 4096 {
|
||||
t.Fatalf("unexpected specs: %+v", inst.Specs)
|
||||
}
|
||||
if inst.Backups.Enabled {
|
||||
t.Fatalf("expected backups disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIPAddressesNullRDNS(t *testing.T) {
|
||||
data := []byte(`[
|
||||
{
|
||||
"address": "192.168.148.94",
|
||||
"public": false,
|
||||
"rdns": null
|
||||
},
|
||||
{
|
||||
"address": "66.228.47.103",
|
||||
"public": true,
|
||||
"rdns": "li328-103.members.linode.com"
|
||||
}
|
||||
]`)
|
||||
var ips []ipAddress
|
||||
if err := json.Unmarshal(data, &ips); err != nil {
|
||||
t.Fatalf("cannot unmarshal ips: %s", err)
|
||||
}
|
||||
if len(ips) != 2 {
|
||||
t.Fatalf("unexpected len: %d", len(ips))
|
||||
}
|
||||
if ips[0].RDNS != "" {
|
||||
t.Fatalf("expected empty rdns for null, got %q", ips[0].RDNS)
|
||||
}
|
||||
if ips[1].RDNS != "li328-103.members.linode.com" {
|
||||
t.Fatalf("unexpected rdns: %q", ips[1].RDNS)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIPv6Ranges(t *testing.T) {
|
||||
data := []byte(`[
|
||||
{
|
||||
"range": "2600:3c03:e000:123::",
|
||||
"prefix": 64,
|
||||
"route_target": "2600:3c03::f03c:92ff:fe1a:fb4c"
|
||||
}
|
||||
]`)
|
||||
var ranges []ipv6Range
|
||||
if err := json.Unmarshal(data, &ranges); err != nil {
|
||||
t.Fatalf("cannot unmarshal ranges: %s", err)
|
||||
}
|
||||
if len(ranges) != 1 || ranges[0].Prefix != 64 {
|
||||
t.Fatalf("unexpected ranges: %+v", ranges)
|
||||
}
|
||||
if ranges[0].RouteTarget != "2600:3c03::f03c:92ff:fe1a:fb4c" {
|
||||
t.Fatalf("unexpected route_target: %q", ranges[0].RouteTarget)
|
||||
}
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
package linode
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/proxy"
|
||||
)
|
||||
|
||||
// SDCheckInterval defines interval for targets refresh.
|
||||
var SDCheckInterval = flag.Duration("promscrape.linodeSDCheckInterval", time.Minute, "Interval for checking for changes in Linode. "+
|
||||
"This works only if linode_sd_configs is configured in '-promscrape.config' file. "+
|
||||
"See https://docs.victoriametrics.com/victoriametrics/sd_configs/#linode_sd_configs for details")
|
||||
|
||||
// failuresTotal counts failed Linode SD refresh attempts.
|
||||
// Analogous to Prometheus prometheus_sd_linode_failures_total.
|
||||
var failuresTotal = metrics.NewCounter(`vm_promscrape_discovery_linode_failures_total`)
|
||||
|
||||
// SDConfig represents service discovery config for Linode.
|
||||
//
|
||||
// See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#linode_sd_config
|
||||
type SDConfig struct {
|
||||
Server string `yaml:"server,omitempty"`
|
||||
Port int `yaml:"port,omitempty"`
|
||||
TagSeparator string `yaml:"tag_separator,omitempty"`
|
||||
Region string `yaml:"region,omitempty"`
|
||||
HTTPClientConfig promauth.HTTPClientConfig `yaml:",inline"`
|
||||
ProxyURL *proxy.URL `yaml:"proxy_url,omitempty"`
|
||||
ProxyClientConfig promauth.ProxyClientConfig `yaml:",inline"`
|
||||
// refresh_interval is obtained from `-promscrape.linodeSDCheckInterval` command-line option.
|
||||
}
|
||||
|
||||
// GetLabels returns Linode instance labels according to sdc.
|
||||
func (sdc *SDConfig) GetLabels(baseDir string) ([]*promutil.Labels, error) {
|
||||
cfg, err := getAPIConfig(sdc, baseDir)
|
||||
if err != nil {
|
||||
failuresTotal.Inc()
|
||||
return nil, fmt.Errorf("cannot get API config: %w", err)
|
||||
}
|
||||
instances, err := getInstances(cfg)
|
||||
if err != nil {
|
||||
failuresTotal.Inc()
|
||||
return nil, err
|
||||
}
|
||||
detailedIPs, err := getIPAddresses(cfg)
|
||||
if err != nil {
|
||||
failuresTotal.Inc()
|
||||
return nil, err
|
||||
}
|
||||
ipv6Ranges, err := getIPv6Ranges(cfg)
|
||||
if err != nil {
|
||||
failuresTotal.Inc()
|
||||
return nil, err
|
||||
}
|
||||
return addInstanceLabels(instances, detailedIPs, ipv6Ranges, cfg.port, cfg.tagSeparator), nil
|
||||
}
|
||||
|
||||
// MustStop stops further usage for sdc.
|
||||
func (sdc *SDConfig) MustStop() {
|
||||
v := configMap.Delete(sdc)
|
||||
if v != nil {
|
||||
cfg := v.(*apiConfig)
|
||||
cfg.client.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// addInstanceLabels builds target labels from Linode API data.
|
||||
// Label semantics match Prometheus linode_sd: https://prometheus.io/docs/prometheus/latest/configuration/configuration/#linode_sd_config
|
||||
func addInstanceLabels(instances []instance, detailedIPs []ipAddress, ipv6RangeList []ipv6Range, port int, tagSeparator string) []*promutil.Labels {
|
||||
ms := make([]*promutil.Labels, 0, len(instances))
|
||||
for _, inst := range instances {
|
||||
if len(inst.IPv4) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var (
|
||||
privateIPv4, publicIPv4, publicIPv6 string
|
||||
privateIPv4RDNS, publicIPv4RDNS, publicIPv6RDNS string
|
||||
extraIPs, ipv6Ranges []string
|
||||
)
|
||||
|
||||
for _, ip := range inst.IPv4 {
|
||||
for _, detailedIP := range detailedIPs {
|
||||
if detailedIP.Address != ip {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case detailedIP.Public && publicIPv4 == "":
|
||||
publicIPv4 = detailedIP.Address
|
||||
if detailedIP.RDNS != "" && detailedIP.RDNS != "null" {
|
||||
publicIPv4RDNS = detailedIP.RDNS
|
||||
}
|
||||
case !detailedIP.Public && privateIPv4 == "":
|
||||
privateIPv4 = detailedIP.Address
|
||||
if detailedIP.RDNS != "" && detailedIP.RDNS != "null" {
|
||||
privateIPv4RDNS = detailedIP.RDNS
|
||||
}
|
||||
default:
|
||||
extraIPs = append(extraIPs, detailedIP.Address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if inst.IPv6 != "" {
|
||||
slaac := strings.Split(inst.IPv6, "/")[0]
|
||||
for _, detailedIP := range detailedIPs {
|
||||
if detailedIP.Address != slaac {
|
||||
continue
|
||||
}
|
||||
publicIPv6 = detailedIP.Address
|
||||
if detailedIP.RDNS != "" && detailedIP.RDNS != "null" {
|
||||
publicIPv6RDNS = detailedIP.RDNS
|
||||
}
|
||||
}
|
||||
for _, r := range ipv6RangeList {
|
||||
if r.RouteTarget != slaac {
|
||||
continue
|
||||
}
|
||||
ipv6Ranges = append(ipv6Ranges, fmt.Sprintf("%s/%d", r.Range, r.Prefix))
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer public IPv4 for __address__ (Prometheus default). Fall back to private
|
||||
// when the instance has no public IPv4 so we never emit empty-host targets like ":80".
|
||||
addrHost := publicIPv4
|
||||
if addrHost == "" {
|
||||
addrHost = privateIPv4
|
||||
}
|
||||
if addrHost == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
backupsStatus := "disabled"
|
||||
if inst.Backups.Enabled {
|
||||
backupsStatus = "enabled"
|
||||
}
|
||||
|
||||
m := promutil.NewLabels(28)
|
||||
m.Add("__address__", discoveryutil.JoinHostPort(addrHost, port))
|
||||
m.Add("__meta_linode_instance_id", strconv.Itoa(inst.ID))
|
||||
m.Add("__meta_linode_instance_label", inst.Label)
|
||||
m.Add("__meta_linode_image", inst.Image)
|
||||
m.Add("__meta_linode_private_ipv4", privateIPv4)
|
||||
m.Add("__meta_linode_public_ipv4", publicIPv4)
|
||||
m.Add("__meta_linode_public_ipv6", publicIPv6)
|
||||
m.Add("__meta_linode_private_ipv4_rdns", privateIPv4RDNS)
|
||||
m.Add("__meta_linode_public_ipv4_rdns", publicIPv4RDNS)
|
||||
m.Add("__meta_linode_public_ipv6_rdns", publicIPv6RDNS)
|
||||
m.Add("__meta_linode_region", inst.Region)
|
||||
m.Add("__meta_linode_type", inst.Type)
|
||||
m.Add("__meta_linode_status", inst.Status)
|
||||
m.Add("__meta_linode_group", inst.Group)
|
||||
m.Add("__meta_linode_gpus", strconv.Itoa(inst.Specs.GPUs))
|
||||
m.Add("__meta_linode_hypervisor", inst.Hypervisor)
|
||||
m.Add("__meta_linode_backups", backupsStatus)
|
||||
// Specs disk/memory/transfer are reported in MiB by the API; Prometheus converts with << 20.
|
||||
m.Add("__meta_linode_specs_disk_bytes", strconv.FormatInt(int64(inst.Specs.Disk)<<20, 10))
|
||||
m.Add("__meta_linode_specs_memory_bytes", strconv.FormatInt(int64(inst.Specs.Memory)<<20, 10))
|
||||
m.Add("__meta_linode_specs_vcpus", strconv.Itoa(inst.Specs.VCPUs))
|
||||
m.Add("__meta_linode_specs_transfer_bytes", strconv.FormatInt(int64(inst.Specs.Transfer)<<20, 10))
|
||||
|
||||
if len(inst.Tags) > 0 {
|
||||
// Surround with separator so relabel regexes do not depend on tag position.
|
||||
tags := tagSeparator + strings.Join(inst.Tags, tagSeparator) + tagSeparator
|
||||
m.Add("__meta_linode_tags", tags)
|
||||
}
|
||||
if len(extraIPs) > 0 {
|
||||
ips := tagSeparator + strings.Join(extraIPs, tagSeparator) + tagSeparator
|
||||
m.Add("__meta_linode_extra_ips", ips)
|
||||
}
|
||||
if len(ipv6Ranges) > 0 {
|
||||
ranges := tagSeparator + strings.Join(ipv6Ranges, tagSeparator) + tagSeparator
|
||||
m.Add("__meta_linode_ipv6_ranges", ranges)
|
||||
}
|
||||
|
||||
ms = append(ms, m)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
@@ -1,270 +0,0 @@
|
||||
package linode
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutil"
|
||||
)
|
||||
|
||||
func TestAddInstanceLabels(t *testing.T) {
|
||||
f := func(instances []instance, detailedIPs []ipAddress, ipv6Ranges []ipv6Range, labelssExpected []*promutil.Labels) {
|
||||
t.Helper()
|
||||
labelss := addInstanceLabels(instances, detailedIPs, ipv6Ranges, 80, ",")
|
||||
discoveryutil.TestEqualLabelss(t, labelss, labelssExpected)
|
||||
}
|
||||
|
||||
// Fixture shapes mirror Prometheus discovery/linode testdata (no_region_filter).
|
||||
instances := []instance{
|
||||
{
|
||||
ID: 26838044, Label: "prometheus-linode-sd-exporter-1", Status: "running",
|
||||
Type: "g6-standard-2", Image: "linode/arch", Region: "us-east", Hypervisor: "kvm",
|
||||
IPv4: []string{"45.33.82.151", "96.126.108.16", "192.168.170.51", "192.168.201.25"},
|
||||
IPv6: "2600:3c03::f03c:92ff:fe1a:1382/128",
|
||||
Tags: []string{"monitoring"},
|
||||
Specs: specs{Disk: 81920, Memory: 4096, VCPUs: 2, GPUs: 0, Transfer: 4000},
|
||||
},
|
||||
{
|
||||
ID: 26848419, Label: "prometheus-linode-sd-exporter-2", Status: "running",
|
||||
Type: "g6-standard-2", Image: "linode/debian10", Region: "eu-west", Hypervisor: "kvm",
|
||||
IPv4: []string{"139.162.196.43"},
|
||||
IPv6: "2a01:7e00::f03c:92ff:fe1a:9976/128",
|
||||
Tags: []string{"monitoring"},
|
||||
Specs: specs{Disk: 81920, Memory: 4096, VCPUs: 2, GPUs: 0, Transfer: 4000},
|
||||
},
|
||||
{
|
||||
ID: 26837938, Label: "prometheus-linode-sd-exporter-3", Status: "running",
|
||||
Type: "g6-standard-1", Image: "linode/ubuntu20.04", Region: "ca-central", Hypervisor: "kvm",
|
||||
IPv4: []string{"192.53.120.25"},
|
||||
IPv6: "2600:3c04::f03c:92ff:fe1a:fb68/128",
|
||||
Tags: []string{"monitoring"},
|
||||
Specs: specs{Disk: 51200, Memory: 2048, VCPUs: 1, GPUs: 0, Transfer: 2000},
|
||||
},
|
||||
{
|
||||
ID: 26837992, Label: "prometheus-linode-sd-exporter-4", Status: "running",
|
||||
Type: "g6-nanode-1", Image: "linode/ubuntu20.04", Region: "us-east", Hypervisor: "kvm",
|
||||
IPv4: []string{"66.228.47.103", "172.104.18.104", "192.168.148.94"},
|
||||
IPv6: "2600:3c03::f03c:92ff:fe1a:fb4c/128",
|
||||
Tags: []string{"monitoring"},
|
||||
Specs: specs{Disk: 25600, Memory: 1024, VCPUs: 1, GPUs: 0, Transfer: 1000},
|
||||
},
|
||||
// No IPv4 — must be skipped (Prometheus parity).
|
||||
{
|
||||
ID: 999, Label: "no-ipv4", Status: "running",
|
||||
Type: "g6-nanode-1", Image: "linode/ubuntu20.04", Region: "us-east",
|
||||
IPv4: nil, IPv6: "2600:3c03::1/128",
|
||||
},
|
||||
}
|
||||
|
||||
detailedIPs := []ipAddress{
|
||||
{Address: "192.53.120.25", Public: true, RDNS: "li2216-25.members.linode.com"},
|
||||
{Address: "66.228.47.103", Public: true, RDNS: "li328-103.members.linode.com"},
|
||||
{Address: "172.104.18.104", Public: true, RDNS: "li1832-104.members.linode.com"},
|
||||
{Address: "192.168.148.94", Public: false, RDNS: ""},
|
||||
{Address: "192.168.170.51", Public: false, RDNS: ""},
|
||||
{Address: "96.126.108.16", Public: true, RDNS: "li365-16.members.linode.com"},
|
||||
{Address: "45.33.82.151", Public: true, RDNS: "li1028-151.members.linode.com"},
|
||||
{Address: "192.168.201.25", Public: false, RDNS: ""},
|
||||
{Address: "139.162.196.43", Public: true, RDNS: "li1359-43.members.linode.com"},
|
||||
{Address: "2600:3c03::f03c:92ff:fe1a:1382", Public: true, RDNS: ""},
|
||||
{Address: "2a01:7e00::f03c:92ff:fe1a:9976", Public: true, RDNS: ""},
|
||||
{Address: "2600:3c04::f03c:92ff:fe1a:fb68", Public: true, RDNS: ""},
|
||||
{Address: "2600:3c03::f03c:92ff:fe1a:fb4c", Public: true, RDNS: ""},
|
||||
}
|
||||
|
||||
ipv6Ranges := []ipv6Range{
|
||||
{Range: "2600:3c03:e000:123::", Prefix: 64, RouteTarget: "2600:3c03::f03c:92ff:fe1a:fb4c"},
|
||||
{Range: "2600:3c04:e001:456::", Prefix: 64, RouteTarget: "2600:3c04::f03c:92ff:fe1a:fb68"},
|
||||
}
|
||||
|
||||
// Expected values match prometheus/discovery/linode/linode_test.go "no_region" case.
|
||||
f(instances, detailedIPs, ipv6Ranges, []*promutil.Labels{
|
||||
promutil.NewLabelsFromMap(map[string]string{
|
||||
"__address__": "45.33.82.151:80",
|
||||
"__meta_linode_instance_id": "26838044",
|
||||
"__meta_linode_instance_label": "prometheus-linode-sd-exporter-1",
|
||||
"__meta_linode_image": "linode/arch",
|
||||
"__meta_linode_private_ipv4": "192.168.170.51",
|
||||
"__meta_linode_public_ipv4": "45.33.82.151",
|
||||
"__meta_linode_public_ipv6": "2600:3c03::f03c:92ff:fe1a:1382",
|
||||
"__meta_linode_private_ipv4_rdns": "",
|
||||
"__meta_linode_public_ipv4_rdns": "li1028-151.members.linode.com",
|
||||
"__meta_linode_public_ipv6_rdns": "",
|
||||
"__meta_linode_region": "us-east",
|
||||
"__meta_linode_type": "g6-standard-2",
|
||||
"__meta_linode_status": "running",
|
||||
"__meta_linode_tags": ",monitoring,",
|
||||
"__meta_linode_group": "",
|
||||
"__meta_linode_gpus": "0",
|
||||
"__meta_linode_hypervisor": "kvm",
|
||||
"__meta_linode_backups": "disabled",
|
||||
"__meta_linode_specs_disk_bytes": "85899345920",
|
||||
"__meta_linode_specs_memory_bytes": "4294967296",
|
||||
"__meta_linode_specs_vcpus": "2",
|
||||
"__meta_linode_specs_transfer_bytes": "4194304000",
|
||||
"__meta_linode_extra_ips": ",96.126.108.16,192.168.201.25,",
|
||||
}),
|
||||
promutil.NewLabelsFromMap(map[string]string{
|
||||
"__address__": "139.162.196.43:80",
|
||||
"__meta_linode_instance_id": "26848419",
|
||||
"__meta_linode_instance_label": "prometheus-linode-sd-exporter-2",
|
||||
"__meta_linode_image": "linode/debian10",
|
||||
"__meta_linode_private_ipv4": "",
|
||||
"__meta_linode_public_ipv4": "139.162.196.43",
|
||||
"__meta_linode_public_ipv6": "2a01:7e00::f03c:92ff:fe1a:9976",
|
||||
"__meta_linode_private_ipv4_rdns": "",
|
||||
"__meta_linode_public_ipv4_rdns": "li1359-43.members.linode.com",
|
||||
"__meta_linode_public_ipv6_rdns": "",
|
||||
"__meta_linode_region": "eu-west",
|
||||
"__meta_linode_type": "g6-standard-2",
|
||||
"__meta_linode_status": "running",
|
||||
"__meta_linode_tags": ",monitoring,",
|
||||
"__meta_linode_group": "",
|
||||
"__meta_linode_gpus": "0",
|
||||
"__meta_linode_hypervisor": "kvm",
|
||||
"__meta_linode_backups": "disabled",
|
||||
"__meta_linode_specs_disk_bytes": "85899345920",
|
||||
"__meta_linode_specs_memory_bytes": "4294967296",
|
||||
"__meta_linode_specs_vcpus": "2",
|
||||
"__meta_linode_specs_transfer_bytes": "4194304000",
|
||||
}),
|
||||
promutil.NewLabelsFromMap(map[string]string{
|
||||
"__address__": "192.53.120.25:80",
|
||||
"__meta_linode_instance_id": "26837938",
|
||||
"__meta_linode_instance_label": "prometheus-linode-sd-exporter-3",
|
||||
"__meta_linode_image": "linode/ubuntu20.04",
|
||||
"__meta_linode_private_ipv4": "",
|
||||
"__meta_linode_public_ipv4": "192.53.120.25",
|
||||
"__meta_linode_public_ipv6": "2600:3c04::f03c:92ff:fe1a:fb68",
|
||||
"__meta_linode_private_ipv4_rdns": "",
|
||||
"__meta_linode_public_ipv4_rdns": "li2216-25.members.linode.com",
|
||||
"__meta_linode_public_ipv6_rdns": "",
|
||||
"__meta_linode_region": "ca-central",
|
||||
"__meta_linode_type": "g6-standard-1",
|
||||
"__meta_linode_status": "running",
|
||||
"__meta_linode_tags": ",monitoring,",
|
||||
"__meta_linode_group": "",
|
||||
"__meta_linode_gpus": "0",
|
||||
"__meta_linode_hypervisor": "kvm",
|
||||
"__meta_linode_backups": "disabled",
|
||||
"__meta_linode_specs_disk_bytes": "53687091200",
|
||||
"__meta_linode_specs_memory_bytes": "2147483648",
|
||||
"__meta_linode_specs_vcpus": "1",
|
||||
"__meta_linode_specs_transfer_bytes": "2097152000",
|
||||
"__meta_linode_ipv6_ranges": ",2600:3c04:e001:456::/64,",
|
||||
}),
|
||||
promutil.NewLabelsFromMap(map[string]string{
|
||||
"__address__": "66.228.47.103:80",
|
||||
"__meta_linode_instance_id": "26837992",
|
||||
"__meta_linode_instance_label": "prometheus-linode-sd-exporter-4",
|
||||
"__meta_linode_image": "linode/ubuntu20.04",
|
||||
"__meta_linode_private_ipv4": "192.168.148.94",
|
||||
"__meta_linode_public_ipv4": "66.228.47.103",
|
||||
"__meta_linode_public_ipv6": "2600:3c03::f03c:92ff:fe1a:fb4c",
|
||||
"__meta_linode_private_ipv4_rdns": "",
|
||||
"__meta_linode_public_ipv4_rdns": "li328-103.members.linode.com",
|
||||
"__meta_linode_public_ipv6_rdns": "",
|
||||
"__meta_linode_region": "us-east",
|
||||
"__meta_linode_type": "g6-nanode-1",
|
||||
"__meta_linode_status": "running",
|
||||
"__meta_linode_tags": ",monitoring,",
|
||||
"__meta_linode_group": "",
|
||||
"__meta_linode_gpus": "0",
|
||||
"__meta_linode_hypervisor": "kvm",
|
||||
"__meta_linode_backups": "disabled",
|
||||
"__meta_linode_specs_disk_bytes": "26843545600",
|
||||
"__meta_linode_specs_memory_bytes": "1073741824",
|
||||
"__meta_linode_specs_vcpus": "1",
|
||||
"__meta_linode_specs_transfer_bytes": "1048576000",
|
||||
"__meta_linode_extra_ips": ",172.104.18.104,",
|
||||
"__meta_linode_ipv6_ranges": ",2600:3c03:e000:123::/64,",
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
func TestAddInstanceLabelsPrivateOnlyFallback(t *testing.T) {
|
||||
instances := []instance{
|
||||
{
|
||||
ID: 2, Label: "vpc-only", Status: "running", Type: "g6-nanode-1",
|
||||
Image: "linode/ubuntu", Region: "us-east", Hypervisor: "kvm",
|
||||
IPv4: []string{"10.0.0.5"},
|
||||
Specs: specs{Disk: 1, Memory: 1, VCPUs: 1, Transfer: 1},
|
||||
},
|
||||
// Has IPv4 list entries but no matching detailed IPs — must be skipped.
|
||||
{
|
||||
ID: 3, Label: "orphan", Status: "running", Type: "g6-nanode-1",
|
||||
Image: "linode/ubuntu", Region: "us-east",
|
||||
IPv4: []string{"10.0.0.99"},
|
||||
Specs: specs{Disk: 1, Memory: 1, VCPUs: 1, Transfer: 1},
|
||||
},
|
||||
}
|
||||
ips := []ipAddress{{Address: "10.0.0.5", Public: false, RDNS: ""}}
|
||||
got := addInstanceLabels(instances, ips, nil, 80, ",")
|
||||
want := []*promutil.Labels{
|
||||
promutil.NewLabelsFromMap(map[string]string{
|
||||
"__address__": "10.0.0.5:80",
|
||||
"__meta_linode_instance_id": "2",
|
||||
"__meta_linode_instance_label": "vpc-only",
|
||||
"__meta_linode_image": "linode/ubuntu",
|
||||
"__meta_linode_private_ipv4": "10.0.0.5",
|
||||
"__meta_linode_public_ipv4": "",
|
||||
"__meta_linode_public_ipv6": "",
|
||||
"__meta_linode_private_ipv4_rdns": "",
|
||||
"__meta_linode_public_ipv4_rdns": "",
|
||||
"__meta_linode_public_ipv6_rdns": "",
|
||||
"__meta_linode_region": "us-east",
|
||||
"__meta_linode_type": "g6-nanode-1",
|
||||
"__meta_linode_status": "running",
|
||||
"__meta_linode_group": "",
|
||||
"__meta_linode_gpus": "0",
|
||||
"__meta_linode_hypervisor": "kvm",
|
||||
"__meta_linode_backups": "disabled",
|
||||
"__meta_linode_specs_disk_bytes": "1048576",
|
||||
"__meta_linode_specs_memory_bytes": "1048576",
|
||||
"__meta_linode_specs_vcpus": "1",
|
||||
"__meta_linode_specs_transfer_bytes": "1048576",
|
||||
}),
|
||||
}
|
||||
discoveryutil.TestEqualLabelss(t, got, want)
|
||||
}
|
||||
|
||||
func TestAddInstanceLabelsCustomPortAndTagSeparator(t *testing.T) {
|
||||
instances := []instance{
|
||||
{
|
||||
ID: 1, Label: "node", Status: "running", Type: "g6-nanode-1",
|
||||
Image: "linode/ubuntu", Region: "us-east", Hypervisor: "kvm",
|
||||
IPv4: []string{"1.2.3.4"},
|
||||
Tags: []string{"a", "b"},
|
||||
Specs: specs{Disk: 1, Memory: 1, VCPUs: 1, Transfer: 1},
|
||||
},
|
||||
}
|
||||
ips := []ipAddress{{Address: "1.2.3.4", Public: true, RDNS: "example.com"}}
|
||||
got := addInstanceLabels(instances, ips, nil, 9100, ";")
|
||||
want := []*promutil.Labels{
|
||||
promutil.NewLabelsFromMap(map[string]string{
|
||||
"__address__": "1.2.3.4:9100",
|
||||
"__meta_linode_instance_id": "1",
|
||||
"__meta_linode_instance_label": "node",
|
||||
"__meta_linode_image": "linode/ubuntu",
|
||||
"__meta_linode_private_ipv4": "",
|
||||
"__meta_linode_public_ipv4": "1.2.3.4",
|
||||
"__meta_linode_public_ipv6": "",
|
||||
"__meta_linode_private_ipv4_rdns": "",
|
||||
"__meta_linode_public_ipv4_rdns": "example.com",
|
||||
"__meta_linode_public_ipv6_rdns": "",
|
||||
"__meta_linode_region": "us-east",
|
||||
"__meta_linode_type": "g6-nanode-1",
|
||||
"__meta_linode_status": "running",
|
||||
"__meta_linode_tags": ";a;b;",
|
||||
"__meta_linode_group": "",
|
||||
"__meta_linode_gpus": "0",
|
||||
"__meta_linode_hypervisor": "kvm",
|
||||
"__meta_linode_backups": "disabled",
|
||||
"__meta_linode_specs_disk_bytes": "1048576",
|
||||
"__meta_linode_specs_memory_bytes": "1048576",
|
||||
"__meta_linode_specs_vcpus": "1",
|
||||
"__meta_linode_specs_transfer_bytes": "1048576",
|
||||
}),
|
||||
}
|
||||
discoveryutil.TestEqualLabelss(t, got, want)
|
||||
}
|
||||
@@ -30,7 +30,6 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/http"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/kubernetes"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/kuma"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/linode"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/marathon"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/nomad"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/openstack"
|
||||
@@ -142,7 +141,6 @@ func runScraper(configFile string, pushData func(at *auth.Token, wr *prompb.Writ
|
||||
scs.add("http_sd_configs", *http.SDCheckInterval, func(cfg *Config, swsPrev []*ScrapeWork) []*ScrapeWork { return cfg.getHTTPDScrapeWork(swsPrev) })
|
||||
scs.add("kubernetes_sd_configs", *kubernetes.SDCheckInterval, func(cfg *Config, swsPrev []*ScrapeWork) []*ScrapeWork { return cfg.getKubernetesSDScrapeWork(swsPrev) })
|
||||
scs.add("kuma_sd_configs", *kuma.SDCheckInterval, func(cfg *Config, swsPrev []*ScrapeWork) []*ScrapeWork { return cfg.getKumaSDScrapeWork(swsPrev) })
|
||||
scs.add("linode_sd_configs", *linode.SDCheckInterval, func(cfg *Config, swsPrev []*ScrapeWork) []*ScrapeWork { return cfg.getLinodeSDScrapeWork(swsPrev) })
|
||||
scs.add("marathon_sd_configs", *marathon.SDCheckInterval, func(cfg *Config, swsPrev []*ScrapeWork) []*ScrapeWork { return cfg.getMarathonSDScrapeWork(swsPrev) })
|
||||
scs.add("nomad_sd_configs", *nomad.SDCheckInterval, func(cfg *Config, swsPrev []*ScrapeWork) []*ScrapeWork { return cfg.getNomadSDScrapeWork(swsPrev) })
|
||||
scs.add("openstack_sd_configs", *openstack.SDCheckInterval, func(cfg *Config, swsPrev []*ScrapeWork) []*ScrapeWork { return cfg.getOpenStackSDScrapeWork(swsPrev) })
|
||||
|
||||
@@ -197,11 +197,6 @@ func (tu *tagsUnmarshaler) addBytes(b []byte) []byte {
|
||||
func (tu *tagsUnmarshaler) unmarshalTags(o *fastjson.Object) error {
|
||||
tu.err = nil
|
||||
o.Visit(func(key []byte, v *fastjson.Value) {
|
||||
if len(key) == 0 {
|
||||
// Skip tags with empty name, since they override the metric name.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4962
|
||||
return
|
||||
}
|
||||
tag := tu.addTag()
|
||||
tag.Key = tu.addBytes(key)
|
||||
sb, err := v.StringBytes()
|
||||
|
||||
@@ -140,26 +140,6 @@ func TestRowsUnmarshalSuccess(t *testing.T) {
|
||||
}},
|
||||
})
|
||||
|
||||
// Line with a tag with empty name.
|
||||
// Such a tag must be skipped, since it overrides the metric name.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4962
|
||||
f(`{"metric":{"__name__":"foo","":"bar","baz":"xx"},"values":[1.23],"timestamps":[456]}`, &Rows{
|
||||
Rows: []Row{{
|
||||
Tags: []Tag{
|
||||
{
|
||||
Key: []byte("__name__"),
|
||||
Value: []byte("foo"),
|
||||
},
|
||||
{
|
||||
Key: []byte("baz"),
|
||||
Value: []byte("xx"),
|
||||
},
|
||||
},
|
||||
Values: []float64{1.23},
|
||||
Timestamps: []int64{456},
|
||||
}},
|
||||
})
|
||||
|
||||
// Multiple lines
|
||||
f(`{"metric":{"foo":"bar","baz":"xx"},"values":[1.23, -3.21],"timestamps" : [456,789]}
|
||||
{"metric":{"__name__":"xx"},"values":[34],"timestamps" : [11]}
|
||||
|
||||
@@ -24,9 +24,7 @@ var (
|
||||
disableCompression = flag.Bool("pushmetrics.disableCompression", false, "Whether to disable request body compression when pushing metrics to every -pushmetrics.url")
|
||||
)
|
||||
|
||||
// InitSecretFlags manages the secret flags for this pkg and must be called by app-level initSecretFlags.
|
||||
// It should run before logger initialization and package Init() (if exists).
|
||||
func InitSecretFlags() {
|
||||
func init() {
|
||||
// The -pushmetrics.url flag can contain basic auth creds, so it mustn't be visible when exposing the flags.
|
||||
flagutil.RegisterSecretFlag("pushmetrics.url")
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ type indexDB struct {
|
||||
// legacy indexDBs, since these indexDBs are readonly.
|
||||
// This field cannot be used for the partition indexDBs, since they may receive data
|
||||
// with bigger timestamps at any time.
|
||||
legacyMinMissingTimestampByKey map[TenantToken]int64
|
||||
legacyMinMissingTimestampByKey map[string]int64
|
||||
// protects legacyMinMissingTimestampByKey
|
||||
legacyMinMissingTimestampByKeyLock sync.Mutex
|
||||
|
||||
@@ -174,7 +174,7 @@ func mustOpenIndexDB(id uint64, tr TimeRange, name, path string, s *Storage, isR
|
||||
tfssCache := lrucache.NewCache(getTagFiltersCacheSize)
|
||||
tb := mergeset.MustOpenTable(path, dataFlushInterval, tfssCache.Reset, 0, mergeTagToMetricIDsRows, isReadOnly)
|
||||
db := &indexDB{
|
||||
legacyMinMissingTimestampByKey: make(map[TenantToken]int64),
|
||||
legacyMinMissingTimestampByKey: make(map[string]int64),
|
||||
id: id,
|
||||
tr: tr,
|
||||
name: name,
|
||||
@@ -508,11 +508,6 @@ func (db *indexDB) SearchLabelNames(qt *querytracer.Tracer, tfss []*TagFilters,
|
||||
qt = qt.NewChild("search label names: filters=%s, timeRange=%s, maxLabelNames=%d, maxMetrics=%d", tfss, &tr, maxLabelNames, maxMetrics)
|
||||
defer qt.Done()
|
||||
|
||||
if !db.legacyContainsTimeRange(tr) {
|
||||
qt.Printf("indexDB doesn't contain data for the given time range: %v", &tr)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
is := db.getIndexSearch(deadline)
|
||||
lns, err := is.searchLabelNamesWithFiltersOnTimeRange(qt, tfss, tr, maxLabelNames, maxMetrics)
|
||||
db.putIndexSearch(is)
|
||||
@@ -715,11 +710,6 @@ func (db *indexDB) SearchLabelValues(qt *querytracer.Tracer, labelName string, t
|
||||
qt = qt.NewChild("search label values: labelName=%q, filters=%s, timeRange=%s, maxLabelValues=%d, maxMetrics=%d", labelName, tfss, &tr, maxLabelValues, maxMetrics)
|
||||
defer qt.Done()
|
||||
|
||||
if !db.legacyContainsTimeRange(tr) {
|
||||
qt.Printf("indexDB doesn't contain data for the given time range: %v", &tr)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
key := labelName
|
||||
if key == "__name__" {
|
||||
key = ""
|
||||
@@ -968,11 +958,6 @@ func (db *indexDB) SearchTagValueSuffixes(qt *querytracer.Tracer, tr TimeRange,
|
||||
&tr, tagKey, tagValuePrefix, delimiter, maxTagValueSuffixes)
|
||||
defer qt.Done()
|
||||
|
||||
if !db.legacyContainsTimeRange(tr) {
|
||||
qt.Printf("indexDB doesn't contain data for the given time range: %v", &tr)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// TODO: cache results?
|
||||
|
||||
is := db.getIndexSearch(deadline)
|
||||
@@ -1107,11 +1092,6 @@ func (db *indexDB) SearchGraphitePaths(qt *querytracer.Tracer, tr TimeRange, qHe
|
||||
qt = qt.NewChild("search graphite paths: timeRange=%s, qHead=%q, qTail=%q, maxPaths=%d", &tr, bytesutil.ToUnsafeString(qHead), bytesutil.ToUnsafeString(qTail), maxPaths)
|
||||
defer qt.Done()
|
||||
|
||||
if !db.legacyContainsTimeRange(tr) {
|
||||
qt.Printf("indexDB doesn't contain data for the given time range: %v", &tr)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
n := bytes.IndexAny(qTail, "*[{")
|
||||
if n < 0 {
|
||||
// Verify that qHead matches a metric name.
|
||||
@@ -1293,11 +1273,6 @@ func (db *indexDB) GetTSDBStatus(qt *querytracer.Tracer, tfss []*TagFilters, dat
|
||||
qt = qt.NewChild("collect TSDB status: filters=%s, date=%s, focusLabel=%q, topN=%d, maxMetrics=%d", tfss, dateToString(date), focusLabel, topN, maxMetrics)
|
||||
defer qt.Done()
|
||||
|
||||
if !db.legacyContainsDate(date) {
|
||||
qt.Printf("indexDB doesn't contain data for the given date: %s", dateToString(date))
|
||||
return &TSDBStatus{}, nil
|
||||
}
|
||||
|
||||
is := db.getIndexSearch(deadline)
|
||||
defer db.putIndexSearch(is)
|
||||
status, err := is.getTSDBStatus(qt, tfss, date, focusLabel, topN, maxMetrics)
|
||||
@@ -1745,11 +1720,6 @@ func (db *indexDB) SearchTSIDs(qt *querytracer.Tracer, tfss []*TagFilters, tr Ti
|
||||
qt = qt.NewChild("search TSIDs: filters=%s, timeRange=%s, maxMetrics=%d", tfss, &tr, maxMetrics)
|
||||
defer qt.Done()
|
||||
|
||||
if !db.legacyContainsTimeRange(tr) {
|
||||
qt.Printf("indexDB doesn't contain data for the given time range: %v", &tr)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
metricIDs, err := db.searchMetricIDs(qt, tfss, tr, maxMetrics, deadline)
|
||||
if err != nil {
|
||||
return nil, db.wrapError("search TSIDs", err)
|
||||
@@ -1833,11 +1803,6 @@ func (db *indexDB) SearchMetricNames(qt *querytracer.Tracer, tfss []*TagFilters,
|
||||
qt = qt.NewChild("search metric names: filters=%s, timeRange=%s, maxMetrics=%d", tfss, &tr, maxMetrics)
|
||||
defer qt.Done()
|
||||
|
||||
if !db.legacyContainsTimeRange(tr) {
|
||||
qt.Printf("indexDB doesn't contain data for the given time range: %v", &tr)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
metricIDs, err := db.searchMetricIDs(qt, tfss, tr, maxMetrics, deadline)
|
||||
if err != nil {
|
||||
return nil, db.wrapError("search metric names", err)
|
||||
@@ -2254,12 +2219,18 @@ func (is *indexSearch) searchMetricIDsInternal(qt *querytracer.Tracer, tfss []*T
|
||||
qt = qt.NewChild("search for metric ids: filters=%s, timeRange=%s, maxMetrics=%d", tfss, &tr, maxMetrics)
|
||||
defer qt.Done()
|
||||
|
||||
metricIDs := &uint64set.Set{}
|
||||
|
||||
if !is.legacyContainsTimeRange(tr) {
|
||||
qt.Printf("indexdb doesn't contain data for the given timeRange=%s", &tr)
|
||||
return metricIDs, nil
|
||||
}
|
||||
|
||||
if tr.MinTimestamp >= is.db.s.minTimestampForCompositeIndex {
|
||||
tfss = convertToCompositeTagFilterss(tfss)
|
||||
qt.Printf("composite filters=%s", tfss)
|
||||
}
|
||||
|
||||
metricIDs := &uint64set.Set{}
|
||||
for _, tfs := range tfss {
|
||||
if len(tfs.tfs) == 0 {
|
||||
// An empty filters must be equivalent to `{__name__!=""}`
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/encoding"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
@@ -86,61 +87,48 @@ func mustOpenLegacyIndexDB(path string, s *Storage) *legacyIndexDB {
|
||||
return legacyIDB
|
||||
}
|
||||
|
||||
func (db *indexDB) legacyContainsDate(date uint64) bool {
|
||||
var tr TimeRange
|
||||
if date == globalIndexDate {
|
||||
tr = globalIndexTimeRange
|
||||
} else {
|
||||
tr.MinTimestamp = int64(date) * msecPerDay
|
||||
tr.MaxTimestamp = int64(date+1)*msecPerDay - 1
|
||||
}
|
||||
return db.legacyContainsTimeRange(tr)
|
||||
}
|
||||
|
||||
func (db *indexDB) legacyContainsTimeRange(tr TimeRange) bool {
|
||||
func (is *indexSearch) legacyContainsTimeRange(tr TimeRange) bool {
|
||||
if tr == globalIndexTimeRange {
|
||||
return true
|
||||
}
|
||||
|
||||
db := is.db
|
||||
if !db.noRegisterNewSeries.Load() {
|
||||
// indexDB could register new time series - it is not safe to cache minMissingTimestamp
|
||||
return true
|
||||
}
|
||||
|
||||
// vmsingle does not have tenants and therefore has just one key.
|
||||
// While vmstorage can potentially have many tenants and the actual
|
||||
// accountID and projectID will be set from the request.
|
||||
key := TenantToken{
|
||||
AccountID: 0,
|
||||
ProjectID: 0,
|
||||
}
|
||||
// use common prefix as a key for minMissingTimestamp
|
||||
// it's needed to properly track timestamps for cluster version
|
||||
// which uses tenant labels for the index search
|
||||
kb := &is.kb
|
||||
kb.B = is.marshalCommonPrefix(kb.B[:0], nsPrefixDateToMetricID)
|
||||
key := kb.B
|
||||
|
||||
db.legacyMinMissingTimestampByKeyLock.Lock()
|
||||
minMissingTimestamp, ok := db.legacyMinMissingTimestampByKey[key]
|
||||
minMissingTimestamp, ok := db.legacyMinMissingTimestampByKey[string(key)]
|
||||
db.legacyMinMissingTimestampByKeyLock.Unlock()
|
||||
|
||||
if ok && tr.MinTimestamp >= minMissingTimestamp {
|
||||
// Fast path.
|
||||
return false
|
||||
}
|
||||
|
||||
// Slow path.
|
||||
is := db.getIndexSearch(noDeadline)
|
||||
defer db.putIndexSearch(is)
|
||||
if is.legacyContainsTimeRange(tr) {
|
||||
if is.legacyContainsTimeRangeSlow(kb, tr) {
|
||||
return true
|
||||
}
|
||||
|
||||
db.legacyMinMissingTimestampByKeyLock.Lock()
|
||||
minMissingTimestamp, ok = db.legacyMinMissingTimestampByKey[key]
|
||||
minMissingTimestamp, ok = db.legacyMinMissingTimestampByKey[string(key)]
|
||||
if !ok || tr.MinTimestamp < minMissingTimestamp {
|
||||
db.legacyMinMissingTimestampByKey[key] = tr.MinTimestamp
|
||||
db.legacyMinMissingTimestampByKey[string(key)] = tr.MinTimestamp
|
||||
}
|
||||
db.legacyMinMissingTimestampByKeyLock.Unlock()
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (is *indexSearch) legacyContainsTimeRange(tr TimeRange) bool {
|
||||
func (is *indexSearch) legacyContainsTimeRangeSlow(prefixBuf *bytesutil.ByteBuffer, tr TimeRange) bool {
|
||||
ts := &is.ts
|
||||
|
||||
// Verify whether the tr.MinTimestamp is included into `ts` or is smaller than the minimum date stored in `ts`.
|
||||
// Do not check whether tr.MaxTimestamp is included into `ts` or is bigger than the max date stored in `ts` for performance reasons.
|
||||
// This means that this func can return true if `tr` is located below the min date stored in `ts`.
|
||||
@@ -148,17 +136,12 @@ func (is *indexSearch) legacyContainsTimeRange(tr TimeRange) bool {
|
||||
// The main practical case allows skipping searching in prev indexdb (`ts`) when `tr`
|
||||
// is located above the max date stored there.
|
||||
minDate := uint64(tr.MinTimestamp) / msecPerDay
|
||||
|
||||
kb := &is.kb
|
||||
kb.B = is.marshalCommonPrefix(kb.B[:0], nsPrefixDateToMetricID)
|
||||
prefix := kb.B
|
||||
kb.B = encoding.MarshalUint64(kb.B, minDate)
|
||||
|
||||
ts := &is.ts
|
||||
ts.Seek(kb.B)
|
||||
prefix := prefixBuf.B
|
||||
prefixBuf.B = encoding.MarshalUint64(prefixBuf.B, minDate)
|
||||
ts.Seek(prefixBuf.B)
|
||||
if !ts.NextItem() {
|
||||
if err := ts.Error(); err != nil {
|
||||
logger.Panicf("FATAL: error when searching for minDate=%d, prefix %q: %s", minDate, kb.B, err)
|
||||
logger.Panicf("FATAL: error when searching for minDate=%d, prefix %q: %s", minDate, prefixBuf.B, err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -29,7 +29,11 @@ func TestLegacyContainsTimeRange(t *testing.T) {
|
||||
|
||||
f := func(idb *indexDB, tr TimeRange, want bool) {
|
||||
t.Helper()
|
||||
got := idb.legacyContainsTimeRange(tr)
|
||||
is := idb.getIndexSearch(noDeadline)
|
||||
defer idb.putIndexSearch(is)
|
||||
|
||||
got := is.legacyContainsTimeRange(tr)
|
||||
|
||||
if got != want {
|
||||
t.Fatalf("legacyContainsTimeRange(%s) for index db %s returns unexpected result: got %t, want %t", tr.String(), idb.name, got, want)
|
||||
}
|
||||
@@ -93,8 +97,8 @@ func TestLegacyContainsTimeRange(t *testing.T) {
|
||||
f(legacyIDBs.getIDBCurr(), tr, true)
|
||||
f(idb, tr, true)
|
||||
|
||||
// Fully inside trPt, overlaps with trPrev on the right side and with trCurr
|
||||
// on the left side.
|
||||
// Fully inside trPt, overlaps with trPrev on the right side and trCurr on
|
||||
// the left side.
|
||||
tr = TimeRange{
|
||||
MinTimestamp: time.Date(2025, 1, 7, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
MaxTimestamp: time.Date(2025, 1, 21, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
|
||||
|
||||
@@ -2241,16 +2241,15 @@ func TestIndexSearchLegacyContainsTimeRange_Concurrent(t *testing.T) {
|
||||
for i := range concurrency {
|
||||
ts := minTimestamp + msecPerDay*i
|
||||
wg.Go(func() {
|
||||
_ = idb.legacyContainsTimeRange(TimeRange{ts, ts})
|
||||
is := idb.getIndexSearch(noDeadline)
|
||||
_ = is.legacyContainsTimeRange(TimeRange{ts, ts})
|
||||
idb.putIndexSearch(is)
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
key := TenantToken{
|
||||
AccountID: 0,
|
||||
ProjectID: 0,
|
||||
}
|
||||
if got, want := idb.legacyMinMissingTimestampByKey[key], minTimestamp; got != want {
|
||||
key := marshalCommonPrefix(nil, nsPrefixDateToMetricID)
|
||||
if got, want := idb.legacyMinMissingTimestampByKey[string(key)], minTimestamp; got != want {
|
||||
t.Fatalf("unexpected min timestamp: got %v, want %v", time.UnixMilli(got).UTC(), time.UnixMilli(want).UTC())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1290,9 +1290,6 @@ func (pt *partition) mergeParts(pws []*partWrapper, stopCh <-chan struct{}, isFi
|
||||
putBlockStreamReader(bsr)
|
||||
}
|
||||
if err != nil {
|
||||
if mpNew != nil {
|
||||
putInmemoryPart(mpNew)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if mpNew != nil {
|
||||
@@ -1447,8 +1444,6 @@ func (pt *partition) openCreatedPart(ph *partHeader, pws []*partWrapper, mpNew *
|
||||
// The created part is empty. Remove it
|
||||
if mpNew == nil {
|
||||
fs.MustRemoveDir(dstPartPath)
|
||||
} else {
|
||||
putInmemoryPart(mpNew)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -33,22 +33,16 @@ func TestLegacyStorage_SearchMetricNames(t *testing.T) {
|
||||
return mrs, want
|
||||
}
|
||||
const numMetrics = 1000
|
||||
tr1 := TimeRange{
|
||||
tr := TimeRange{
|
||||
MinTimestamp: time.Date(2023, 6, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
MaxTimestamp: time.Date(2024, 5, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
|
||||
}
|
||||
tr2 := TimeRange{
|
||||
MinTimestamp: time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
MaxTimestamp: time.Date(2024, 6, 30, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
|
||||
}
|
||||
legacyData, wantLegacy := genData(numMetrics, "legacy", tr1)
|
||||
new1Data, wantNew1 := genData(numMetrics, "new1", tr1)
|
||||
new2Data, wantNew2 := genData(numMetrics, "new2", tr2)
|
||||
newData := slices.Concat(new1Data, new2Data)
|
||||
wantLegacyAndNew1 := slices.Concat(wantLegacy, wantNew1)
|
||||
slices.Sort(wantLegacyAndNew1)
|
||||
legacyData, wantLegacy := genData(numMetrics, "legacy", tr)
|
||||
newData, wantNew := genData(numMetrics, "new", tr)
|
||||
wantNew = append(wantNew, wantLegacy...)
|
||||
slices.Sort(wantNew)
|
||||
|
||||
assertSearchResults := func(s *Storage, tr TimeRange, want []string) {
|
||||
assertSearchResults := func(s *Storage, want []string) {
|
||||
t.Helper()
|
||||
tfsAll := NewTagFilters()
|
||||
if err := tfsAll.Add([]byte("__name__"), []byte(".*"), false, true); err != nil {
|
||||
@@ -73,11 +67,10 @@ func TestLegacyStorage_SearchMetricNames(t *testing.T) {
|
||||
}
|
||||
|
||||
assertLegacyData := func(s *Storage) {
|
||||
assertSearchResults(s, tr1, wantLegacy)
|
||||
assertSearchResults(s, wantLegacy)
|
||||
}
|
||||
assertNewData := func(s *Storage) {
|
||||
assertSearchResults(s, tr1, wantLegacyAndNew1)
|
||||
assertSearchResults(s, tr2, wantNew2)
|
||||
assertSearchResults(s, wantNew)
|
||||
}
|
||||
testSearchOpWithLegacyIndexDBs(t, legacyData, newData, assertLegacyData, assertNewData)
|
||||
}
|
||||
@@ -102,22 +95,15 @@ func TestLegacyStorage_SearchLabelNames(t *testing.T) {
|
||||
return mrs, want
|
||||
}
|
||||
const numMetrics = 1000
|
||||
tr1 := TimeRange{
|
||||
tr := TimeRange{
|
||||
MinTimestamp: time.Date(2023, 6, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
MaxTimestamp: time.Date(2024, 5, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
|
||||
}
|
||||
tr2 := TimeRange{
|
||||
MinTimestamp: time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
MaxTimestamp: time.Date(2024, 6, 30, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
|
||||
}
|
||||
legacyData, wantLegacy := genData(numMetrics, "legacy", tr1)
|
||||
new1Data, wantNew1 := genData(numMetrics, "new1", tr1)
|
||||
new2Data, wantNew2 := genData(numMetrics, "new2", tr2)
|
||||
newData := slices.Concat(new1Data, new2Data)
|
||||
wantLegacyAndNew1 := slices.Concat(wantLegacy, wantNew1)
|
||||
slices.Sort(wantLegacyAndNew1)
|
||||
legacyData, wantLegacy := genData(numMetrics, "legacy", tr)
|
||||
newData, wantNew := genData(numMetrics, "new", tr)
|
||||
wantNew = append(wantNew, wantLegacy...)
|
||||
|
||||
assertSearchResults := func(s *Storage, tr TimeRange, want []string) {
|
||||
assertSearchResults := func(s *Storage, want []string) {
|
||||
t.Helper()
|
||||
got, err := s.SearchLabelNames(nil, nil, tr, 1e9, 1e9, noDeadline)
|
||||
if err != nil {
|
||||
@@ -132,16 +118,12 @@ func TestLegacyStorage_SearchLabelNames(t *testing.T) {
|
||||
assertLegacyData := func(s *Storage) {
|
||||
want := append(wantLegacy, "__name__")
|
||||
slices.Sort(want)
|
||||
assertSearchResults(s, tr1, want)
|
||||
assertSearchResults(s, want)
|
||||
}
|
||||
assertNewData := func(s *Storage) {
|
||||
want := append(wantLegacyAndNew1, "__name__")
|
||||
want := append(wantNew, "__name__")
|
||||
slices.Sort(want)
|
||||
assertSearchResults(s, tr1, want)
|
||||
|
||||
want = append(wantNew2, "__name__")
|
||||
slices.Sort(want)
|
||||
assertSearchResults(s, tr2, want)
|
||||
assertSearchResults(s, want)
|
||||
}
|
||||
testSearchOpWithLegacyIndexDBs(t, legacyData, newData, assertLegacyData, assertNewData)
|
||||
}
|
||||
@@ -166,22 +148,16 @@ func TestLegacyStorage_SearchLabelValues(t *testing.T) {
|
||||
return mrs, want
|
||||
}
|
||||
const numMetrics = 1000
|
||||
tr1 := TimeRange{
|
||||
tr := TimeRange{
|
||||
MinTimestamp: time.Date(2023, 6, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
MaxTimestamp: time.Date(2024, 5, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
|
||||
}
|
||||
tr2 := TimeRange{
|
||||
MinTimestamp: time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
MaxTimestamp: time.Date(2024, 6, 30, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
|
||||
}
|
||||
legacyData, wantLegacy := genData(numMetrics, "legacy", tr1)
|
||||
new1Data, wantNew1 := genData(numMetrics, "new1", tr1)
|
||||
new2Data, wantNew2 := genData(numMetrics, "new2", tr2)
|
||||
newData := slices.Concat(new1Data, new2Data)
|
||||
wantLegacyAndNew1 := slices.Concat(wantLegacy, wantNew1)
|
||||
slices.Sort(wantLegacyAndNew1)
|
||||
legacyData, wantLegacy := genData(numMetrics, "legacy", tr)
|
||||
newData, wantNew := genData(numMetrics, "new", tr)
|
||||
wantNew = append(wantNew, wantLegacy...)
|
||||
slices.Sort(wantNew)
|
||||
|
||||
assertSearchResults := func(s *Storage, tr TimeRange, want []string) {
|
||||
assertSearchResults := func(s *Storage, want []string) {
|
||||
t.Helper()
|
||||
got, err := s.SearchLabelValues(nil, "label", nil, tr, 1e9, 1e9, noDeadline)
|
||||
if err != nil {
|
||||
@@ -195,12 +171,11 @@ func TestLegacyStorage_SearchLabelValues(t *testing.T) {
|
||||
|
||||
assertLegacyData := func(s *Storage) {
|
||||
t.Helper()
|
||||
assertSearchResults(s, tr1, wantLegacy)
|
||||
assertSearchResults(s, wantLegacy)
|
||||
}
|
||||
assertNewData := func(s *Storage) {
|
||||
t.Helper()
|
||||
assertSearchResults(s, tr1, wantLegacyAndNew1)
|
||||
assertSearchResults(s, tr2, wantNew2)
|
||||
assertSearchResults(s, wantNew)
|
||||
}
|
||||
testSearchOpWithLegacyIndexDBs(t, legacyData, newData, assertLegacyData, assertNewData)
|
||||
}
|
||||
@@ -222,22 +197,16 @@ func TestLegacyStorage_SearchTagValueSuffixes(t *testing.T) {
|
||||
return mrs, want
|
||||
}
|
||||
const numMetrics = 1000
|
||||
tr1 := TimeRange{
|
||||
tr := TimeRange{
|
||||
MinTimestamp: time.Date(2023, 6, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
MaxTimestamp: time.Date(2024, 5, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
|
||||
}
|
||||
tr2 := TimeRange{
|
||||
MinTimestamp: time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
MaxTimestamp: time.Date(2024, 6, 30, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
|
||||
}
|
||||
legacyData, wantLegacy := genData(numMetrics, "legacy", tr1)
|
||||
new1Data, wantNew1 := genData(numMetrics, "new1", tr1)
|
||||
new2Data, wantNew2 := genData(numMetrics, "new2", tr2)
|
||||
newData := slices.Concat(new1Data, new2Data)
|
||||
wantLegacyAndNew1 := slices.Concat(wantLegacy, wantNew1)
|
||||
slices.Sort(wantLegacyAndNew1)
|
||||
legacyData, wantLegacy := genData(numMetrics, "legacy", tr)
|
||||
newData, wantNew := genData(numMetrics, "new", tr)
|
||||
wantNew = append(wantNew, wantLegacy...)
|
||||
slices.Sort(wantNew)
|
||||
|
||||
assertSearchResults := func(s *Storage, tr TimeRange, want []string) {
|
||||
assertSearchResults := func(s *Storage, want []string) {
|
||||
t.Helper()
|
||||
got, err := s.SearchTagValueSuffixes(nil, tr, "", "prefix.", '.', 1e9, noDeadline)
|
||||
if err != nil {
|
||||
@@ -252,12 +221,11 @@ func TestLegacyStorage_SearchTagValueSuffixes(t *testing.T) {
|
||||
|
||||
assertLegacyData := func(s *Storage) {
|
||||
t.Helper()
|
||||
assertSearchResults(s, tr1, wantLegacy)
|
||||
assertSearchResults(s, wantLegacy)
|
||||
}
|
||||
assertNewData := func(s *Storage) {
|
||||
t.Helper()
|
||||
assertSearchResults(s, tr1, wantLegacyAndNew1)
|
||||
assertSearchResults(s, tr2, wantNew2)
|
||||
assertSearchResults(s, wantNew)
|
||||
}
|
||||
testSearchOpWithLegacyIndexDBs(t, legacyData, newData, assertLegacyData, assertNewData)
|
||||
}
|
||||
@@ -279,22 +247,16 @@ func TestLegacyStorage_SearchGraphitePaths(t *testing.T) {
|
||||
return mrs, want
|
||||
}
|
||||
const numMetrics = 1000
|
||||
tr1 := TimeRange{
|
||||
tr := TimeRange{
|
||||
MinTimestamp: time.Date(2023, 6, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
MaxTimestamp: time.Date(2024, 5, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
|
||||
}
|
||||
tr2 := TimeRange{
|
||||
MinTimestamp: time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
MaxTimestamp: time.Date(2024, 6, 30, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
|
||||
}
|
||||
legacyData, wantLegacy := genData(numMetrics, "legacy", tr1)
|
||||
new1Data, wantNew1 := genData(numMetrics, "new1", tr1)
|
||||
new2Data, wantNew2 := genData(numMetrics, "new2", tr2)
|
||||
newData := slices.Concat(new1Data, new2Data)
|
||||
wantLegacyAndNew1 := slices.Concat(wantLegacy, wantNew1)
|
||||
slices.Sort(wantLegacyAndNew1)
|
||||
legacyData, wantLegacy := genData(numMetrics, "legacy", tr)
|
||||
newData, wantNew := genData(numMetrics, "new", tr)
|
||||
wantNew = append(wantNew, wantLegacy...)
|
||||
slices.Sort(wantNew)
|
||||
|
||||
assertSearchResults := func(s *Storage, tr TimeRange, want []string) {
|
||||
assertSearchResults := func(s *Storage, want []string) {
|
||||
t.Helper()
|
||||
got, err := s.SearchGraphitePaths(nil, tr, []byte("*.*"), 1e9, noDeadline)
|
||||
if err != nil {
|
||||
@@ -309,17 +271,16 @@ func TestLegacyStorage_SearchGraphitePaths(t *testing.T) {
|
||||
|
||||
assertLegacyData := func(s *Storage) {
|
||||
t.Helper()
|
||||
assertSearchResults(s, tr1, wantLegacy)
|
||||
assertSearchResults(s, wantLegacy)
|
||||
}
|
||||
assertNewData := func(s *Storage) {
|
||||
t.Helper()
|
||||
assertSearchResults(s, tr1, wantLegacyAndNew1)
|
||||
assertSearchResults(s, tr2, wantNew2)
|
||||
assertSearchResults(s, wantNew)
|
||||
}
|
||||
testSearchOpWithLegacyIndexDBs(t, legacyData, newData, assertLegacyData, assertNewData)
|
||||
}
|
||||
|
||||
func TestLegacyStorage_SearchData(t *testing.T) {
|
||||
func TestLegacyStorage_Search(t *testing.T) {
|
||||
genData := func(numMetrics int, prefix string, tr TimeRange) []MetricRow {
|
||||
mrs := make([]MetricRow, numMetrics)
|
||||
for i := range numMetrics {
|
||||
@@ -334,20 +295,14 @@ func TestLegacyStorage_SearchData(t *testing.T) {
|
||||
return mrs
|
||||
}
|
||||
const numMetrics = 1000
|
||||
tr1 := TimeRange{
|
||||
tr := TimeRange{
|
||||
MinTimestamp: time.Date(2023, 6, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
MaxTimestamp: time.Date(2024, 5, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
|
||||
}
|
||||
tr2 := TimeRange{
|
||||
MinTimestamp: time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
MaxTimestamp: time.Date(2024, 6, 30, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
|
||||
}
|
||||
legacyData := genData(numMetrics, "legacy", tr1)
|
||||
new1Data := genData(numMetrics, "new1", tr1)
|
||||
new2Data := genData(numMetrics, "new2", tr2)
|
||||
newData := slices.Concat(new1Data, new2Data)
|
||||
legacyData := genData(numMetrics, "legacy", tr)
|
||||
newData := genData(numMetrics, "new", tr)
|
||||
|
||||
assertSearchResults := func(s *Storage, tr TimeRange, want []MetricRow) {
|
||||
assertSearchResults := func(s *Storage, want []MetricRow) {
|
||||
tfsAll := NewTagFilters()
|
||||
if err := tfsAll.Add([]byte("__name__"), []byte(".*"), false, true); err != nil {
|
||||
t.Fatalf("unexpected error in TagFilters.Add: %v", err)
|
||||
@@ -359,13 +314,13 @@ func TestLegacyStorage_SearchData(t *testing.T) {
|
||||
|
||||
assertLegacyData := func(s *Storage) {
|
||||
t.Helper()
|
||||
assertSearchResults(s, tr1, legacyData)
|
||||
want := legacyData
|
||||
assertSearchResults(s, want)
|
||||
}
|
||||
assertNewData := func(s *Storage) {
|
||||
t.Helper()
|
||||
want := slices.Concat(legacyData, new1Data)
|
||||
assertSearchResults(s, tr1, want)
|
||||
assertSearchResults(s, tr2, new2Data)
|
||||
want := slices.Concat(legacyData, newData)
|
||||
assertSearchResults(s, want)
|
||||
}
|
||||
testSearchOpWithLegacyIndexDBs(t, legacyData, newData, assertLegacyData, assertNewData)
|
||||
}
|
||||
|
||||
@@ -30,12 +30,6 @@ const (
|
||||
modeWhole = 2
|
||||
)
|
||||
|
||||
const (
|
||||
// minCurrCacheSaveMissRate is the minimum miss rate of curr cache
|
||||
// for saving prev instead of curr during split mode.
|
||||
minCurrCacheSaveMissRate = 0.8
|
||||
)
|
||||
|
||||
// Cache is a cache for working set entries.
|
||||
//
|
||||
// The cache evicts inactive entries after the given expireDuration.
|
||||
@@ -47,10 +41,6 @@ type Cache struct {
|
||||
// csHistory holds cache stats history
|
||||
csHistory fastcache.Stats
|
||||
|
||||
// prevStatsAtRotation holds prev cache stats at the moment it became prev from curr.
|
||||
// It is used for calculating prev miss rate since the last cache rotation.
|
||||
prevStatsAtRotation fastcache.Stats
|
||||
|
||||
// mode indicates whether to use only curr and skip prev.
|
||||
//
|
||||
// This flag is set to modeSwitching if curr is filled for more than 50% space.
|
||||
@@ -155,7 +145,6 @@ func newCacheInternal(curr, prev *fastcache.Cache, mode, maxBytes int, expireDur
|
||||
c.maxBytes = maxBytes
|
||||
c.curr.Store(curr)
|
||||
c.prev.Store(prev)
|
||||
prev.UpdateStats(&c.prevStatsAtRotation)
|
||||
c.stopCh = make(chan struct{})
|
||||
c.mode.Store(uint32(mode))
|
||||
c.runWatchers(expireDuration)
|
||||
@@ -195,7 +184,7 @@ func (c *Cache) expirationWatcher(expireDuration time.Duration) {
|
||||
prev := c.prev.Load()
|
||||
curr := c.curr.Load()
|
||||
c.updateCacheStatsHistoryBeforeRotationLocked(prev, curr)
|
||||
c.storeCurrStatsBeforeRotationLocked(curr)
|
||||
|
||||
c.prev.Store(curr)
|
||||
prev.Reset()
|
||||
c.curr.Store(prev)
|
||||
@@ -316,7 +305,7 @@ func (c *Cache) transitIntoWholeModeLocked(maxBytesSize uint64, t *time.Ticker)
|
||||
prev := c.prev.Load()
|
||||
curr := c.curr.Load()
|
||||
c.updateCacheStatsHistoryBeforeRotationLocked(prev, curr)
|
||||
c.storeCurrStatsBeforeRotationLocked(curr)
|
||||
|
||||
c.prev.Store(curr)
|
||||
prev.Reset()
|
||||
|
||||
@@ -367,7 +356,6 @@ func (c *Cache) transitIntoWholeModeLocked(maxBytesSize uint64, t *time.Ticker)
|
||||
c.updateCacheStatsHistoryBeforeRotationLocked(prev, curr)
|
||||
|
||||
c.prev.Store(newWithAutoCleanup(1024))
|
||||
c.prevStatsAtRotation.Reset()
|
||||
prev.Reset()
|
||||
}
|
||||
|
||||
@@ -375,15 +363,14 @@ func (c *Cache) transitIntoWholeModeLocked(maxBytesSize uint64, t *time.Ticker)
|
||||
func (c *Cache) MustSave(filePath string) {
|
||||
startTime := time.Now()
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
cacheToSave, cs, cacheName := c.selectCacheToSave()
|
||||
var cs fastcache.Stats
|
||||
curr := c.curr.Load()
|
||||
curr.UpdateStats(&cs)
|
||||
|
||||
concurrency := cgroup.AvailableCPUs()
|
||||
|
||||
logger.Infof("saving %s cache to %s by using %d concurrent workers", cacheName, filePath, concurrency)
|
||||
err := cacheToSave.SaveToFileConcurrent(filePath, concurrency)
|
||||
logger.Infof("saving cache to %s by using %d concurrent workers", filePath, concurrency)
|
||||
err := curr.SaveToFileConcurrent(filePath, concurrency)
|
||||
if err != nil {
|
||||
logger.Panicf("FATAL: cannot save cache to %s: %s", filePath, err)
|
||||
}
|
||||
@@ -391,47 +378,6 @@ func (c *Cache) MustSave(filePath string) {
|
||||
logger.Infof("cache has been successfully saved to %s in %.3f seconds; entriesCount: %d, sizeBytes: %d", filePath, time.Since(startTime).Seconds(), cs.EntriesCount, cs.BytesSize)
|
||||
}
|
||||
|
||||
func (c *Cache) selectCacheToSave() (*fastcache.Cache, fastcache.Stats, string) {
|
||||
curr := c.curr.Load()
|
||||
|
||||
var csCurr fastcache.Stats
|
||||
curr.UpdateStats(&csCurr)
|
||||
|
||||
if c.mode.Load() != modeSplit {
|
||||
return curr, csCurr, "curr"
|
||||
}
|
||||
|
||||
prev := c.prev.Load()
|
||||
|
||||
var csPrev fastcache.Stats
|
||||
prev.UpdateStats(&csPrev)
|
||||
|
||||
if csPrev.EntriesCount == 0 || csPrev.GetCalls == 0 {
|
||||
return curr, csCurr, "curr"
|
||||
}
|
||||
|
||||
csPrevAtRotation := &c.prevStatsAtRotation
|
||||
prevMissRateAfterRotation := float64(1)
|
||||
if csPrev.GetCalls > csPrevAtRotation.GetCalls {
|
||||
prevGetCallsAfterRotation := csPrev.GetCalls - csPrevAtRotation.GetCalls
|
||||
prevMissesAfterRotation := uint64(0)
|
||||
if csPrev.Misses > csPrevAtRotation.Misses {
|
||||
prevMissesAfterRotation = csPrev.Misses - csPrevAtRotation.Misses
|
||||
}
|
||||
if prevMissesAfterRotation < prevGetCallsAfterRotation {
|
||||
prevMissRateAfterRotation = float64(prevMissesAfterRotation) / float64(prevGetCallsAfterRotation)
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer saving prev cache when:
|
||||
// 1. 80% requests were missed in curr cache and served by prev cache.
|
||||
// 2. less than 80% requests were missed in prev cache since the last rotation.
|
||||
if csCurr.GetCalls < 10 || (float64(csCurr.Misses)/float64(csCurr.GetCalls) > minCurrCacheSaveMissRate && prevMissRateAfterRotation < minCurrCacheSaveMissRate) {
|
||||
return prev, csPrev, "prev"
|
||||
}
|
||||
return curr, csCurr, "curr"
|
||||
}
|
||||
|
||||
// Stop stops the cache.
|
||||
//
|
||||
// The cache cannot be used after the Stop call.
|
||||
@@ -460,18 +406,12 @@ func (c *Cache) Reset() {
|
||||
// so we have to restore it into original size for split mode
|
||||
c.prev.Store(newWithAutoCleanup(c.maxBytes / 2))
|
||||
c.curr.Store(newWithAutoCleanup(c.maxBytes / 2))
|
||||
c.prevStatsAtRotation.Reset()
|
||||
|
||||
c.mode.Store(modeSplit)
|
||||
}
|
||||
|
||||
prev.Reset()
|
||||
curr.Reset()
|
||||
c.prevStatsAtRotation.Reset()
|
||||
}
|
||||
|
||||
func (c *Cache) storeCurrStatsBeforeRotationLocked(curr *fastcache.Cache) {
|
||||
c.prevStatsAtRotation.Reset()
|
||||
curr.UpdateStats(&c.prevStatsAtRotation)
|
||||
}
|
||||
|
||||
// UpdateStats updates fcs with cache stats.
|
||||
|
||||
@@ -5,7 +5,6 @@ package workingsetcache
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
@@ -166,163 +165,6 @@ func TestSetGetStatsInSplitMode_cacheLoadedFromEmptyFile(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestMustSaveSelectsCacheInSplitMode(t *testing.T) {
|
||||
t.Run("prefers prev cache if curr is rarely visited", func(t *testing.T) {
|
||||
cachePath := filepath.Join(t.TempDir(), "cache")
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
var (
|
||||
k = []byte("k")
|
||||
v = []byte("v")
|
||||
dst []byte
|
||||
)
|
||||
|
||||
c := Load(cachePath, 1024)
|
||||
c.Set(k, v)
|
||||
for range 10 {
|
||||
dst = c.Get(dst[:0], k)
|
||||
}
|
||||
|
||||
// prev and curr were rotated, k is now in prev, curr is empty.
|
||||
time.Sleep(*cacheExpireDuration + time.Minute)
|
||||
synctest.Wait()
|
||||
assertMode(t, c, modeSplit)
|
||||
|
||||
c.MustSave(cachePath)
|
||||
c.Stop()
|
||||
|
||||
c = Load(cachePath, 1024)
|
||||
defer c.Stop()
|
||||
if got := c.Get(dst[:0], k); string(got) != string(v) {
|
||||
t.Fatalf("unexpected value loaded from saved cache; got %q; want %q", got, v)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("prefers prev cache when prev is still useful", func(t *testing.T) {
|
||||
cachePath := filepath.Join(t.TempDir(), "cache")
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
const keysCount = 10
|
||||
var (
|
||||
v = []byte("v")
|
||||
dst []byte
|
||||
)
|
||||
|
||||
c := Load(cachePath, 1024)
|
||||
for i := range keysCount {
|
||||
c.Set([]byte(fmt.Sprintf("prev_%d", i)), v)
|
||||
}
|
||||
|
||||
// prev and curr were rotated, prev_0-prev_9 are now in prev, curr is empty.
|
||||
time.Sleep(*cacheExpireDuration + time.Minute)
|
||||
synctest.Wait()
|
||||
assertMode(t, c, modeSplit)
|
||||
|
||||
// all get calls are missed in curr cache, but can be served by prev cache.
|
||||
for i := range keysCount {
|
||||
dst = c.Get(dst[:0], []byte(fmt.Sprintf("prev_%d", i)))
|
||||
if string(dst) != string(v) {
|
||||
t.Fatalf("unexpected value loaded from prev cache for key %q; got %q; want %q", fmt.Sprintf("prev_%d", i), dst, v)
|
||||
}
|
||||
}
|
||||
|
||||
c.MustSave(cachePath)
|
||||
c.Stop()
|
||||
|
||||
c = Load(cachePath, 1024)
|
||||
defer c.Stop()
|
||||
for i := range keysCount {
|
||||
key := []byte(fmt.Sprintf("prev_%d", i))
|
||||
if got := c.Get(dst[:0], key); string(got) != string(v) {
|
||||
t.Fatalf("unexpected value loaded from saved cache for key %q; got %q; want %q", key, got, v)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("prefers curr cache when prev is cold", func(t *testing.T) {
|
||||
cachePath := filepath.Join(t.TempDir(), "cache")
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
const keysCount = 10
|
||||
var (
|
||||
v = []byte("v")
|
||||
dst []byte
|
||||
)
|
||||
|
||||
c := Load(cachePath, 1024)
|
||||
for i := range keysCount {
|
||||
c.Set([]byte(fmt.Sprintf("prev_%d", i)), v)
|
||||
}
|
||||
|
||||
// prev and curr were rotated, prev_0-prev_9 are now in prev, curr is empty.
|
||||
time.Sleep(*cacheExpireDuration + time.Minute)
|
||||
synctest.Wait()
|
||||
assertMode(t, c, modeSplit)
|
||||
|
||||
// all get calls are missed in both curr and prev cache.
|
||||
for i := range keysCount {
|
||||
newKey := []byte(fmt.Sprintf("new_%d", i))
|
||||
dst = c.Get(dst[:0], newKey)
|
||||
}
|
||||
|
||||
c.MustSave(cachePath)
|
||||
c.Stop()
|
||||
|
||||
c = Load(cachePath, 1024)
|
||||
defer c.Stop()
|
||||
for i := range keysCount {
|
||||
prevKey := []byte(fmt.Sprintf("prev_%d", i))
|
||||
if got := c.Get(dst[:0], prevKey); len(got) != 0 {
|
||||
t.Fatalf("unexpected prev value loaded from saved cache for key %q; got %q; want an empty value", prevKey, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("prefers curr cache when prev hits rate is low after rotation", func(t *testing.T) {
|
||||
cachePath := filepath.Join(t.TempDir(), "cache")
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
const keysCount = 10
|
||||
var (
|
||||
prevKey = []byte("prev")
|
||||
currKey = []byte("curr")
|
||||
v = []byte("v")
|
||||
dst []byte
|
||||
)
|
||||
|
||||
c := Load(cachePath, 1024)
|
||||
c.Set(prevKey, v)
|
||||
// the curr cache hit all the requests.
|
||||
for range keysCount {
|
||||
dst = c.Get(dst[:0], prevKey)
|
||||
}
|
||||
|
||||
// prev and curr were rotated, prevKey is now in prev, whose cache hit ratio is 100%.
|
||||
time.Sleep(*cacheExpireDuration + time.Minute)
|
||||
synctest.Wait()
|
||||
assertMode(t, c, modeSplit)
|
||||
|
||||
c.Set(currKey, v)
|
||||
// the prev cache miss all the requests after the rotation
|
||||
for i := range keysCount {
|
||||
newKey := []byte(fmt.Sprintf("new_%d", i))
|
||||
dst = c.Get(dst[:0], newKey)
|
||||
}
|
||||
|
||||
c.MustSave(cachePath)
|
||||
c.Stop()
|
||||
|
||||
c = Load(cachePath, 1024)
|
||||
defer c.Stop()
|
||||
if got := c.Get(dst[:0], currKey); string(got) != string(v) {
|
||||
t.Fatalf("unexpected value loaded from saved cache for key %q; got %q; want %q", currKey, got, v)
|
||||
}
|
||||
if got := c.Get(dst[:0], prevKey); len(got) != 0 {
|
||||
t.Fatalf("unexpected prev value loaded from saved cache for key %q; got %q; want an empty value", prevKey, got)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func testSetGetStatsInSplitMode(t *testing.T, c *Cache) {
|
||||
var (
|
||||
k1, v1 = []byte("k1"), []byte("v1")
|
||||
|
||||