mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-08-14 03:34:15 +03:00
Compare commits
16 Commits
nwanduka-p
...
vmagent-ma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b7fce14ed | ||
|
|
1b242a8c71 | ||
|
|
c1f3589248 | ||
|
|
389ad7933e | ||
|
|
1a1c61083d | ||
|
|
8b59f970f3 | ||
|
|
15a21d9791 | ||
|
|
abdd6d853e | ||
|
|
b4b14ede65 | ||
|
|
e31e58185c | ||
|
|
20ffe1f679 | ||
|
|
7afd7c2a16 | ||
|
|
f8f87f316b | ||
|
|
2777800bc2 | ||
|
|
398a3d74aa | ||
|
|
aa32d59cc8 |
@@ -2,11 +2,13 @@ package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -549,6 +551,33 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
procutil.SelfSIGHUP()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return true
|
||||
case "/remotewrite/maintenance":
|
||||
if !remotewrite.CheckMaintenanceAuthKey(w, r) {
|
||||
return true
|
||||
}
|
||||
remoteWriteMaintenanceRequests.Inc()
|
||||
if v := r.FormValue("enable"); v != "" {
|
||||
enable, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
httpserver.Errorf(w, r, "cannot parse `enable` query arg: %s", err)
|
||||
return true
|
||||
}
|
||||
rwURL := r.FormValue("url")
|
||||
if rwURL == "" {
|
||||
httpserver.Errorf(w, r, "missing `url` query arg; it must match the `url` label of vmagent_remotewrite_* metrics for the target -remoteWrite.url, "+
|
||||
"or be set to `*` to target all the configured -remoteWrite.url destinations")
|
||||
return true
|
||||
}
|
||||
if matched := remotewrite.SetMaintenanceMode(rwURL, enable); matched == 0 {
|
||||
httpserver.Errorf(w, r, "no -remoteWrite.url destinations match `url=%s`", rwURL)
|
||||
return true
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"maintenance": remotewrite.GetMaintenanceMode(),
|
||||
})
|
||||
return true
|
||||
case "/ready":
|
||||
if rdy := promscrape.PendingScrapeConfigs.Load(); rdy > 0 {
|
||||
errMsg := fmt.Sprintf("waiting for scrapes to init, left: %d", rdy)
|
||||
@@ -833,6 +862,8 @@ var (
|
||||
remoteWriteStatusURLRelabelConfigRequests = metrics.NewCounter(`vmagent_http_requests_total{path="/api/v1/status/remotewrite-url-relabel-config"}`)
|
||||
|
||||
promscrapeConfigReloadRequests = metrics.NewCounter(`vmagent_http_requests_total{path="/-/reload"}`)
|
||||
|
||||
remoteWriteMaintenanceRequests = metrics.NewCounter(`vmagent_http_requests_total{path="/remotewrite/maintenance"}`)
|
||||
)
|
||||
|
||||
func usage() {
|
||||
|
||||
@@ -52,6 +52,7 @@ func setUp() {
|
||||
|
||||
func tearDown() {
|
||||
protoparserutil.StopUnmarshalWorkers()
|
||||
remotewrite.Stop()
|
||||
srv.Close()
|
||||
logger.ResetOutputForTest()
|
||||
tmpDataDir := flag.Lookup("remoteWrite.tmpDataPath").Value.String()
|
||||
|
||||
@@ -97,6 +97,8 @@ type client struct {
|
||||
useVMProto atomic.Bool
|
||||
canDowngradeVMProto atomic.Bool
|
||||
|
||||
maintenanceMode atomic.Bool
|
||||
|
||||
fq *persistentqueue.FastQueue
|
||||
hc *http.Client
|
||||
|
||||
@@ -322,6 +324,12 @@ func (c *client) runWorker(readBlock func(dst []byte) ([]byte, bool)) {
|
||||
var block []byte
|
||||
ch := make(chan bool, 1)
|
||||
for {
|
||||
if c.maintenanceMode.Load() {
|
||||
if !c.waitForMaintenanceModeOff() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
block, ok = readBlock(block[:0])
|
||||
if !ok {
|
||||
return
|
||||
@@ -339,6 +347,14 @@ func (c *client) runWorker(readBlock func(dst []byte) ([]byte, bool)) {
|
||||
}
|
||||
// Return unsent block to the queue.
|
||||
c.fq.MustWriteBlockIgnoreDisabledPQ(block)
|
||||
select {
|
||||
case <-c.stopCh:
|
||||
// c must be stopped.
|
||||
default:
|
||||
// sendBlock returned false because maintenance mode is enabled, not because
|
||||
// c is stopping. Keep the worker alive so it resumes once maintenance mode is disabled.
|
||||
continue
|
||||
}
|
||||
return
|
||||
case <-c.stopCh:
|
||||
// c must be stopped. Wait up to 5 seconds for the in-flight request to complete.
|
||||
@@ -363,6 +379,23 @@ func (c *client) runWorker(readBlock func(dst []byte) ([]byte, bool)) {
|
||||
}
|
||||
}
|
||||
|
||||
// waitForMaintenanceModeOff blocks while maintenance mode is enabled for c.
|
||||
//
|
||||
// It returns false only if c.stopCh is closed while waiting.
|
||||
func (c *client) waitForMaintenanceModeOff() bool {
|
||||
t := time.NewTicker(time.Second)
|
||||
defer t.Stop()
|
||||
|
||||
for c.maintenanceMode.Load() {
|
||||
select {
|
||||
case <-t.C:
|
||||
case <-c.stopCh:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *client) doRequest(url string, body []byte) (*http.Response, error) {
|
||||
req, err := c.newRequest(url, body)
|
||||
if err != nil {
|
||||
@@ -421,7 +454,7 @@ func (c *client) newRequest(url string, body []byte) (*http.Request, error) {
|
||||
|
||||
// sendBlockHTTP sends the given block to c.remoteWriteURL.
|
||||
//
|
||||
// The function returns false only if c.stopCh is closed.
|
||||
// The function returns false if c.stopCh is closed or if maintenance mode is enabled for c.
|
||||
// Otherwise, it tries sending the block to remote storage indefinitely.
|
||||
func (c *client) sendBlockHTTP(block []byte) bool {
|
||||
c.rl.Register(len(block))
|
||||
@@ -429,6 +462,10 @@ func (c *client) sendBlockHTTP(block []byte) bool {
|
||||
retriesCount := 0
|
||||
|
||||
again:
|
||||
if c.maintenanceMode.Load() {
|
||||
return false
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
resp, err := c.doRequest(c.remoteWriteURL, block)
|
||||
c.requestDuration.UpdateDuration(startTime)
|
||||
@@ -511,12 +548,7 @@ again:
|
||||
// Handle response
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
if err != nil {
|
||||
logger.Errorf("cannot read response body from %q during retry #%d: %s", c.sanitizedURL, retriesCount, err)
|
||||
} else {
|
||||
logger.Errorf("unexpected status code received after sending a block with size %d bytes to %q during retry #%d: %d; response body=%q; "+
|
||||
"re-sending the block in %s", len(block), c.sanitizedURL, retriesCount, statusCode, body, bt.CurrentDelay())
|
||||
}
|
||||
logUnexpectedStatusCode(block, c.sanitizedURL, statusCode, retriesCount, bt.CurrentDelay(), retryAfterHeader > 0, body, err)
|
||||
if !bt.Wait(c.stopCh) {
|
||||
return false
|
||||
}
|
||||
@@ -552,6 +584,26 @@ func (c *client) drainInMemoryQueue(stopCtx context.Context, block []byte) {
|
||||
|
||||
var remoteWriteRejectedLogger = logger.WithThrottler("remoteWriteRejected", 5*time.Second)
|
||||
var remoteWriteRetryLogger = logger.WithThrottler("remoteWriteRetry", 5*time.Second)
|
||||
var remoteWriteUnexpectedStatusLogger = logger.WithThrottler("remoteWriteUnexpectedStatus", 5*time.Second)
|
||||
|
||||
func logUnexpectedStatusCode(block []byte, sanitizedURL string, statusCode, retriesCount int, retryDelay time.Duration, isExpectedBackoff bool, body []byte, bodyErr error) {
|
||||
if bodyErr != nil {
|
||||
remoteWriteUnexpectedStatusLogger.Errorf("cannot read response body from %q during retry #%d: %s", sanitizedURL, retriesCount, bodyErr)
|
||||
return
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("unexpected status code received after sending a block with size %d bytes to %q during retry #%d: %d; response body=%q; "+
|
||||
"re-sending the block in %s", len(block), sanitizedURL, retriesCount, statusCode, body, retryDelay)
|
||||
|
||||
if isExpectedBackoff {
|
||||
// The remote storage explicitly signaled backoff duration via the Retry-After header,
|
||||
// so this isn't an anomaly worth an ERROR log.
|
||||
remoteWriteUnexpectedStatusLogger.Warnf("%s", msg)
|
||||
return
|
||||
}
|
||||
|
||||
remoteWriteUnexpectedStatusLogger.Errorf("%s", msg)
|
||||
}
|
||||
|
||||
// repackBlockFromZstdToSnappy repacks the given zstd-compressed block to snappy-compressed block.
|
||||
//
|
||||
|
||||
@@ -113,6 +113,7 @@ var (
|
||||
"Multiple label names should be separated by `^^`, e.g. \"job^^instance,ip\". "+
|
||||
"Can be combined with -remoteWrite.mdx.enable to hide sensitive label values in VictoriaMetrics self-monitoring metrics. "+
|
||||
"Please see https://docs.victoriametrics.com/victoriametrics/vmagent/#obfuscating-label-values")
|
||||
maintenanceAuthKey = flagutil.NewPassword("remoteWrite.maintenanceAuthKey", "Auth key for /remotewrite/maintenance http endpoint. It must be passed via authKey query arg. It overrides -httpAuth.*")
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -141,6 +142,42 @@ func MultitenancyEnabled() bool {
|
||||
return *enableMultitenantHandlers
|
||||
}
|
||||
|
||||
// SetMaintenanceMode enables or disables maintenance mode for the remote write destination(s)
|
||||
// identified by sanitizedURL, which must match the `url` label of the corresponding
|
||||
// vmagent_remotewrite_* metrics. Pass "*" to target all the configured -remoteWrite.url destinations.
|
||||
//
|
||||
// While a destination is in maintenance mode, vmagent doesn't attempt to send data to it at all -
|
||||
// it stops draining that destination's queue; buffering remains subject to the configured queue limits.
|
||||
//
|
||||
// It returns the number of destinations matched by sanitizedURL.
|
||||
func SetMaintenanceMode(sanitizedURL string, enabled bool) int {
|
||||
matched := 0
|
||||
for _, rwctx := range rwctxsGlobal {
|
||||
if sanitizedURL != "*" && rwctx.c.sanitizedURL != sanitizedURL {
|
||||
continue
|
||||
}
|
||||
rwctx.c.maintenanceMode.Store(enabled)
|
||||
matched++
|
||||
}
|
||||
return matched
|
||||
}
|
||||
|
||||
// GetMaintenanceMode returns the maintenance mode status for each configured -remoteWrite.url,
|
||||
// keyed by its sanitized URL.
|
||||
func GetMaintenanceMode() map[string]bool {
|
||||
m := make(map[string]bool, len(rwctxsGlobal))
|
||||
for _, rwctx := range rwctxsGlobal {
|
||||
m[rwctx.c.sanitizedURL] = rwctx.c.maintenanceMode.Load()
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// CheckMaintenanceAuthKey verifies the authKey query arg for the /remotewrite/maintenance http endpoint
|
||||
// against -remoteWrite.maintenanceAuthKey. See httpserver.CheckAuthFlag for the semantics of the return value.
|
||||
func CheckMaintenanceAuthKey(w http.ResponseWriter, r *http.Request) bool {
|
||||
return httpserver.CheckAuthFlag(w, r, maintenanceAuthKey)
|
||||
}
|
||||
|
||||
// Contains the current relabelConfigs.
|
||||
var allRelabelConfigs atomic.Pointer[relabelConfigs]
|
||||
|
||||
@@ -246,6 +283,8 @@ func Init() {
|
||||
dropDanglingQueues()
|
||||
|
||||
// Start config reloader.
|
||||
configReloaderStopCh = make(chan struct{})
|
||||
configReloaderWG = sync.WaitGroup{}
|
||||
configReloaderWG.Go(func() {
|
||||
for {
|
||||
select {
|
||||
@@ -332,7 +371,7 @@ func initRemoteWriteCtxs(urls []string) {
|
||||
}
|
||||
|
||||
var (
|
||||
configReloaderStopCh = make(chan struct{})
|
||||
configReloaderStopCh chan struct{}
|
||||
configReloaderWG sync.WaitGroup
|
||||
)
|
||||
|
||||
|
||||
@@ -290,6 +290,8 @@ func (g *Group) updateWith(newGroup *Group) error {
|
||||
g.Headers = newGroup.Headers
|
||||
g.NotifierHeaders = newGroup.NotifierHeaders
|
||||
g.Labels = newGroup.Labels
|
||||
g.EvalDelay = newGroup.EvalDelay
|
||||
g.evalAlignment = newGroup.evalAlignment
|
||||
g.Limit = newGroup.Limit
|
||||
g.checksum = newGroup.checksum
|
||||
g.Rules = newRules
|
||||
@@ -337,7 +339,7 @@ func (g *Group) Init() {
|
||||
i := g.Interval.Seconds()
|
||||
return i
|
||||
})
|
||||
g.metrics.iterationLimit = g.metrics.set.NewGauge(fmt.Sprintf(`vmalert_rule_group_results_limit{%s}`, labels), func() float64 {
|
||||
g.metrics.iterationLimit = g.metrics.set.NewGauge(fmt.Sprintf(`vmalert_group_rule_results_limit{%s}`, labels), func() float64 {
|
||||
g.mu.RLock()
|
||||
limit := g.Limit
|
||||
g.mu.RUnlock()
|
||||
|
||||
@@ -78,6 +78,12 @@ func TestUpdateWith(t *testing.T) {
|
||||
if g.Debug != expect.Debug {
|
||||
t.Fatalf("expected to have debug %v; got %v", expect.Debug, g.Debug)
|
||||
}
|
||||
if !durationPtrEqual(g.EvalDelay, expect.EvalDelay) {
|
||||
t.Fatalf("expected to have eval_delay %v; got %v", expect.EvalDelay, g.EvalDelay)
|
||||
}
|
||||
if !boolPtrEqual(g.evalAlignment, expect.evalAlignment) {
|
||||
t.Fatalf("expected to have eval_alignment %v; got %v", expect.evalAlignment, g.evalAlignment)
|
||||
}
|
||||
}
|
||||
|
||||
// new rule
|
||||
@@ -237,6 +243,37 @@ func TestUpdateWith(t *testing.T) {
|
||||
{Alert: "foo1", Debug: &debug},
|
||||
},
|
||||
})
|
||||
|
||||
// update group evaluation settings
|
||||
evalDelay := promutil.NewDuration(time.Minute)
|
||||
evalAlignment := false
|
||||
f(config.Group{
|
||||
Rules: []config.Rule{{
|
||||
Record: "foo",
|
||||
Expr: "max(up)",
|
||||
}},
|
||||
}, config.Group{
|
||||
EvalDelay: evalDelay,
|
||||
EvalAlignment: &evalAlignment,
|
||||
Rules: []config.Rule{{
|
||||
Record: "foo",
|
||||
Expr: "min(up)",
|
||||
}},
|
||||
})
|
||||
}
|
||||
|
||||
func durationPtrEqual(a, b *time.Duration) bool {
|
||||
if a == nil || b == nil {
|
||||
return a == b
|
||||
}
|
||||
return *a == *b
|
||||
}
|
||||
|
||||
func boolPtrEqual(a, b *bool) bool {
|
||||
if a == nil || b == nil {
|
||||
return a == b
|
||||
}
|
||||
return *a == *b
|
||||
}
|
||||
|
||||
func TestUpdateDuringRandSleep(t *testing.T) {
|
||||
|
||||
@@ -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("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)
|
||||
return fmt.Errorf("delete API does not support specific time ranges using start and end args, the series can only be deleted completely")
|
||||
}
|
||||
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 err
|
||||
return httpserver.InvalidParamError(err)
|
||||
}
|
||||
limit, err := httputil.GetInt(r, "limit")
|
||||
if err != nil {
|
||||
return err
|
||||
return httpserver.InvalidParamError(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 err
|
||||
return httpserver.InvalidParamError(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 fmt.Errorf("cannot parse `date` arg %q: %w", dateStr, err)
|
||||
return httpserver.InvalidParamError(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 fmt.Errorf("cannot parse `topN` arg %q: %w", topNStr, err)
|
||||
return httpserver.InvalidParamError(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 err
|
||||
return httpserver.InvalidParamError(err)
|
||||
}
|
||||
limit, err := httputil.GetInt(r, "limit")
|
||||
if err != nil {
|
||||
return err
|
||||
return httpserver.InvalidParamError(err)
|
||||
}
|
||||
sq := storage.NewSearchQuery(cp.start, cp.end, cp.filterss, *maxLabelsAPISeries)
|
||||
labels, err := netstorage.LabelNames(qt, sq, limit, cp.deadline)
|
||||
@@ -671,10 +671,9 @@ 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 err
|
||||
return httpserver.InvalidParamError(err)
|
||||
}
|
||||
if limit < 0 {
|
||||
limit = 0
|
||||
@@ -734,11 +733,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 err
|
||||
return httpserver.InvalidParamError(err)
|
||||
}
|
||||
limit, err := httputil.GetInt(r, "limit")
|
||||
if err != nil {
|
||||
return err
|
||||
return httpserver.InvalidParamError(err)
|
||||
}
|
||||
|
||||
sq := storage.NewSearchQuery(cp.start, cp.end, cp.filterss, *maxSeriesLimit)
|
||||
@@ -772,19 +771,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 fmt.Errorf("missing `query` arg")
|
||||
return httpserver.InvalidParamError(fmt.Errorf("missing `query` arg"))
|
||||
}
|
||||
start, err := httputil.GetTime(r, "time", ct)
|
||||
if err != nil {
|
||||
return err
|
||||
return httpserver.InvalidParamError(err)
|
||||
}
|
||||
lookbackDelta, err := getMaxLookback(r)
|
||||
if err != nil {
|
||||
return err
|
||||
return httpserver.InvalidParamError(err)
|
||||
}
|
||||
step, err := httputil.GetDuration(r, "step", lookbackDelta)
|
||||
if err != nil {
|
||||
return err
|
||||
return httpserver.InvalidParamError(err)
|
||||
}
|
||||
if step <= 0 {
|
||||
step = defaultStep
|
||||
@@ -792,16 +791,16 @@ func QueryHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWr
|
||||
|
||||
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)
|
||||
return httpserver.InvalidParamError(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 err
|
||||
return httpserver.InvalidParamError(err)
|
||||
}
|
||||
if childQuery, windowExpr, offsetExpr := promql.IsMetricSelectorWithRollup(query); childQuery != "" {
|
||||
window, err := windowExpr.NonNegativeDuration(step)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot parse lookbehind window in square brackets at %s: %w", query, err)
|
||||
return httpserver.InvalidParamError(fmt.Errorf("cannot parse lookbehind window in square brackets at %s: %w", query, err))
|
||||
}
|
||||
offset := offsetExpr.Duration(step)
|
||||
start -= offset
|
||||
@@ -815,7 +814,7 @@ func QueryHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWr
|
||||
|
||||
tagFilterss, err := getTagFilterssFromMatches([]string{childQuery})
|
||||
if err != nil {
|
||||
return err
|
||||
return httpserver.InvalidParamError(err)
|
||||
}
|
||||
filterss := searchutil.JoinTagFilterss(tagFilterss, etfs)
|
||||
|
||||
@@ -831,22 +830,25 @@ 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 fmt.Errorf("cannot parse step in square brackets at %s: %w", query, err)
|
||||
return httpserver.InvalidParamError(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 fmt.Errorf("cannot parse lookbehind window in square brackets at %s: %w", query, err)
|
||||
return httpserver.InvalidParamError(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, r, ct, etfs); err != nil {
|
||||
if err := queryRangeHandler(qt, startTime, w, childQuery, start, end, step, lookbackDelta, 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
|
||||
@@ -854,7 +856,7 @@ func QueryHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWr
|
||||
|
||||
queryOffset, err := getLatencyOffsetMilliseconds(r)
|
||||
if err != nil {
|
||||
return err
|
||||
return httpserver.InvalidParamError(err)
|
||||
}
|
||||
if !httputil.GetBool(r, "nocache") && ct-start < queryOffset && start-ct < queryOffset {
|
||||
// Adjust start time only if `nocache` arg isn't set.
|
||||
@@ -928,45 +930,43 @@ func QueryRangeHandler(qt *querytracer.Tracer, startTime time.Time, w http.Respo
|
||||
ct := startTime.UnixNano() / 1e6
|
||||
query := r.FormValue("query")
|
||||
if len(query) == 0 {
|
||||
return fmt.Errorf("missing `query` arg")
|
||||
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))
|
||||
}
|
||||
start, err := httputil.GetTime(r, "start", ct-defaultStep)
|
||||
if err != nil {
|
||||
return err
|
||||
return httpserver.InvalidParamError(err)
|
||||
}
|
||||
end, err := httputil.GetTime(r, "end", ct)
|
||||
if err != nil {
|
||||
return err
|
||||
return httpserver.InvalidParamError(err)
|
||||
}
|
||||
step, err := httputil.GetDuration(r, "step", defaultStep)
|
||||
if err != nil {
|
||||
return err
|
||||
return httpserver.InvalidParamError(err)
|
||||
}
|
||||
etfs, err := searchutil.GetExtraTagFilters(r)
|
||||
if err != nil {
|
||||
return err
|
||||
return httpserver.InvalidParamError(err)
|
||||
}
|
||||
if err := queryRangeHandler(qt, startTime, w, query, start, end, step, r, ct, etfs); err != nil {
|
||||
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 {
|
||||
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 int64, r *http.Request, ct int64, etfs [][]storage.TagFilter) error {
|
||||
start, end, step, lookbackDelta 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 err
|
||||
return httpserver.InvalidParamError(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 fmt.Errorf("cannot parse `topN` arg %q: %w", topNStr, err)
|
||||
return httpserver.InvalidParamError(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 fmt.Errorf("cannot parse `maxLifetime` arg: %w", err)
|
||||
return httpserver.InvalidParamError(fmt.Errorf("cannot parse `maxLifetime` arg: %w", err))
|
||||
}
|
||||
maxLifetime := time.Duration(maxLifetimeMsecs) * time.Millisecond
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
@@ -11,6 +11,7 @@ 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"
|
||||
@@ -46,15 +47,15 @@ func Exec(qt *querytracer.Tracer, ec *EvalConfig, q string, isFirstPointOnly boo
|
||||
|
||||
e, err := parsePromQLWithCache(q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, httpserver.InvalidParamError(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, 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, 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"))
|
||||
}
|
||||
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))
|
||||
|
||||
@@ -32,8 +32,15 @@ See also [LTS releases](https://docs.victoriametrics.com/victoriametrics/lts-rel
|
||||
* 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).
|
||||
|
||||
* 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/) 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)
|
||||
|
||||
@@ -448,8 +455,6 @@ 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).
|
||||
@@ -860,8 +865,6 @@ 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,15 +6,16 @@ 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 the particular time period;
|
||||
- check how the system behaves at a particular time period;
|
||||
- correlate behavior changes to other measurements;
|
||||
- observe or forecast trends;
|
||||
- trigger events (alerts) if the metric exceeds a threshold.
|
||||
@@ -25,7 +26,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 supposed to clarify
|
||||
or `request_errors_total` (for requests which failed). Choosing a metric name is very important and is supposed to clarify
|
||||
what is actually measured to every person who reads it, just like **variable names** in programming.
|
||||
|
||||
#### Labels
|
||||
@@ -54,14 +55,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 of label filters for [query API](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#prometheus-querying-api-enhancements)
|
||||
VictoriaMetrics supports enforcing label filters for the [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 `code` label.
|
||||
are two different time series because they have different values for the `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
|
||||
@@ -69,8 +70,8 @@ See [what is an active time series](https://docs.victoriametrics.com/victoriamet
|
||||
|
||||
#### Cardinality
|
||||
|
||||
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.
|
||||
The number of unique [time series](#time-series) is named `cardinality`. Having too many unique time series is named `high cardinality`.
|
||||
High cardinality may result in increased resource usage in VictoriaMetrics.
|
||||
See [these docs](https://docs.victoriametrics.com/victoriametrics/faq/#what-is-high-cardinality) for more details.
|
||||
|
||||
#### Raw samples
|
||||
@@ -108,13 +109,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 update each `30s`.
|
||||
This means, its resolution is also a `30s`.
|
||||
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`.
|
||||
|
||||
> 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
|
||||
> samples timestamps and is controlled by a client (metrics collector).
|
||||
> sample 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 +127,10 @@ type exists specifically to help users to understand how the metric was measured
|
||||
|
||||
#### Counter
|
||||
|
||||
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`,
|
||||
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`,
|
||||
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 start.
|
||||
So, the `counter` metric shows the number of observed events since the service started.
|
||||
|
||||
In programming, `counter` is a variable that you **increment** each time something happens.
|
||||
|
||||
@@ -139,7 +140,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.
|
||||
|
||||
Counter is used for measuring the number of events, like the number of requests, errors, logs, messages, etc.
|
||||
A 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 +149,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, `request_duration_seconds_sum` counter may sum the durations of all the requests.
|
||||
It is OK to have fractional counters. For example, the `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 +163,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 +179,11 @@ and [rollup functions](https://docs.victoriametrics.com/victoriametrics/metricsq
|
||||
|
||||
#### Histogram
|
||||
|
||||
Histogram is a set of [counter](#counter) metrics with different `vmrange` or `le` labels.
|
||||
A 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 `_bucket` suffix in their names.
|
||||
Histogram buckets usually have a `_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 +201,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 `_bucket` suffix allow estimating arbitrary percentile
|
||||
The counters ending with the `_bucket` suffix allow estimating arbitrary percentiles
|
||||
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 each query during the last hour (see `1h` in square brackets):
|
||||
on the number of rows read per 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 +216,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 99th percentile over `vmrange` buckets returned at step 2.
|
||||
1. The `histogram_quantile(0.99, ...)` calculates the 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 +234,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 Go application in the following way
|
||||
The `vm_rows_read_per_query` histogram may be used in a Go application in the following way
|
||||
by using the [github.com/VictoriaMetrics/metrics](https://github.com/VictoriaMetrics/metrics) package:
|
||||
|
||||
```go
|
||||
@@ -246,7 +247,7 @@ for _, query := range queries {
|
||||
}
|
||||
```
|
||||
|
||||
Now let's see what happens each time when `rowsReadPerQuery.Update` is called:
|
||||
Now let's see what happens each time `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 +263,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 +272,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,7 +304,7 @@ 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 quantile over multiple summary metrics, e.g. `sum(go_gc_duration_seconds{quantile="0.75"})`,
|
||||
- It is impossible to calculate a 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 [this article](https://latencytipoftheday.blogspot.de/2014/06/latencytipoftheday-you-cant-average.html) for details.
|
||||
@@ -313,16 +314,16 @@ Such an approach makes summaries easier to use but also puts significant limitat
|
||||
- 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 are these metrics, what do they measure, and how - all this depends on the application which emits them.
|
||||
What these metrics are, what they measure, and how - all these depend on the application which emits them.
|
||||
|
||||
To instrument your application with metrics compatible with VictoriaMetrics we recommend
|
||||
using [github.com/VictoriaMetrics/metrics](https://github.com/VictoriaMetrics/metrics) package.
|
||||
To instrument your application with metrics compatible with VictoriaMetrics, we recommend
|
||||
using the [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 +332,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 the convention helps to keep names meaningful, descriptive, and clear to other people.
|
||||
Following convention is a good practice.
|
||||
But this convention helps to keep names meaningful, descriptive, and clear to other people.
|
||||
Following the 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 big number of labels.
|
||||
Otherwise, it would be difficult to deal with measurements containing a large 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 `-maxLabelsPerTimeseries` command-line flag if necessary (but this isn't recommended).
|
||||
This limit can be changed via the `-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's value size with 4KiB. This limit can be changed via `-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 values to 4KiB. This limit can be changed via the `-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 +357,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 [single-server](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/)
|
||||
Multi-tenancy can be emulated for the [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 +392,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 push model:
|
||||
The pros of the push model:
|
||||
|
||||
* Simpler configuration at VictoriaMetrics side - there is no need to configure VictoriaMetrics with locations of the monitored applications.
|
||||
There is no need in complex [service discovery schemes](https://docs.victoriametrics.com/victoriametrics/sd_configs/).
|
||||
There is no need for 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,18 +407,18 @@ 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
|
||||
|
||||
Pull model is an approach popularized by [Prometheus](https://prometheus.io/), where the monitoring system decides when
|
||||
The pull model is an approach popularized by [Prometheus](https://prometheus.io/), where the monitoring system decides when
|
||||
and where to pull metrics from:
|
||||
|
||||

|
||||
|
||||
In pull model, the monitoring system needs to be aware of all the applications it needs to monitor. The metrics are
|
||||
In the 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 -
|
||||
@@ -431,7 +432,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`.
|
||||
* Monitoring system controls the frequency of metrics' scrape, so it is easier to control its load.
|
||||
* The monitoring system controls the frequency of metrics scraping, 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 +449,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 +481,7 @@ The API consists of two main handlers for serving [instant queries](#instant-que
|
||||
|
||||
### Instant query
|
||||
|
||||
Instant query executes the `query` expression at the given `time`:
|
||||
An instant query executes the `query` expression at the given `time`:
|
||||
|
||||
```
|
||||
GET | POST /api/v1/query?query=...&time=...&step=...&timeout=...
|
||||
@@ -497,13 +498,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`. 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.
|
||||
* `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.
|
||||
|
||||
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`.
|
||||
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`.
|
||||
|
||||
To understand how instant queries work, let's begin with a data sample:
|
||||
|
||||
@@ -530,7 +531,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 +596,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 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.
|
||||
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.
|
||||
|
||||
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
|
||||
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
|
||||
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 +706,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 the following:
|
||||
this request in VictoriaMetrics, the graph will be shown as follows:
|
||||
|
||||

|
||||
{width="500"}
|
||||
@@ -720,13 +721,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, lookbehind window isn't equal to the `step` parameter. It is calculated as the median of the intervals between
|
||||
queries, the 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 +735,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 on the time interval;
|
||||
* Correlate changes between multiple metrics over 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 +746,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 from non-consistent results due to the fact that only part of the values are scraped in the last scrape interval.
|
||||
This flag prevents inconsistent 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 +759,12 @@ duration throughout the `-search.latencyOffset` duration:
|
||||

|
||||
{width="1000"}
|
||||
|
||||
It can be overridden on per-query basis via `latency_offset` query arg.
|
||||
It can be overridden on a per-query basis via the `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 `-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 the `-search.latencyOffset` command-line flag is set to 0,
|
||||
or if `latency_offset` query arg is set to 0.
|
||||
You can send GET request to `/internal/force_flush` http handler at single-node VictoriaMetrics
|
||||
You can send a GET request to the `/internal/force_flush` HTTP handler at a 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,7 +772,7 @@ performance and increase resource usage.
|
||||
|
||||
### MetricsQL
|
||||
|
||||
VictoriaMetrics provide a special query language for executing read queries - [MetricsQL](https://docs.victoriametrics.com/victoriametrics/metricsql/).
|
||||
VictoriaMetrics provides 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
|
||||
@@ -779,7 +780,7 @@ described [here](https://valyala.medium.com/promql-tutorial-for-beginners-9ab455
|
||||
|
||||
#### 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 +794,14 @@ requests_total{path="/", code="200"}
|
||||
requests_total{path="/", code="403"}
|
||||
```
|
||||
|
||||
To select only time series with specific label value specify the matching filter in curly braces:
|
||||
To select only time series with a 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 label value. For negative match use `!=` operator. Filters also support positive regex matching via `=~`
|
||||
match the label value. For negative matches, use the `!=` operator. Filters also support positive regex matching via `=~`
|
||||
and negative regex matching via `!~`:
|
||||
|
||||
```metricsql
|
||||
@@ -813,7 +814,7 @@ Filters can also be combined:
|
||||
requests_total{code=~"200", path="/home"}
|
||||
```
|
||||
|
||||
The query above returns all time series with `requests_total` name, which simultaneously have labels `code="200"` and `path="/home"`.
|
||||
The query above returns all time series with the `requests_total` name, which simultaneously have labels `code="200"` and `path="/home"`.
|
||||
|
||||
#### Filtering by name
|
||||
|
||||
@@ -829,7 +830,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, which match at least one of multiple "or" filters.
|
||||
[MetricsQL](https://docs.victoriametrics.com/victoriametrics/metricsql/) supports selecting time series that 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 +839,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 `and` operation, e.g. they select series simultaneously matching all the filters in the group.
|
||||
Per-group filters are applied with the `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 +850,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 performance PoV
|
||||
If you need to select series matching multiple filters for the same label, then it is better from a performance PoV
|
||||
to use regexp filter `{label=~"value1|...|valueN"}` instead of `{label="value1" or ... or label="valueN"}`.
|
||||
|
||||
|
||||
@@ -878,8 +879,8 @@ 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 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, 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
|
||||
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 [these docs](https://prometheus.io/docs/prometheus/latest/querying/operators/#vector-matching) for details.
|
||||
@@ -896,7 +897,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 time series with only matching data points. For instance, the following query would return
|
||||
comparison operation is a 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 +907,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 per each group. For instance, the following query returns
|
||||
given aggregation function is applied individually to each group. For instance, the following query returns
|
||||
summary memory usage for each `job`:
|
||||
|
||||
```metricsql
|
||||
@@ -919,14 +920,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
|
||||
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:
|
||||
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:
|
||||
|
||||
```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` param
|
||||
By default, VictoriaMetrics calculates the `rate` over [raw samples](#raw-samples) on the lookbehind window specified in the `step` parameter
|
||||
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 +936,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 metric name while leaving all the labels for the inner time series. If you need to keep the metric name,
|
||||
`rate` strips the 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 +953,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 `http://victoriametrics:8428/vmui` page, type the query and see the results:
|
||||
Open the `http://victoriametrics:8428/vmui` page, type the query, and see the results:
|
||||
|
||||

|
||||
|
||||
@@ -963,8 +964,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 applies some limitations on data
|
||||
updates. In short, modifying already written [time series](#time-series) requires re-writing the whole data block where
|
||||
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
|
||||
it is stored. Due to this limitation, VictoriaMetrics does not support direct data modification.
|
||||
|
||||
### Deletion
|
||||
|
||||
@@ -488,7 +488,8 @@ func isProtectedByAuthFlag(path string) bool {
|
||||
return strings.HasSuffix(path, "/config") || strings.HasSuffix(path, "/reload") ||
|
||||
strings.HasSuffix(path, "/resetRollupResultCache") || strings.HasSuffix(path, "/delSeries") || strings.HasSuffix(path, "/delete_series") ||
|
||||
strings.HasSuffix(path, "/force_merge") || strings.HasSuffix(path, "/force_flush") || strings.HasSuffix(path, "/snapshot") ||
|
||||
strings.HasPrefix(path, "/snapshot/") || strings.HasSuffix(path, "/admin/status/metric_names_stats/reset")
|
||||
strings.HasPrefix(path, "/snapshot/") || strings.HasSuffix(path, "/admin/status/metric_names_stats/reset") ||
|
||||
strings.HasSuffix(path, "/remotewrite/maintenance")
|
||||
}
|
||||
|
||||
// CheckAuthFlag checks whether the given authKey is set and valid
|
||||
|
||||
@@ -5,9 +5,18 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
func SendPrometheusError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
errStr := err.Error()
|
||||
logHTTPError(r, errStr)
|
||||
|
||||
@@ -2,6 +2,7 @@ package netutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
@@ -41,7 +42,7 @@ func newStatDialFunc(metricPrefix string, dialFunc func(ctx context.Context, net
|
||||
sc.dialsTotal.Inc()
|
||||
if err != nil {
|
||||
sc.dialErrors.Inc()
|
||||
if !TCP6Enabled() && !isTCPv4Addr(addr) {
|
||||
if !TCP6Enabled() && !isTCPv4Addr(addr) && !isUnixSocketDialError(err) {
|
||||
err = fmt.Errorf("%w; try -enableTCP6 command-line flag for dialing ipv6 addresses", err)
|
||||
}
|
||||
return nil, err
|
||||
@@ -52,6 +53,14 @@ 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,9 +1,38 @@
|
||||
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()
|
||||
|
||||
@@ -37,8 +37,9 @@ type containerNetworkSettings struct {
|
||||
}
|
||||
|
||||
type containerNetwork struct {
|
||||
IPAddress string
|
||||
NetworkID string
|
||||
GlobalIPv6Address string
|
||||
IPAddress string
|
||||
NetworkID string
|
||||
}
|
||||
|
||||
func getContainersLabels(cfg *apiConfig) ([]*promutil.Labels, error) {
|
||||
@@ -118,14 +119,18 @@ 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(n.IPAddress, p.PrivatePort))
|
||||
m.Add("__meta_docker_network_ip", n.IPAddress)
|
||||
m.Add("__address__", discoveryutil.JoinHostPort(ipAddress, p.PrivatePort))
|
||||
m.Add("__meta_docker_network_ip", 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))
|
||||
@@ -141,11 +146,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(n.IPAddress, defaultPort)
|
||||
addr = discoveryutil.JoinHostPort(ipAddress, defaultPort)
|
||||
}
|
||||
m := promutil.NewLabels(16)
|
||||
m.Add("__address__", addr)
|
||||
m.Add("__meta_docker_network_ip", n.IPAddress)
|
||||
m.Add("__meta_docker_network_ip", ipAddress)
|
||||
addCommonLabels(m, c, networkLabels[n.NetworkID])
|
||||
// Remove possible duplicate labels, which can appear after addCommonLabels() call
|
||||
m.RemoveDuplicates()
|
||||
|
||||
@@ -431,6 +431,96 @@ 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) {
|
||||
|
||||
@@ -197,6 +197,11 @@ 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,6 +140,26 @@ 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]}
|
||||
|
||||
@@ -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[string]int64
|
||||
legacyMinMissingTimestampByKey map[TenantToken]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[string]int64),
|
||||
legacyMinMissingTimestampByKey: make(map[TenantToken]int64),
|
||||
id: id,
|
||||
tr: tr,
|
||||
name: name,
|
||||
@@ -508,6 +508,11 @@ 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)
|
||||
@@ -710,6 +715,11 @@ 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 = ""
|
||||
@@ -958,6 +968,11 @@ 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)
|
||||
@@ -1092,6 +1107,11 @@ 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.
|
||||
@@ -1273,6 +1293,11 @@ 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)
|
||||
@@ -1720,6 +1745,11 @@ 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)
|
||||
@@ -1803,6 +1833,11 @@ 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)
|
||||
@@ -2219,18 +2254,12 @@ 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,7 +7,6 @@ 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"
|
||||
@@ -87,48 +86,61 @@ func mustOpenLegacyIndexDB(path string, s *Storage) *legacyIndexDB {
|
||||
return legacyIDB
|
||||
}
|
||||
|
||||
func (is *indexSearch) legacyContainsTimeRange(tr TimeRange) bool {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// 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,
|
||||
}
|
||||
db.legacyMinMissingTimestampByKeyLock.Lock()
|
||||
minMissingTimestamp, ok := db.legacyMinMissingTimestampByKey[string(key)]
|
||||
minMissingTimestamp, ok := db.legacyMinMissingTimestampByKey[key]
|
||||
db.legacyMinMissingTimestampByKeyLock.Unlock()
|
||||
|
||||
if ok && tr.MinTimestamp >= minMissingTimestamp {
|
||||
// Fast path.
|
||||
return false
|
||||
}
|
||||
if is.legacyContainsTimeRangeSlow(kb, tr) {
|
||||
|
||||
// Slow path.
|
||||
is := db.getIndexSearch(noDeadline)
|
||||
defer db.putIndexSearch(is)
|
||||
if is.legacyContainsTimeRange(tr) {
|
||||
return true
|
||||
}
|
||||
|
||||
db.legacyMinMissingTimestampByKeyLock.Lock()
|
||||
minMissingTimestamp, ok = db.legacyMinMissingTimestampByKey[string(key)]
|
||||
minMissingTimestamp, ok = db.legacyMinMissingTimestampByKey[key]
|
||||
if !ok || tr.MinTimestamp < minMissingTimestamp {
|
||||
db.legacyMinMissingTimestampByKey[string(key)] = tr.MinTimestamp
|
||||
db.legacyMinMissingTimestampByKey[key] = tr.MinTimestamp
|
||||
}
|
||||
db.legacyMinMissingTimestampByKeyLock.Unlock()
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (is *indexSearch) legacyContainsTimeRangeSlow(prefixBuf *bytesutil.ByteBuffer, tr TimeRange) bool {
|
||||
ts := &is.ts
|
||||
|
||||
func (is *indexSearch) legacyContainsTimeRange(tr TimeRange) bool {
|
||||
// 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`.
|
||||
@@ -136,12 +148,17 @@ func (is *indexSearch) legacyContainsTimeRangeSlow(prefixBuf *bytesutil.ByteBuff
|
||||
// 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
|
||||
prefix := prefixBuf.B
|
||||
prefixBuf.B = encoding.MarshalUint64(prefixBuf.B, minDate)
|
||||
ts.Seek(prefixBuf.B)
|
||||
|
||||
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)
|
||||
if !ts.NextItem() {
|
||||
if err := ts.Error(); err != nil {
|
||||
logger.Panicf("FATAL: error when searching for minDate=%d, prefix %q: %s", minDate, prefixBuf.B, err)
|
||||
logger.Panicf("FATAL: error when searching for minDate=%d, prefix %q: %s", minDate, kb.B, err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -29,11 +29,7 @@ func TestLegacyContainsTimeRange(t *testing.T) {
|
||||
|
||||
f := func(idb *indexDB, tr TimeRange, want bool) {
|
||||
t.Helper()
|
||||
is := idb.getIndexSearch(noDeadline)
|
||||
defer idb.putIndexSearch(is)
|
||||
|
||||
got := is.legacyContainsTimeRange(tr)
|
||||
|
||||
got := idb.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)
|
||||
}
|
||||
@@ -97,8 +93,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 trCurr on
|
||||
// the left side.
|
||||
// Fully inside trPt, overlaps with trPrev on the right side and with 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,15 +2241,16 @@ func TestIndexSearchLegacyContainsTimeRange_Concurrent(t *testing.T) {
|
||||
for i := range concurrency {
|
||||
ts := minTimestamp + msecPerDay*i
|
||||
wg.Go(func() {
|
||||
is := idb.getIndexSearch(noDeadline)
|
||||
_ = is.legacyContainsTimeRange(TimeRange{ts, ts})
|
||||
idb.putIndexSearch(is)
|
||||
_ = idb.legacyContainsTimeRange(TimeRange{ts, ts})
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
key := marshalCommonPrefix(nil, nsPrefixDateToMetricID)
|
||||
if got, want := idb.legacyMinMissingTimestampByKey[string(key)], minTimestamp; got != want {
|
||||
key := TenantToken{
|
||||
AccountID: 0,
|
||||
ProjectID: 0,
|
||||
}
|
||||
if got, want := idb.legacyMinMissingTimestampByKey[key], minTimestamp; got != want {
|
||||
t.Fatalf("unexpected min timestamp: got %v, want %v", time.UnixMilli(got).UTC(), time.UnixMilli(want).UTC())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1290,6 +1290,9 @@ 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 {
|
||||
@@ -1444,6 +1447,8 @@ 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,16 +33,22 @@ func TestLegacyStorage_SearchMetricNames(t *testing.T) {
|
||||
return mrs, want
|
||||
}
|
||||
const numMetrics = 1000
|
||||
tr := TimeRange{
|
||||
tr1 := 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(),
|
||||
}
|
||||
legacyData, wantLegacy := genData(numMetrics, "legacy", tr)
|
||||
newData, wantNew := genData(numMetrics, "new", tr)
|
||||
wantNew = append(wantNew, wantLegacy...)
|
||||
slices.Sort(wantNew)
|
||||
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)
|
||||
|
||||
assertSearchResults := func(s *Storage, want []string) {
|
||||
assertSearchResults := func(s *Storage, tr TimeRange, want []string) {
|
||||
t.Helper()
|
||||
tfsAll := NewTagFilters()
|
||||
if err := tfsAll.Add([]byte("__name__"), []byte(".*"), false, true); err != nil {
|
||||
@@ -67,10 +73,11 @@ func TestLegacyStorage_SearchMetricNames(t *testing.T) {
|
||||
}
|
||||
|
||||
assertLegacyData := func(s *Storage) {
|
||||
assertSearchResults(s, wantLegacy)
|
||||
assertSearchResults(s, tr1, wantLegacy)
|
||||
}
|
||||
assertNewData := func(s *Storage) {
|
||||
assertSearchResults(s, wantNew)
|
||||
assertSearchResults(s, tr1, wantLegacyAndNew1)
|
||||
assertSearchResults(s, tr2, wantNew2)
|
||||
}
|
||||
testSearchOpWithLegacyIndexDBs(t, legacyData, newData, assertLegacyData, assertNewData)
|
||||
}
|
||||
@@ -95,15 +102,22 @@ func TestLegacyStorage_SearchLabelNames(t *testing.T) {
|
||||
return mrs, want
|
||||
}
|
||||
const numMetrics = 1000
|
||||
tr := TimeRange{
|
||||
tr1 := 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(),
|
||||
}
|
||||
legacyData, wantLegacy := genData(numMetrics, "legacy", tr)
|
||||
newData, wantNew := genData(numMetrics, "new", tr)
|
||||
wantNew = append(wantNew, wantLegacy...)
|
||||
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)
|
||||
|
||||
assertSearchResults := func(s *Storage, want []string) {
|
||||
assertSearchResults := func(s *Storage, tr TimeRange, want []string) {
|
||||
t.Helper()
|
||||
got, err := s.SearchLabelNames(nil, nil, tr, 1e9, 1e9, noDeadline)
|
||||
if err != nil {
|
||||
@@ -118,12 +132,16 @@ func TestLegacyStorage_SearchLabelNames(t *testing.T) {
|
||||
assertLegacyData := func(s *Storage) {
|
||||
want := append(wantLegacy, "__name__")
|
||||
slices.Sort(want)
|
||||
assertSearchResults(s, want)
|
||||
assertSearchResults(s, tr1, want)
|
||||
}
|
||||
assertNewData := func(s *Storage) {
|
||||
want := append(wantNew, "__name__")
|
||||
want := append(wantLegacyAndNew1, "__name__")
|
||||
slices.Sort(want)
|
||||
assertSearchResults(s, want)
|
||||
assertSearchResults(s, tr1, want)
|
||||
|
||||
want = append(wantNew2, "__name__")
|
||||
slices.Sort(want)
|
||||
assertSearchResults(s, tr2, want)
|
||||
}
|
||||
testSearchOpWithLegacyIndexDBs(t, legacyData, newData, assertLegacyData, assertNewData)
|
||||
}
|
||||
@@ -148,16 +166,22 @@ func TestLegacyStorage_SearchLabelValues(t *testing.T) {
|
||||
return mrs, want
|
||||
}
|
||||
const numMetrics = 1000
|
||||
tr := TimeRange{
|
||||
tr1 := 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(),
|
||||
}
|
||||
legacyData, wantLegacy := genData(numMetrics, "legacy", tr)
|
||||
newData, wantNew := genData(numMetrics, "new", tr)
|
||||
wantNew = append(wantNew, wantLegacy...)
|
||||
slices.Sort(wantNew)
|
||||
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)
|
||||
|
||||
assertSearchResults := func(s *Storage, want []string) {
|
||||
assertSearchResults := func(s *Storage, tr TimeRange, want []string) {
|
||||
t.Helper()
|
||||
got, err := s.SearchLabelValues(nil, "label", nil, tr, 1e9, 1e9, noDeadline)
|
||||
if err != nil {
|
||||
@@ -171,11 +195,12 @@ func TestLegacyStorage_SearchLabelValues(t *testing.T) {
|
||||
|
||||
assertLegacyData := func(s *Storage) {
|
||||
t.Helper()
|
||||
assertSearchResults(s, wantLegacy)
|
||||
assertSearchResults(s, tr1, wantLegacy)
|
||||
}
|
||||
assertNewData := func(s *Storage) {
|
||||
t.Helper()
|
||||
assertSearchResults(s, wantNew)
|
||||
assertSearchResults(s, tr1, wantLegacyAndNew1)
|
||||
assertSearchResults(s, tr2, wantNew2)
|
||||
}
|
||||
testSearchOpWithLegacyIndexDBs(t, legacyData, newData, assertLegacyData, assertNewData)
|
||||
}
|
||||
@@ -197,16 +222,22 @@ func TestLegacyStorage_SearchTagValueSuffixes(t *testing.T) {
|
||||
return mrs, want
|
||||
}
|
||||
const numMetrics = 1000
|
||||
tr := TimeRange{
|
||||
tr1 := 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(),
|
||||
}
|
||||
legacyData, wantLegacy := genData(numMetrics, "legacy", tr)
|
||||
newData, wantNew := genData(numMetrics, "new", tr)
|
||||
wantNew = append(wantNew, wantLegacy...)
|
||||
slices.Sort(wantNew)
|
||||
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)
|
||||
|
||||
assertSearchResults := func(s *Storage, want []string) {
|
||||
assertSearchResults := func(s *Storage, tr TimeRange, want []string) {
|
||||
t.Helper()
|
||||
got, err := s.SearchTagValueSuffixes(nil, tr, "", "prefix.", '.', 1e9, noDeadline)
|
||||
if err != nil {
|
||||
@@ -221,11 +252,12 @@ func TestLegacyStorage_SearchTagValueSuffixes(t *testing.T) {
|
||||
|
||||
assertLegacyData := func(s *Storage) {
|
||||
t.Helper()
|
||||
assertSearchResults(s, wantLegacy)
|
||||
assertSearchResults(s, tr1, wantLegacy)
|
||||
}
|
||||
assertNewData := func(s *Storage) {
|
||||
t.Helper()
|
||||
assertSearchResults(s, wantNew)
|
||||
assertSearchResults(s, tr1, wantLegacyAndNew1)
|
||||
assertSearchResults(s, tr2, wantNew2)
|
||||
}
|
||||
testSearchOpWithLegacyIndexDBs(t, legacyData, newData, assertLegacyData, assertNewData)
|
||||
}
|
||||
@@ -247,16 +279,22 @@ func TestLegacyStorage_SearchGraphitePaths(t *testing.T) {
|
||||
return mrs, want
|
||||
}
|
||||
const numMetrics = 1000
|
||||
tr := TimeRange{
|
||||
tr1 := 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(),
|
||||
}
|
||||
legacyData, wantLegacy := genData(numMetrics, "legacy", tr)
|
||||
newData, wantNew := genData(numMetrics, "new", tr)
|
||||
wantNew = append(wantNew, wantLegacy...)
|
||||
slices.Sort(wantNew)
|
||||
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)
|
||||
|
||||
assertSearchResults := func(s *Storage, want []string) {
|
||||
assertSearchResults := func(s *Storage, tr TimeRange, want []string) {
|
||||
t.Helper()
|
||||
got, err := s.SearchGraphitePaths(nil, tr, []byte("*.*"), 1e9, noDeadline)
|
||||
if err != nil {
|
||||
@@ -271,16 +309,17 @@ func TestLegacyStorage_SearchGraphitePaths(t *testing.T) {
|
||||
|
||||
assertLegacyData := func(s *Storage) {
|
||||
t.Helper()
|
||||
assertSearchResults(s, wantLegacy)
|
||||
assertSearchResults(s, tr1, wantLegacy)
|
||||
}
|
||||
assertNewData := func(s *Storage) {
|
||||
t.Helper()
|
||||
assertSearchResults(s, wantNew)
|
||||
assertSearchResults(s, tr1, wantLegacyAndNew1)
|
||||
assertSearchResults(s, tr2, wantNew2)
|
||||
}
|
||||
testSearchOpWithLegacyIndexDBs(t, legacyData, newData, assertLegacyData, assertNewData)
|
||||
}
|
||||
|
||||
func TestLegacyStorage_Search(t *testing.T) {
|
||||
func TestLegacyStorage_SearchData(t *testing.T) {
|
||||
genData := func(numMetrics int, prefix string, tr TimeRange) []MetricRow {
|
||||
mrs := make([]MetricRow, numMetrics)
|
||||
for i := range numMetrics {
|
||||
@@ -295,14 +334,20 @@ func TestLegacyStorage_Search(t *testing.T) {
|
||||
return mrs
|
||||
}
|
||||
const numMetrics = 1000
|
||||
tr := TimeRange{
|
||||
tr1 := 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(),
|
||||
}
|
||||
legacyData := genData(numMetrics, "legacy", tr)
|
||||
newData := genData(numMetrics, "new", tr)
|
||||
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)
|
||||
|
||||
assertSearchResults := func(s *Storage, want []MetricRow) {
|
||||
assertSearchResults := func(s *Storage, tr TimeRange, want []MetricRow) {
|
||||
tfsAll := NewTagFilters()
|
||||
if err := tfsAll.Add([]byte("__name__"), []byte(".*"), false, true); err != nil {
|
||||
t.Fatalf("unexpected error in TagFilters.Add: %v", err)
|
||||
@@ -314,13 +359,13 @@ func TestLegacyStorage_Search(t *testing.T) {
|
||||
|
||||
assertLegacyData := func(s *Storage) {
|
||||
t.Helper()
|
||||
want := legacyData
|
||||
assertSearchResults(s, want)
|
||||
assertSearchResults(s, tr1, legacyData)
|
||||
}
|
||||
assertNewData := func(s *Storage) {
|
||||
t.Helper()
|
||||
want := slices.Concat(legacyData, newData)
|
||||
assertSearchResults(s, want)
|
||||
want := slices.Concat(legacyData, new1Data)
|
||||
assertSearchResults(s, tr1, want)
|
||||
assertSearchResults(s, tr2, new2Data)
|
||||
}
|
||||
testSearchOpWithLegacyIndexDBs(t, legacyData, newData, assertLegacyData, assertNewData)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user