mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-06-20 17:18:52 +03:00
Compare commits
53 Commits
state-only
...
fix-broken
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
487c1293f8 | ||
|
|
9356c2111a | ||
|
|
45f0b87150 | ||
|
|
8480f6b43e | ||
|
|
61668f0672 | ||
|
|
d1ebbf573c | ||
|
|
16422b2d14 | ||
|
|
0f1ca87611 | ||
|
|
0dd2b2cee6 | ||
|
|
7caec5fcb4 | ||
|
|
612f8ac8d6 | ||
|
|
6aa31a09d7 | ||
|
|
b6e6a50e29 | ||
|
|
a6d48b6af3 | ||
|
|
dc4cf5631b | ||
|
|
005f133146 | ||
|
|
35fc595e6f | ||
|
|
710c920d60 | ||
|
|
0ceeb14076 | ||
|
|
adc29732f9 | ||
|
|
41ffe23b18 | ||
|
|
6229a8fe7d | ||
|
|
b58c73ac90 | ||
|
|
77efbb2e36 | ||
|
|
e388e41430 | ||
|
|
ed795a8443 | ||
|
|
94e5955b1f | ||
|
|
5b31a047a5 | ||
|
|
17c3fb5656 | ||
|
|
30133ec182 | ||
|
|
8b27a36fb5 | ||
|
|
f33cd8a937 | ||
|
|
615e49c983 | ||
|
|
eb1b4c6df4 | ||
|
|
ca71127158 | ||
|
|
1df805e23b | ||
|
|
dfc459eb38 | ||
|
|
83ef694e9c | ||
|
|
f6830298dc | ||
|
|
f16bcb1355 | ||
|
|
22802101e0 | ||
|
|
00420e16f9 | ||
|
|
6c3c548ddb | ||
|
|
d52de359d5 | ||
|
|
892f4aced2 | ||
|
|
05903c8acd | ||
|
|
a9fae230ae | ||
|
|
19fac13418 | ||
|
|
3a6054f8a2 | ||
|
|
6653f6a5e7 | ||
|
|
71c7a73716 | ||
|
|
10eb212d2b | ||
|
|
5e005f5dbb |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -66,6 +66,8 @@ jobs:
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Go
|
||||
id: go
|
||||
|
||||
1
.github/workflows/changelog-linter.yml
vendored
1
.github/workflows/changelog-linter.yml
vendored
@@ -17,6 +17,7 @@ jobs:
|
||||
with:
|
||||
# needed for proper diff
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Validate that changelog changes are under ## tip'
|
||||
run: |
|
||||
|
||||
1
.github/workflows/check-commit-signed.yml
vendored
1
.github/workflows/check-commit-signed.yml
vendored
@@ -15,6 +15,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0 # we need full history for commit verification
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check commit signatures
|
||||
run: |
|
||||
|
||||
2
.github/workflows/check-licenses.yml
vendored
2
.github/workflows/check-licenses.yml
vendored
@@ -18,6 +18,8 @@ jobs:
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Go
|
||||
id: go
|
||||
|
||||
2
.github/workflows/codeql-analysis-go.yml
vendored
2
.github/workflows/codeql-analysis-go.yml
vendored
@@ -32,6 +32,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
id: go
|
||||
|
||||
2
.github/workflows/docs.yaml
vendored
2
.github/workflows/docs.yaml
vendored
@@ -21,6 +21,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
path: __vm
|
||||
persist-credentials: false
|
||||
|
||||
- name: Checkout private code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -28,6 +29,7 @@ jobs:
|
||||
repository: VictoriaMetrics/vmdocs
|
||||
token: ${{ secrets.VM_BOT_GH_TOKEN }}
|
||||
path: __vm-docs
|
||||
persist-credentials: true
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
|
||||
|
||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -35,6 +35,8 @@ jobs:
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Go
|
||||
id: go
|
||||
@@ -78,6 +80,8 @@ jobs:
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Go
|
||||
id: go
|
||||
@@ -103,6 +107,8 @@ jobs:
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Go
|
||||
id: go
|
||||
|
||||
2
.github/workflows/vmui.yml
vendored
2
.github/workflows/vmui.yml
vendored
@@ -33,6 +33,8 @@ jobs:
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Cache node_modules
|
||||
id: cache
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
version: "2"
|
||||
linters:
|
||||
enable:
|
||||
- errorlint
|
||||
settings:
|
||||
errcheck:
|
||||
exclude-functions:
|
||||
- (net/http.ResponseWriter).Write
|
||||
errorlint:
|
||||
errorf: true
|
||||
# Do not enable `comparison` and `asserts`: they produce false positives,
|
||||
# since many call sites intentionally compare sentinel errors directly (e.g. err == io.EOF)
|
||||
# when the producer is documented to return them unwrapped. See https://github.com/VictoriaMetrics/VictoriaLogs/pull/1490
|
||||
comparison: false
|
||||
asserts: false
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
func Compress(wr WriteRequest) []byte {
|
||||
data, err := wr.Marshal()
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("BUG: cannot compress WriteRequest: %s", err))
|
||||
panic(fmt.Errorf("BUG: cannot compress WriteRequest: %w", err))
|
||||
}
|
||||
return snappy.Encode(nil, data)
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ func newHTTPClient(argIdx int, remoteWriteURL, sanitizedURL string, fq *persiste
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *client) init(argIdx, concurrency int, sanitizedURL string) {
|
||||
func (c *client) init(argIdx int, sanitizedURL string) {
|
||||
limitReached := metrics.GetOrCreateCounter(fmt.Sprintf(`vmagent_remotewrite_rate_limit_reached_total{url=%q}`, c.sanitizedURL))
|
||||
if bytesPerSec := rateLimit.GetOptionalArg(argIdx); bytesPerSec > 0 {
|
||||
logger.Infof("applying %d bytes per second rate limit for -remoteWrite.url=%q", bytesPerSec, sanitizedURL)
|
||||
@@ -204,11 +204,20 @@ func (c *client) init(argIdx, concurrency int, sanitizedURL string) {
|
||||
c.packetsDropped = metrics.GetOrCreateCounter(fmt.Sprintf(`vmagent_remotewrite_packets_dropped_total{url=%q}`, c.sanitizedURL))
|
||||
c.retriesCount = metrics.GetOrCreateCounter(fmt.Sprintf(`vmagent_remotewrite_retries_count_total{url=%q}`, c.sanitizedURL))
|
||||
c.sendDuration = metrics.GetOrCreateFloatCounter(fmt.Sprintf(`vmagent_remotewrite_send_duration_seconds_total{url=%q}`, c.sanitizedURL))
|
||||
metrics.GetOrCreateGauge(fmt.Sprintf(`vmagent_remotewrite_queues{url=%q}`, c.sanitizedURL), func() float64 {
|
||||
return float64(concurrency)
|
||||
})
|
||||
for range concurrency {
|
||||
c.wg.Go(c.runWorker)
|
||||
workers := queues.GetOptionalArg(argIdx)
|
||||
if workers <= 0 {
|
||||
workers = 1
|
||||
}
|
||||
inmemoryWorkers := inmemoryQueues.GetOptionalArg(argIdx)
|
||||
for range inmemoryWorkers {
|
||||
c.wg.Go(func() {
|
||||
c.runWorker(c.fq.MustReadInMemoryBlockBlocking)
|
||||
})
|
||||
}
|
||||
for range workers {
|
||||
c.wg.Go(func() {
|
||||
c.runWorker(c.fq.MustReadBlock)
|
||||
})
|
||||
}
|
||||
logger.Infof("initialized client for -remoteWrite.url=%q", c.sanitizedURL)
|
||||
}
|
||||
@@ -302,12 +311,12 @@ func getAWSAPIConfig(argIdx int) (*awsapi.Config, error) {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (c *client) runWorker() {
|
||||
func (c *client) runWorker(readBlock func(dst []byte) ([]byte, bool)) {
|
||||
var ok bool
|
||||
var block []byte
|
||||
ch := make(chan bool, 1)
|
||||
for {
|
||||
block, ok = c.fq.MustReadBlock(block[:0])
|
||||
block, ok = readBlock(block[:0])
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -209,13 +209,12 @@ func (wr *writeRequest) tryPushMetadata(mms []prompb.MetricMetadata) bool {
|
||||
func (wr *writeRequest) copyMetadata(dst, src *prompb.MetricMetadata) {
|
||||
// Direct copy for non-string fields, which are safe by value.
|
||||
dst.Type = src.Type
|
||||
dst.Unit = src.Unit
|
||||
|
||||
dst.AccountID = src.AccountID
|
||||
dst.ProjectID = src.ProjectID
|
||||
|
||||
// Pre-allocate memory for all string fields.
|
||||
neededBufLen := len(src.MetricFamilyName) + len(src.Help)
|
||||
neededBufLen := len(src.MetricFamilyName) + len(src.Help) + len(src.Unit)
|
||||
bufLen := len(wr.metadatabuf)
|
||||
wr.metadatabuf = slicesutil.SetLength(wr.metadatabuf, bufLen+neededBufLen)
|
||||
buf := wr.metadatabuf[:bufLen]
|
||||
@@ -230,6 +229,11 @@ func (wr *writeRequest) copyMetadata(dst, src *prompb.MetricMetadata) {
|
||||
buf = append(buf, src.Help...)
|
||||
dst.Help = bytesutil.ToUnsafeString(buf[bufLen:])
|
||||
|
||||
// Copy Unit
|
||||
bufLen = len(buf)
|
||||
buf = append(buf, src.Unit...)
|
||||
dst.Unit = bytesutil.ToUnsafeString(buf[bufLen:])
|
||||
|
||||
wr.metadatabuf = buf
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,9 @@ var (
|
||||
queues = flagutil.NewArrayInt("remoteWrite.queues", cgroup.AvailableCPUs()*2, "The number of concurrent queues to each -remoteWrite.url. Set more queues if default number of queues "+
|
||||
"isn't enough for sending high volume of collected data to remote storage. "+
|
||||
"Default value depends on the number of available CPU cores. It should work fine in most cases since it minimizes resource usage")
|
||||
inmemoryQueues = flagutil.NewArrayInt("remoteWrite.inmemoryQueues", 0, "The number of additional workers per each -remoteWrite.url, which send only recently ingested data from the in-memory queue, "+
|
||||
"while the file-based queue at -remoteWrite.tmpDataPath is drained by workers configured via -remoteWrite.queues. "+
|
||||
"This reduces delivery lag for fresh samples when the file-based queue contains a backlog accumulated during remote storage outages.")
|
||||
showRemoteWriteURL = flag.Bool("remoteWrite.showURL", false, "Whether to show -remoteWrite.url in the exported metrics. "+
|
||||
"It is hidden by default, since it can contain sensitive info such as auth key")
|
||||
maxPendingBytesPerURL = flagutil.NewArrayBytes("remoteWrite.maxDiskUsagePerURL", 0, "The maximum file-based buffer size in bytes at -remoteWrite.tmpDataPath "+
|
||||
@@ -906,7 +909,8 @@ func newRemoteWriteCtx(argIdx int, remoteWriteURL *url.URL, sanitizedURL string)
|
||||
}
|
||||
|
||||
isPQDisabled := disableOnDiskQueue.GetOptionalArg(argIdx)
|
||||
queuesSize := queues.GetOptionalArg(argIdx)
|
||||
inmemoryQueueSize := inmemoryQueues.GetOptionalArg(argIdx)
|
||||
queuesSize := queues.GetOptionalArg(argIdx) + inmemoryQueueSize
|
||||
if queuesSize > maxQueues {
|
||||
queuesSize = maxQueues
|
||||
} else if queuesSize <= 0 {
|
||||
@@ -923,7 +927,13 @@ func newRemoteWriteCtx(argIdx int, remoteWriteURL *url.URL, sanitizedURL string)
|
||||
if maxInmemoryBlocks < 2 {
|
||||
maxInmemoryBlocks = 2
|
||||
}
|
||||
fq := persistentqueue.MustOpenFastQueue(queuePath, sanitizedURL, maxInmemoryBlocks, maxPendingBytes, isPQDisabled)
|
||||
fqOpts := persistentqueue.OpenFastQueueOpts{
|
||||
MaxInmemoryBlocks: maxInmemoryBlocks,
|
||||
MaxPendingBytes: maxPendingBytes,
|
||||
IsPQDisabled: isPQDisabled,
|
||||
PrioritizeInmemoryData: inmemoryQueueSize > 0,
|
||||
}
|
||||
fq := persistentqueue.MustOpenFastQueueWithOpts(queuePath, sanitizedURL, fqOpts)
|
||||
_ = metrics.GetOrCreateGauge(fmt.Sprintf(`vmagent_remotewrite_pending_data_bytes{path=%q, url=%q}`, queuePath, sanitizedURL), func() float64 {
|
||||
return float64(fq.GetPendingBytes())
|
||||
})
|
||||
@@ -936,6 +946,9 @@ func newRemoteWriteCtx(argIdx int, remoteWriteURL *url.URL, sanitizedURL string)
|
||||
}
|
||||
return 0
|
||||
})
|
||||
metrics.GetOrCreateGauge(fmt.Sprintf(`vmagent_remotewrite_queues{url=%q}`, sanitizedURL), func() float64 {
|
||||
return float64(queuesSize)
|
||||
})
|
||||
|
||||
var c *client
|
||||
switch remoteWriteURL.Scheme {
|
||||
@@ -944,7 +957,7 @@ func newRemoteWriteCtx(argIdx int, remoteWriteURL *url.URL, sanitizedURL string)
|
||||
default:
|
||||
logger.Fatalf("unsupported scheme: %s for remoteWriteURL: %s, want `http`, `https`", remoteWriteURL.Scheme, sanitizedURL)
|
||||
}
|
||||
c.init(argIdx, queuesSize, sanitizedURL)
|
||||
c.init(argIdx, sanitizedURL)
|
||||
|
||||
// Initialize pss
|
||||
sf := significantFigures.GetOptionalArg(argIdx)
|
||||
|
||||
@@ -61,15 +61,15 @@ func parseInputSeries(input []series, interval *promutil.Duration, startStamp ti
|
||||
for _, data := range input {
|
||||
expr, err := metricsql.Parse(data.Series)
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("failed to parse series %s: %v", data.Series, err)
|
||||
return res, fmt.Errorf("failed to parse series %s: %w", data.Series, err)
|
||||
}
|
||||
promvals, err := parseInputValue(data.Values, true)
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("failed to parse input series value %s: %v", data.Values, err)
|
||||
return res, fmt.Errorf("failed to parse input series value %s: %w", data.Values, err)
|
||||
}
|
||||
metricExpr, ok := expr.(*metricsql.MetricExpr)
|
||||
if !ok || len(metricExpr.LabelFilterss) != 1 {
|
||||
return res, fmt.Errorf("got invalid input series %s: %v", data.Series, err)
|
||||
return res, fmt.Errorf("got invalid input series %s: %w", data.Series, err)
|
||||
}
|
||||
samples := make([]testutil.Sample, 0, len(promvals))
|
||||
ts := startStamp
|
||||
|
||||
@@ -53,13 +53,13 @@ Outer:
|
||||
if s.Labels != "" {
|
||||
metricsqlExpr, err := metricsql.Parse(s.Labels)
|
||||
if err != nil {
|
||||
checkErrs = append(checkErrs, fmt.Errorf("\n expr: %q, time: %s, err: %v", mt.Expr,
|
||||
checkErrs = append(checkErrs, fmt.Errorf("\n expr: %q, time: %s, err: %w", mt.Expr,
|
||||
mt.EvalTime.Duration().String(), fmt.Errorf("failed to parse labels %q: %w", s.Labels, err)))
|
||||
continue Outer
|
||||
}
|
||||
metricsqlMetricExpr, ok := metricsqlExpr.(*metricsql.MetricExpr)
|
||||
if !ok || len(metricsqlMetricExpr.LabelFilterss) > 1 {
|
||||
checkErrs = append(checkErrs, fmt.Errorf("\n expr: %q, time: %s, err: %v", mt.Expr,
|
||||
checkErrs = append(checkErrs, fmt.Errorf("\n expr: %q, time: %s, err: %w", mt.Expr,
|
||||
mt.EvalTime.Duration().String(), fmt.Errorf("got invalid exp_samples: %q", s.Labels)))
|
||||
continue Outer
|
||||
}
|
||||
|
||||
@@ -329,11 +329,11 @@ func (tg *testGroup) test(evalInterval time.Duration, groupOrderMap map[string]i
|
||||
|
||||
q, err := datasource.Init(nil)
|
||||
if err != nil {
|
||||
return []error{fmt.Errorf("failed to init datasource: %v", err)}
|
||||
return []error{fmt.Errorf("failed to init datasource: %w", err)}
|
||||
}
|
||||
rw, err := remotewrite.NewDebugClient()
|
||||
if err != nil {
|
||||
return []error{fmt.Errorf("failed to init wr: %v", err)}
|
||||
return []error{fmt.Errorf("failed to init wr: %w", err)}
|
||||
}
|
||||
|
||||
alertEvalTimesMap := map[time.Duration]struct{}{}
|
||||
|
||||
@@ -89,7 +89,7 @@ func (pi *promInstant) Unmarshal(b []byte) error {
|
||||
labels.Visit(func(key []byte, v *fastjson.Value) {
|
||||
lv, errLocal := v.StringBytes()
|
||||
if errLocal != nil {
|
||||
err = fmt.Errorf("error when parsing label value %q: %s", v, errLocal)
|
||||
err = fmt.Errorf("error when parsing label value %q: %w", v, errLocal)
|
||||
return
|
||||
}
|
||||
r.Labels = append(r.Labels, prompb.Label{
|
||||
@@ -112,7 +112,7 @@ func (pi *promInstant) Unmarshal(b []byte) error {
|
||||
r.Timestamps = []int64{sample[0].GetInt64()}
|
||||
val, err := sample[1].StringBytes()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error when parsing `value` object %q: %s", sample[1], err)
|
||||
return fmt.Errorf("error when parsing `value` object %q: %w", sample[1], err)
|
||||
}
|
||||
f, err := strconv.ParseFloat(bytesutil.ToUnsafeString(val), 64)
|
||||
if err != nil {
|
||||
|
||||
@@ -315,6 +315,11 @@ func configReload(ctx context.Context, m *manager, groupsCfg []config.Group, sig
|
||||
|
||||
parseFn := config.Parse
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
@@ -601,7 +601,7 @@ func (ar *AlertingRule) exec(ctx context.Context, ts time.Time, limit int) ([]pr
|
||||
func (ar *AlertingRule) expandLabelTemplates(m datasource.Metric, qFn templates.QueryFn) (*labelSet, error) {
|
||||
ls, err := ar.toLabels(m, qFn)
|
||||
if err != nil {
|
||||
return ls, fmt.Errorf("failed to expand label templates: %s", err)
|
||||
return ls, fmt.Errorf("failed to expand label templates: %w", err)
|
||||
}
|
||||
return ls, nil
|
||||
}
|
||||
@@ -620,7 +620,7 @@ func (ar *AlertingRule) expandAnnotationTemplates(m datasource.Metric, qFn templ
|
||||
}
|
||||
as, err := notifier.ExecTemplate(qFn, ar.Annotations, tplData)
|
||||
if err != nil {
|
||||
return as, fmt.Errorf("failed to expand annotation templates: %s", err)
|
||||
return as, fmt.Errorf("failed to expand annotation templates: %w", err)
|
||||
}
|
||||
return as, nil
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ var (
|
||||
func marshalJson(v any, kind string) ([]byte, *httpserver.ErrorWithStatusCode) {
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, errResponse(fmt.Errorf("failed to marshal %s: %s", kind, err), http.StatusInternalServerError)
|
||||
return nil, errResponse(fmt.Errorf("failed to marshal %s: %w", kind, err), http.StatusInternalServerError)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
@@ -840,6 +840,11 @@ func authConfigReloader(sighupCh <-chan os.Signal) {
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stopCh:
|
||||
return
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case <-stopCh:
|
||||
return
|
||||
@@ -906,7 +911,8 @@ func reloadAuthConfigData(data []byte) (bool, error) {
|
||||
return false, fmt.Errorf("failed to parse auth config: %w", err)
|
||||
}
|
||||
|
||||
jui, oidcDP, err := parseJWTUsers(ac)
|
||||
oidcDP := &oidcDiscovererPool{}
|
||||
jui, err := parseJWTUsers(ac, oidcDP)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to parse JWT users from auth config: %w", err)
|
||||
}
|
||||
|
||||
@@ -72,9 +72,8 @@ type JWTConfig struct {
|
||||
verifierPool atomic.Pointer[jwt.VerifierPool]
|
||||
}
|
||||
|
||||
func parseJWTUsers(ac *AuthConfig) ([]*UserInfo, *oidcDiscovererPool, error) {
|
||||
func parseJWTUsers(ac *AuthConfig, oidcDP *oidcDiscovererPool) ([]*UserInfo, error) {
|
||||
jui := make([]*UserInfo, 0, len(ac.Users))
|
||||
oidcDP := &oidcDiscovererPool{}
|
||||
|
||||
uniqClaims := make(map[string]*UserInfo)
|
||||
var sortedClaims []string
|
||||
@@ -85,10 +84,10 @@ func parseJWTUsers(ac *AuthConfig) ([]*UserInfo, *oidcDiscovererPool, error) {
|
||||
}
|
||||
|
||||
if ui.AuthToken != "" || ui.BearerToken != "" || ui.Username != "" || ui.Password != "" {
|
||||
return nil, nil, fmt.Errorf("auth_token, bearer_token, username and password cannot be specified if jwt is set")
|
||||
return nil, fmt.Errorf("auth_token, bearer_token, username and password cannot be specified if jwt is set")
|
||||
}
|
||||
if len(jwtToken.PublicKeys) == 0 && len(jwtToken.PublicKeyFiles) == 0 && !jwtToken.SkipVerify && jwtToken.OIDC == nil {
|
||||
return nil, nil, fmt.Errorf("jwt must contain at least a single public key, public_key_files, oidc or have skip_verify=true")
|
||||
return nil, fmt.Errorf("jwt must contain at least a single public key, public_key_files, oidc or have skip_verify=true")
|
||||
}
|
||||
var claimsString string
|
||||
sortedClaims = sortedClaims[:0]
|
||||
@@ -97,7 +96,7 @@ func parseJWTUsers(ac *AuthConfig) ([]*UserInfo, *oidcDiscovererPool, error) {
|
||||
sortedClaims = append(sortedClaims, fmt.Sprintf("%s=%s", ck, cv))
|
||||
pc, err := jwt.NewClaim(ck, cv)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("incorrect match claim, key=%q, value regex=%q: %w", ck, cv, err)
|
||||
return nil, fmt.Errorf("incorrect match claim, key=%q, value regex=%q: %w", ck, cv, err)
|
||||
}
|
||||
parsedClaims = append(parsedClaims, pc)
|
||||
}
|
||||
@@ -106,7 +105,7 @@ func parseJWTUsers(ac *AuthConfig) ([]*UserInfo, *oidcDiscovererPool, error) {
|
||||
claimsString = strings.Join(sortedClaims, ",")
|
||||
|
||||
if oldUI, ok := uniqClaims[claimsString]; ok {
|
||||
return nil, nil, fmt.Errorf("duplicate match claims=%q found for name=%q at idx=%d; the previous one is set for name=%q", claimsString, ui.Name, idx, oldUI.Name)
|
||||
return nil, fmt.Errorf("duplicate match claims=%q found for name=%q at idx=%d; the previous one is set for name=%q", claimsString, ui.Name, idx, oldUI.Name)
|
||||
}
|
||||
uniqClaims[claimsString] = &ui
|
||||
if len(jwtToken.PublicKeys) > 0 || len(jwtToken.PublicKeyFiles) > 0 {
|
||||
@@ -115,7 +114,7 @@ func parseJWTUsers(ac *AuthConfig) ([]*UserInfo, *oidcDiscovererPool, error) {
|
||||
for i := range jwtToken.PublicKeys {
|
||||
k, err := jwt.ParseKey([]byte(jwtToken.PublicKeys[i]))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
keys = append(keys, k)
|
||||
}
|
||||
@@ -123,52 +122,52 @@ func parseJWTUsers(ac *AuthConfig) ([]*UserInfo, *oidcDiscovererPool, error) {
|
||||
for _, filePath := range jwtToken.PublicKeyFiles {
|
||||
keyData, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot read public key from file %q: %w", filePath, err)
|
||||
return nil, fmt.Errorf("cannot read public key from file %q: %w", filePath, err)
|
||||
}
|
||||
k, err := jwt.ParseKey(keyData)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot parse public key from file %q: %w", filePath, err)
|
||||
return nil, fmt.Errorf("cannot parse public key from file %q: %w", filePath, err)
|
||||
}
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
vp, err := jwt.NewVerifierPool(keys)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jwtToken.verifierPool.Store(vp)
|
||||
}
|
||||
if jwtToken.OIDC != nil {
|
||||
if len(jwtToken.PublicKeys) > 0 || len(jwtToken.PublicKeyFiles) > 0 || jwtToken.SkipVerify {
|
||||
return nil, nil, fmt.Errorf("jwt with oidc cannot contain public keys or have skip_verify=true")
|
||||
return nil, fmt.Errorf("jwt with oidc cannot contain public keys or have skip_verify=true")
|
||||
}
|
||||
|
||||
if jwtToken.OIDC.Issuer == "" {
|
||||
return nil, nil, fmt.Errorf("oidc issuer cannot be empty")
|
||||
return nil, fmt.Errorf("oidc issuer cannot be empty")
|
||||
}
|
||||
isserURL, err := url.Parse(jwtToken.OIDC.Issuer)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("oidc issuer %q must be a valid URL", jwtToken.OIDC.Issuer)
|
||||
return nil, fmt.Errorf("oidc issuer %q must be a valid URL", jwtToken.OIDC.Issuer)
|
||||
}
|
||||
if isserURL.Scheme != "https" && isserURL.Scheme != "http" {
|
||||
return nil, nil, fmt.Errorf("oidc issuer %q must have http or https scheme", jwtToken.OIDC.Issuer)
|
||||
return nil, fmt.Errorf("oidc issuer %q must have http or https scheme", jwtToken.OIDC.Issuer)
|
||||
}
|
||||
|
||||
oidcDP.createOrAdd(ui.JWT.OIDC.Issuer, &ui.JWT.verifierPool)
|
||||
}
|
||||
|
||||
if err := parseJWTPlaceholdersForUserInfo(&ui, true); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := ui.initURLs(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metricLabels, err := ui.getMetricLabels()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot parse metric_labels: %w", err)
|
||||
return nil, fmt.Errorf("cannot parse metric_labels: %w", err)
|
||||
}
|
||||
ui.requests = ac.ms.GetOrCreateCounter(`vmauth_user_requests_total` + metricLabels)
|
||||
ui.requestErrors = ac.ms.GetOrCreateCounter(`vmauth_user_request_errors_total` + metricLabels)
|
||||
@@ -187,7 +186,7 @@ func parseJWTUsers(ac *AuthConfig) ([]*UserInfo, *oidcDiscovererPool, error) {
|
||||
|
||||
rt, err := newRoundTripper(ui.TLSCAFile, ui.TLSCertFile, ui.TLSKeyFile, ui.TLSServerName, ui.TLSInsecureSkipVerify)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot initialize HTTP RoundTripper: %w", err)
|
||||
return nil, fmt.Errorf("cannot initialize HTTP RoundTripper: %w", err)
|
||||
}
|
||||
ui.rt = rt
|
||||
|
||||
@@ -200,7 +199,7 @@ func parseJWTUsers(ac *AuthConfig) ([]*UserInfo, *oidcDiscovererPool, error) {
|
||||
return len(jui[i].JWT.MatchClaims) > len(jui[j].JWT.MatchClaims)
|
||||
})
|
||||
|
||||
return jui, oidcDP, nil
|
||||
return jui, nil
|
||||
}
|
||||
|
||||
var tokenPool sync.Pool
|
||||
|
||||
@@ -39,16 +39,14 @@ XOtclIk1uhc03oL9nOQ=
|
||||
}
|
||||
return
|
||||
}
|
||||
users, oidcDP, err := parseJWTUsers(ac)
|
||||
oidcDP := &oidcDiscovererPool{}
|
||||
users, err := parseJWTUsers(ac, oidcDP)
|
||||
if err == nil {
|
||||
t.Fatalf("expecting non-nil error; got %v", users)
|
||||
}
|
||||
if expErr != err.Error() {
|
||||
t.Fatalf("unexpected error; got\n%q\nwant \n%q", err.Error(), expErr)
|
||||
}
|
||||
if oidcDP != nil {
|
||||
t.Fatalf("expecting nil oidcDP; got %v", oidcDP)
|
||||
}
|
||||
}
|
||||
|
||||
// unauthorized_user cannot be used with jwt
|
||||
@@ -326,7 +324,8 @@ XOtclIk1uhc03oL9nOQ=
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
jui, oidcDP, err := parseJWTUsers(ac)
|
||||
oidcDP := &oidcDiscovererPool{}
|
||||
jui, err := parseJWTUsers(ac, oidcDP)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
@@ -1639,7 +1639,7 @@ func (w *fakeResponseWriter) WriteHeader(statusCode int) {
|
||||
"X-Content-Type-Options": true,
|
||||
})
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot marshal headers: %s", err))
|
||||
panic(fmt.Errorf("cannot marshal headers: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ func fetchAndParseJWKs(ctx context.Context, jwksURI string) (*jwt.VerifierPool,
|
||||
|
||||
vp, err := jwt.ParseJWKs(b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse jwks keys from %q: %v", jwksURI, err)
|
||||
return nil, fmt.Errorf("failed to parse jwks keys from %q: %w", jwksURI, err)
|
||||
}
|
||||
|
||||
return vp, nil
|
||||
@@ -188,7 +188,7 @@ func getOpenIDConfiguration(ctx context.Context, issuer string) (openidConfig, e
|
||||
|
||||
var cfg openidConfig
|
||||
if err := json.NewDecoder(resp.Body).Decode(&cfg); err != nil {
|
||||
return openidConfig{}, fmt.Errorf("failed to decode openid config from %q: %s", configURL, err)
|
||||
return openidConfig{}, fmt.Errorf("failed to decode openid config from %q: %w", configURL, err)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
|
||||
@@ -131,16 +131,13 @@ func (ac *authContext) initFromBasicAuthConfig(ba *BasicAuthConfig) error {
|
||||
if ba.Username == "" {
|
||||
return fmt.Errorf("missing `username` in `basic_auth` section")
|
||||
}
|
||||
if ba.Password != "" {
|
||||
ac.getAuthHeader = func() string {
|
||||
// See https://en.wikipedia.org/wiki/Basic_access_authentication
|
||||
token := ba.Username + ":" + ba.Password
|
||||
token64 := base64.StdEncoding.EncodeToString([]byte(token))
|
||||
return "Basic " + token64
|
||||
}
|
||||
ac.authDigest = fmt.Sprintf("basic(username=%q, password=%q)", ba.Username, ba.Password)
|
||||
return nil
|
||||
ac.getAuthHeader = func() string {
|
||||
// See https://en.wikipedia.org/wiki/Basic_access_authentication
|
||||
token := ba.Username + ":" + ba.Password
|
||||
token64 := base64.StdEncoding.EncodeToString([]byte(token))
|
||||
return "Basic " + token64
|
||||
}
|
||||
ac.authDigest = fmt.Sprintf("basic(username=%q, password=%q)", ba.Username, ba.Password)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,8 @@ const (
|
||||
vmAddr = "vm-addr"
|
||||
vmUser = "vm-user"
|
||||
vmPassword = "vm-password"
|
||||
vmHeaders = "vm-headers"
|
||||
vmBearerToken = "vm-bearer-token"
|
||||
vmAccountID = "vm-account-id"
|
||||
vmConcurrency = "vm-concurrency"
|
||||
vmCompress = "vm-compress"
|
||||
@@ -112,6 +114,16 @@ var (
|
||||
Usage: "VictoriaMetrics password for basic auth",
|
||||
EnvVars: []string{"VM_PASSWORD"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: vmHeaders,
|
||||
Usage: "Optional HTTP headers to send with each request to the corresponding destination address. \n" +
|
||||
"For example, --vm-headers='My-Auth:foobar' would send 'My-Auth: foobar' HTTP header with every request to the corresponding destination address. \n" +
|
||||
"Multiple headers must be delimited by '^^': --vm-headers='header1:value1^^header2:value2'",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: vmBearerToken,
|
||||
Usage: "Optional bearer auth token to use for the corresponding --vm-addr",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: vmAccountID,
|
||||
Usage: "AccountID is an arbitrary 32-bit integer identifying namespace for data ingestion (aka tenant). \n" +
|
||||
|
||||
@@ -43,7 +43,7 @@ func newInfluxProcessor(ic *influx.Client, im *vm.Importer, cc int, separator st
|
||||
func (ip *influxProcessor) run(ctx context.Context) error {
|
||||
series, err := ip.ic.Explore()
|
||||
if err != nil {
|
||||
return fmt.Errorf("explore query failed: %s", err)
|
||||
return fmt.Errorf("explore query failed: %w", err)
|
||||
}
|
||||
if len(series) < 1 {
|
||||
return fmt.Errorf("found no timeseries to import")
|
||||
@@ -71,7 +71,7 @@ func (ip *influxProcessor) run(ctx context.Context) error {
|
||||
for s := range seriesCh {
|
||||
if err := ip.do(s); err != nil {
|
||||
influxErrorsTotal.Inc()
|
||||
errCh <- fmt.Errorf("request failed for %q.%q: %s", s.Measurement, s.Field, err)
|
||||
errCh <- fmt.Errorf("request failed for %q.%q: %w", s.Measurement, s.Field, err)
|
||||
return
|
||||
}
|
||||
influxSeriesProcessed.Inc()
|
||||
@@ -84,10 +84,10 @@ func (ip *influxProcessor) run(ctx context.Context) error {
|
||||
for _, s := range series {
|
||||
select {
|
||||
case infErr := <-errCh:
|
||||
return fmt.Errorf("influx error: %s", infErr)
|
||||
return fmt.Errorf("influx error: %w", infErr)
|
||||
case vmErr := <-ip.im.Errors():
|
||||
influxErrorsTotal.Inc()
|
||||
return fmt.Errorf("import process failed: %s", wrapErr(vmErr, ip.isVerbose))
|
||||
return fmt.Errorf("import process failed: %w", wrapErr(vmErr, ip.isVerbose))
|
||||
case seriesCh <- s:
|
||||
}
|
||||
}
|
||||
@@ -100,11 +100,11 @@ func (ip *influxProcessor) run(ctx context.Context) error {
|
||||
for vmErr := range ip.im.Errors() {
|
||||
if vmErr.Err != nil {
|
||||
influxErrorsTotal.Inc()
|
||||
return fmt.Errorf("import process failed: %s", wrapErr(vmErr, ip.isVerbose))
|
||||
return fmt.Errorf("import process failed: %w", wrapErr(vmErr, ip.isVerbose))
|
||||
}
|
||||
}
|
||||
for err := range errCh {
|
||||
return fmt.Errorf("import process failed: %s", err)
|
||||
return fmt.Errorf("import process failed: %w", err)
|
||||
}
|
||||
|
||||
log.Println("Import finished!")
|
||||
@@ -119,7 +119,7 @@ const valueField = "value"
|
||||
func (ip *influxProcessor) do(s *influx.Series) error {
|
||||
cr, err := ip.ic.FetchDataPoints(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch datapoints: %s", err)
|
||||
return fmt.Errorf("failed to fetch datapoints: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = cr.Close()
|
||||
|
||||
@@ -96,10 +96,10 @@ func NewClient(cfg Config) (*Client, error) {
|
||||
}
|
||||
hc, err := influx.NewHTTPClient(c)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to establish conn: %s", err)
|
||||
return nil, fmt.Errorf("failed to establish conn: %w", err)
|
||||
}
|
||||
if _, _, err := hc.Ping(time.Second); err != nil {
|
||||
return nil, fmt.Errorf("ping failed: %s", err)
|
||||
return nil, fmt.Errorf("ping failed: %w", err)
|
||||
}
|
||||
|
||||
chunkSize := cfg.ChunkSize
|
||||
@@ -155,7 +155,7 @@ func (c *Client) Explore() ([]*Series, error) {
|
||||
// {"measurement1": ["value1", "value2"]}
|
||||
mFields, err := c.fieldsByMeasurement()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get field keys: %s", err)
|
||||
return nil, fmt.Errorf("failed to get field keys: %w", err)
|
||||
}
|
||||
|
||||
if len(mFields) < 1 {
|
||||
@@ -165,12 +165,12 @@ func (c *Client) Explore() ([]*Series, error) {
|
||||
// {"measurement1": {"tag1", "tag2"}}
|
||||
measurementTags, err := c.getMeasurementTags()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get tags of measurements: %s", err)
|
||||
return nil, fmt.Errorf("failed to get tags of measurements: %w", err)
|
||||
}
|
||||
|
||||
series, err := c.getSeries()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get series: %s", err)
|
||||
return nil, fmt.Errorf("failed to get series: %w", err)
|
||||
}
|
||||
|
||||
var iSeries []*Series
|
||||
@@ -237,7 +237,7 @@ func (cr *ChunkedResponse) Next() ([]int64, []float64, error) {
|
||||
return nil, nil, err
|
||||
}
|
||||
if resp.Error() != nil {
|
||||
return nil, nil, fmt.Errorf("response error for %s: %s", cr.iq.Command, resp.Error())
|
||||
return nil, nil, fmt.Errorf("response error for %s: %w", cr.iq.Command, resp.Error())
|
||||
}
|
||||
if len(resp.Results) != 1 {
|
||||
return nil, nil, fmt.Errorf("unexpected number of results in response: %d", len(resp.Results))
|
||||
@@ -259,14 +259,13 @@ func (cr *ChunkedResponse) Next() ([]int64, []float64, error) {
|
||||
|
||||
fieldValues, ok := r.values[cr.field]
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("response doesn't contain filed %q", cr.field)
|
||||
return nil, nil, fmt.Errorf("response doesn't contain field %q", cr.field)
|
||||
}
|
||||
values := make([]float64, len(fieldValues))
|
||||
for i, fv := range fieldValues {
|
||||
v, err := toFloat64(fv)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to convert value %q.%v to float64: %s",
|
||||
cr.field, v, err)
|
||||
return nil, nil, fmt.Errorf("failed to convert value %q.%v to float64: %w", cr.field, v, err)
|
||||
}
|
||||
values[i] = v
|
||||
}
|
||||
@@ -294,7 +293,7 @@ func (c *Client) FetchDataPoints(s *Series) (*ChunkedResponse, error) {
|
||||
}
|
||||
cr, err := c.QueryAsChunk(iq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query %q err: %s", iq.Command, err)
|
||||
return nil, fmt.Errorf("query %q err: %w", iq.Command, err)
|
||||
}
|
||||
return &ChunkedResponse{cr, iq, s.Field}, nil
|
||||
}
|
||||
@@ -308,7 +307,7 @@ func (c *Client) fieldsByMeasurement() (map[string][]string, error) {
|
||||
log.Printf("fetching fields: %s", stringify(q))
|
||||
qValues, err := c.do(q)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while executing query %q: %s", q.Command, err)
|
||||
return nil, fmt.Errorf("error while executing query %q: %w", q.Command, err)
|
||||
}
|
||||
|
||||
var total int
|
||||
@@ -352,7 +351,7 @@ func (c *Client) getSeries() ([]*Series, error) {
|
||||
log.Printf("fetching series: %s", stringify(q))
|
||||
cr, err := c.QueryAsChunk(q)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while executing query %q: %s", q.Command, err)
|
||||
return nil, fmt.Errorf("error while executing query %q: %w", q.Command, err)
|
||||
}
|
||||
|
||||
const key = "key"
|
||||
@@ -366,7 +365,7 @@ func (c *Client) getSeries() ([]*Series, error) {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Error() != nil {
|
||||
return nil, fmt.Errorf("response error for query %q: %s", q.Command, resp.Error())
|
||||
return nil, fmt.Errorf("response error for query %q: %w", q.Command, resp.Error())
|
||||
}
|
||||
qValues, err := parseResult(resp.Results[0])
|
||||
if err != nil {
|
||||
@@ -417,7 +416,7 @@ func (c *Client) getMeasurementTags() (map[string]map[string]struct{}, error) {
|
||||
log.Printf("fetching tag keys: %s", stringify(q))
|
||||
cr, err := c.QueryAsChunk(q)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while executing query %q: %s", q.Command, err)
|
||||
return nil, fmt.Errorf("error while executing query %q: %w", q.Command, err)
|
||||
}
|
||||
|
||||
const tagKey = "tagKey"
|
||||
@@ -432,7 +431,7 @@ func (c *Client) getMeasurementTags() (map[string]map[string]struct{}, error) {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Error() != nil {
|
||||
return nil, fmt.Errorf("response error for query %q: %s", q.Command, resp.Error())
|
||||
return nil, fmt.Errorf("response error for query %q: %w", q.Command, resp.Error())
|
||||
}
|
||||
qValues, err := parseResult(resp.Results[0])
|
||||
if err != nil {
|
||||
@@ -455,10 +454,10 @@ func (c *Client) getMeasurementTags() (map[string]map[string]struct{}, error) {
|
||||
func (c *Client) do(q influx.Query) ([]queryValues, error) {
|
||||
res, err := c.Query(q)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query error: %s", err)
|
||||
return nil, fmt.Errorf("query error: %w", err)
|
||||
}
|
||||
if res.Error() != nil {
|
||||
return nil, fmt.Errorf("response error: %s", res.Error())
|
||||
return nil, fmt.Errorf("response error: %w", res.Error())
|
||||
}
|
||||
if len(res.Results) < 1 {
|
||||
return nil, fmt.Errorf("query returned 0 results")
|
||||
|
||||
@@ -71,7 +71,7 @@ func toFloat64(v any) (float64, error) {
|
||||
func parseDate(dateStr string) (int64, error) {
|
||||
startTime, err := time.Parse(time.RFC3339, dateStr)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("cannot parse %q: %s", dateStr, err)
|
||||
return 0, fmt.Errorf("cannot parse %q: %w", dateStr, err)
|
||||
}
|
||||
return startTime.UnixNano() / 1e6, nil
|
||||
}
|
||||
@@ -92,7 +92,7 @@ func (s *Series) unmarshal(v string) error {
|
||||
var err error
|
||||
s.LabelPairs, err = unmarshalTags(v[n+1:], noEscapeChars)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarhsal tags: %s", err)
|
||||
return fmt.Errorf("failed to unmarhsal tags: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ func main() {
|
||||
|
||||
tr, err := promauth.NewTLSTransport(certFile, keyFile, caFile, serverName, insecureSkipVerify, "vmctl_opentsdb")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create transport for -%s=%q: %s", otsdbAddr, addr, err)
|
||||
return fmt.Errorf("failed to create transport for -%s=%q: %w", otsdbAddr, addr, err)
|
||||
}
|
||||
oCfg := opentsdb.Config{
|
||||
Addr: addr,
|
||||
@@ -103,17 +103,17 @@ func main() {
|
||||
}
|
||||
otsdbClient, err := opentsdb.NewClient(oCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create opentsdb client: %s", err)
|
||||
return fmt.Errorf("failed to create opentsdb client: %w", err)
|
||||
}
|
||||
|
||||
vmCfg, err := initConfigVM(c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to init VM configuration: %s", err)
|
||||
return fmt.Errorf("failed to init VM configuration: %w", err)
|
||||
}
|
||||
|
||||
importer, err := vm.NewImporter(ctx, vmCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create VM importer: %s", err)
|
||||
return fmt.Errorf("failed to create VM importer: %w", err)
|
||||
}
|
||||
|
||||
otsdbProcessor := newOtsdbProcessor(otsdbClient, importer, c.Int(otsdbConcurrency), c.Bool(globalVerbose))
|
||||
@@ -137,7 +137,7 @@ func main() {
|
||||
|
||||
tc, err := promauth.NewTLSConfig(certFile, keyFile, caFile, serverName, insecureSkipVerify)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create TLS Config: %s", err)
|
||||
return fmt.Errorf("failed to create TLS Config: %w", err)
|
||||
}
|
||||
|
||||
iCfg := influx.Config{
|
||||
@@ -157,17 +157,17 @@ func main() {
|
||||
|
||||
influxClient, err := influx.NewClient(iCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create influx client: %s", err)
|
||||
return fmt.Errorf("failed to create influx client: %w", err)
|
||||
}
|
||||
|
||||
vmCfg, err := initConfigVM(c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to init VM configuration: %s", err)
|
||||
return fmt.Errorf("failed to init VM configuration: %w", err)
|
||||
}
|
||||
|
||||
importer, err = vm.NewImporter(ctx, vmCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create VM importer: %s", err)
|
||||
return fmt.Errorf("failed to create VM importer: %w", err)
|
||||
}
|
||||
|
||||
processor := newInfluxProcessor(
|
||||
@@ -203,7 +203,7 @@ func main() {
|
||||
|
||||
tr, err := promauth.NewTLSTransport(certFile, keyFile, caFile, serverName, insecureSkipVerify, "vmctl_remoteread")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create transport for -%s=%q: %s", remoteReadSrcAddr, addr, err)
|
||||
return fmt.Errorf("failed to create transport for -%s=%q: %w", remoteReadSrcAddr, addr, err)
|
||||
}
|
||||
|
||||
// Backwards compatible default values if none provided by user
|
||||
@@ -227,17 +227,17 @@ func main() {
|
||||
DisablePathAppend: c.Bool(remoteReadDisablePathAppend),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error create remote read client: %s", err)
|
||||
return fmt.Errorf("error create remote read client: %w", err)
|
||||
}
|
||||
|
||||
vmCfg, err := initConfigVM(c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to init VM configuration: %s", err)
|
||||
return fmt.Errorf("failed to init VM configuration: %w", err)
|
||||
}
|
||||
|
||||
importer, err := vm.NewImporter(ctx, vmCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create VM importer: %s", err)
|
||||
return fmt.Errorf("failed to create VM importer: %w", err)
|
||||
}
|
||||
|
||||
rmp := remoteReadProcessor{
|
||||
@@ -265,12 +265,12 @@ func main() {
|
||||
|
||||
vmCfg, err := initConfigVM(c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to init VM configuration: %s", err)
|
||||
return fmt.Errorf("failed to init VM configuration: %w", err)
|
||||
}
|
||||
|
||||
importer, err = vm.NewImporter(ctx, vmCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create VM importer: %s", err)
|
||||
return fmt.Errorf("failed to create VM importer: %w", err)
|
||||
}
|
||||
|
||||
promCfg := prometheus.Config{
|
||||
@@ -285,7 +285,7 @@ func main() {
|
||||
}
|
||||
cl, err := prometheus.NewClient(promCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create prometheus client: %s", err)
|
||||
return fmt.Errorf("failed to create prometheus client: %w", err)
|
||||
}
|
||||
|
||||
pp := prometheusProcessor{
|
||||
@@ -307,12 +307,12 @@ func main() {
|
||||
|
||||
vmCfg, err := initConfigVM(c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to init VM configuration: %s", err)
|
||||
return fmt.Errorf("failed to init VM configuration: %w", err)
|
||||
}
|
||||
|
||||
importer, err = vm.NewImporter(ctx, vmCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create VM importer: %s", err)
|
||||
return fmt.Errorf("failed to create VM importer: %w", err)
|
||||
}
|
||||
|
||||
mCfg := mimir.Config{
|
||||
@@ -335,7 +335,7 @@ func main() {
|
||||
}
|
||||
cl, err := mimir.NewClient(ctx, mCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create mimir client: %s", err)
|
||||
return fmt.Errorf("failed to create mimir client: %w", err)
|
||||
}
|
||||
|
||||
pp := prometheusProcessor{
|
||||
@@ -356,12 +356,12 @@ func main() {
|
||||
fmt.Println("Thanos import mode")
|
||||
vmCfg, err := initConfigVM(c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to init VM configuration: %s", err)
|
||||
return fmt.Errorf("failed to init VM configuration: %w", err)
|
||||
}
|
||||
|
||||
importer, err = vm.NewImporter(ctx, vmCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create VM importer: %s", err)
|
||||
return fmt.Errorf("failed to create VM importer: %w", err)
|
||||
}
|
||||
thanosCfg := thanos.Config{
|
||||
Snapshot: c.String(thanosSnapshot),
|
||||
@@ -374,7 +374,7 @@ func main() {
|
||||
}
|
||||
cl, err := thanos.NewClient(thanosCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create thanos client: %s", err)
|
||||
return fmt.Errorf("failed to create thanos client: %w", err)
|
||||
}
|
||||
|
||||
var aggrTypes []thanos.AggrType
|
||||
@@ -382,7 +382,7 @@ func main() {
|
||||
for _, typeStr := range aggrTypesStr {
|
||||
aggrType, err := thanos.ParseAggrType(typeStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse aggregate type %q: %s", typeStr, err)
|
||||
return fmt.Errorf("failed to parse aggregate type %q: %w", typeStr, err)
|
||||
}
|
||||
aggrTypes = append(aggrTypes, aggrType)
|
||||
}
|
||||
@@ -415,7 +415,7 @@ func main() {
|
||||
bfMinDuration := c.Duration(vmNativeBackoffMinDuration)
|
||||
bf, err := backoff.New(bfRetries, bfFactor, bfMinDuration)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create backoff object: %s", err)
|
||||
return fmt.Errorf("failed to create backoff object: %w", err)
|
||||
}
|
||||
|
||||
disableKeepAlive := c.Bool(vmNativeDisableHTTPKeepAlive)
|
||||
@@ -439,7 +439,7 @@ func main() {
|
||||
|
||||
srcTC, err := promauth.NewTLSConfig(srcCertFile, srcKeyFile, srcCAFile, srcServerName, srcInsecureSkipVerify)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create TLS Config: %s", err)
|
||||
return fmt.Errorf("failed to create TLS Config: %w", err)
|
||||
}
|
||||
|
||||
trSrc := httputil.NewTransport(false, "vmctl_src")
|
||||
@@ -457,7 +457,7 @@ func main() {
|
||||
auth.WithBearer(c.String(vmNativeDstBearerToken)),
|
||||
auth.WithHeaders(c.String(vmNativeDstHeaders)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error initialize auth config for destination: %s", dstAddr)
|
||||
return fmt.Errorf("error initialize auth config for destination: %s: %w", dstAddr, err)
|
||||
}
|
||||
|
||||
// create TLS config
|
||||
@@ -469,7 +469,7 @@ func main() {
|
||||
|
||||
dstTC, err := promauth.NewTLSConfig(dstCertFile, dstKeyFile, dstCAFile, dstServerName, dstInsecureSkipVerify)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create TLS Config: %s", err)
|
||||
return fmt.Errorf("failed to create TLS Config: %w", err)
|
||||
}
|
||||
|
||||
trDst := httputil.NewTransport(false, "vmctl_dst")
|
||||
@@ -534,7 +534,7 @@ func main() {
|
||||
log.Printf("verifying block at path=%q", blockPath)
|
||||
f, err := os.OpenFile(blockPath, os.O_RDONLY, 0600)
|
||||
if err != nil {
|
||||
return cli.Exit(fmt.Errorf("cannot open exported block at path=%q err=%w", blockPath, err), 1)
|
||||
return cli.Exit(fmt.Errorf("cannot open exported block at path=%q: %w", blockPath, err), 1)
|
||||
}
|
||||
defer f.Close()
|
||||
var blocksCount atomic.Uint64
|
||||
@@ -542,7 +542,7 @@ func main() {
|
||||
blocksCount.Add(1)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return cli.Exit(fmt.Errorf("cannot parse block at path=%q, blocksCount=%d, err=%w", blockPath, blocksCount.Load(), err), 1)
|
||||
return cli.Exit(fmt.Errorf("cannot parse block at path=%q, blocksCount=%d: %w", blockPath, blocksCount.Load(), err), 1)
|
||||
}
|
||||
log.Printf("successfully verified block at path=%q, blockCount=%d", blockPath, blocksCount.Load())
|
||||
return nil
|
||||
@@ -563,11 +563,11 @@ func main() {
|
||||
}()
|
||||
|
||||
err = app.Run(os.Args)
|
||||
pushmetrics.StopAndPush()
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
log.Printf("Total time: %v", time.Since(start))
|
||||
pushmetrics.StopAndPush()
|
||||
}
|
||||
|
||||
func initConfigVM(c *cli.Context) (vm.Config, error) {
|
||||
@@ -585,7 +585,7 @@ func initConfigVM(c *cli.Context) (vm.Config, error) {
|
||||
|
||||
tr, err := promauth.NewTLSTransport(certFile, keyFile, caFile, serverName, insecureSkipVerify, "vmctl_client")
|
||||
if err != nil {
|
||||
return vm.Config{}, fmt.Errorf("failed to create transport for -%s=%q: %s", vmAddr, addr, err)
|
||||
return vm.Config{}, fmt.Errorf("failed to create transport for -%s=%q: %w", vmAddr, addr, err)
|
||||
}
|
||||
|
||||
bfRetries := c.Int(vmBackoffRetries)
|
||||
@@ -593,14 +593,21 @@ func initConfigVM(c *cli.Context) (vm.Config, error) {
|
||||
bfMinDuration := c.Duration(vmBackoffMinDuration)
|
||||
bf, err := backoff.New(bfRetries, bfFactor, bfMinDuration)
|
||||
if err != nil {
|
||||
return vm.Config{}, fmt.Errorf("failed to create backoff object: %s", err)
|
||||
return vm.Config{}, fmt.Errorf("failed to create backoff object: %w", err)
|
||||
}
|
||||
|
||||
authCfg, err := auth.Generate(
|
||||
auth.WithBasicAuth(c.String(vmUser), c.String(vmPassword)),
|
||||
auth.WithBearer(c.String(vmBearerToken)),
|
||||
auth.WithHeaders(c.String(vmHeaders)))
|
||||
if err != nil {
|
||||
return vm.Config{}, fmt.Errorf("error initialize auth config for destination: %s: %w", addr, err)
|
||||
}
|
||||
|
||||
return vm.Config{
|
||||
Addr: addr,
|
||||
Transport: tr,
|
||||
User: c.String(vmUser),
|
||||
Password: c.String(vmPassword),
|
||||
AuthCfg: authCfg,
|
||||
Concurrency: uint8(c.Int(vmConcurrency)),
|
||||
Compress: c.Bool(vmCompress),
|
||||
AccountID: c.String(vmAccountID),
|
||||
|
||||
@@ -54,7 +54,7 @@ func (lbr *lazyBlockReader) initialize() error {
|
||||
// fetching block and parse it and store it in lbr.reader
|
||||
temp, err := lbr.mkTempDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp dir: %s", err)
|
||||
return fmt.Errorf("failed to create temp dir: %w", err)
|
||||
}
|
||||
|
||||
lbr.tempDirPath = temp
|
||||
@@ -85,7 +85,7 @@ func (lbr *lazyBlockReader) initialize() error {
|
||||
return fmt.Errorf("failed to fetch chunk file: %q: %w", chunkName, err)
|
||||
}
|
||||
if err := lbr.writeFile(temp, blockChunkPath, chunk); err != nil {
|
||||
return fmt.Errorf("failed to write chunk file: %q: %s", chunkName, err)
|
||||
return fmt.Errorf("failed to write chunk file: %q: %w", chunkName, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ func (lbr *lazyBlockReader) Meta() tsdb.BlockMeta {
|
||||
// Size returns the number of bytes that the block takes up on disk.
|
||||
func (lbr *lazyBlockReader) Size() int64 {
|
||||
if err := lbr.initialize(); err != nil {
|
||||
lbr.err = fmt.Errorf("error get Size of the block: %s, return zero size", err)
|
||||
lbr.err = fmt.Errorf("error get Size of the block: %w, return zero size", err)
|
||||
return 0
|
||||
}
|
||||
return lbr.reader.Size()
|
||||
@@ -167,11 +167,11 @@ func (lbr *lazyBlockReader) Close() error {
|
||||
func (lbr *lazyBlockReader) mkTempDir() (string, error) {
|
||||
temp, err := os.MkdirTemp("", lbr.ID.String())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create temp dir: %s", err)
|
||||
return "", fmt.Errorf("failed to create temp dir: %w", err)
|
||||
}
|
||||
err = os.Mkdir(filepath.Join(temp, "chunks"), os.ModePerm)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create temp dir: %s", err)
|
||||
return "", fmt.Errorf("failed to create temp dir: %w", err)
|
||||
}
|
||||
return temp, nil
|
||||
}
|
||||
|
||||
@@ -133,11 +133,11 @@ func NewClient(ctx context.Context, cfg Config) (*Client, error) {
|
||||
c.RemoteFS = rfs
|
||||
timeMin, err := utils.ParseTime(cfg.Filter.TimeMin)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse min time in filter: %s", err)
|
||||
return nil, fmt.Errorf("failed to parse min time in filter: %w", err)
|
||||
}
|
||||
timeMax, err := utils.ParseTime(cfg.Filter.TimeMax)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse max time in filter: %s", err)
|
||||
return nil, fmt.Errorf("failed to parse max time in filter: %w", err)
|
||||
}
|
||||
c.filter = filter{
|
||||
min: timeMin.UnixMilli(),
|
||||
@@ -156,7 +156,7 @@ func (c *Client) Explore() ([]tsdb.BlockReader, error) {
|
||||
|
||||
indexFile, err := c.fetchIndexFile()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch index file: %s", err)
|
||||
return nil, fmt.Errorf("failed to fetch index file: %w", err)
|
||||
}
|
||||
|
||||
var blocksToImport []tsdb.BlockReader
|
||||
@@ -172,7 +172,7 @@ func (c *Client) Explore() ([]tsdb.BlockReader, error) {
|
||||
|
||||
lazyBlockReader, err := newLazyBlockReader(block, c.RemoteFS)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create lazy block reader: %s", err)
|
||||
return nil, fmt.Errorf("failed to create lazy block reader: %w", err)
|
||||
}
|
||||
blocksToImport = append(blocksToImport, lazyBlockReader)
|
||||
}
|
||||
@@ -185,7 +185,7 @@ func (c *Client) Explore() ([]tsdb.BlockReader, error) {
|
||||
func (c *Client) Read(ctx context.Context, block tsdb.BlockReader) (*prometheus.CloseableSeriesSet, error) {
|
||||
meta := block.Meta()
|
||||
if b, ok := block.(*lazyBlockReader); ok && b.Err() != nil {
|
||||
return nil, fmt.Errorf("failed to read block: %s", b.Err())
|
||||
return nil, fmt.Errorf("failed to read block: %w", b.Err())
|
||||
}
|
||||
|
||||
if meta.ULID.String() == "" {
|
||||
@@ -218,20 +218,20 @@ func (c *Client) fetchIndexFile() (*Index, error) {
|
||||
|
||||
file, err := c.ReadFile(bucketIndexCompressedFilename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read bucket index: %s", err)
|
||||
return nil, fmt.Errorf("failed to read bucket index: %w", err)
|
||||
}
|
||||
|
||||
r := bytes.NewReader(file)
|
||||
// Read all the content.
|
||||
gzipReader, err := gzip.NewReader(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create gzip reader: %s", err)
|
||||
return nil, fmt.Errorf("failed to create gzip reader: %w", err)
|
||||
}
|
||||
|
||||
var indexFile Index
|
||||
err = json.NewDecoder(gzipReader).Decode(&indexFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode bucket index: %s", err)
|
||||
return nil, fmt.Errorf("failed to decode bucket index: %w", err)
|
||||
}
|
||||
|
||||
return &indexFile, nil
|
||||
|
||||
@@ -47,7 +47,7 @@ func (c *Client) Explore(ctx context.Context, f Filter, tenantID string, start,
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
exploreRequestsErrorsTotal.Inc()
|
||||
return nil, fmt.Errorf("cannot create request to %q: %s", url, err)
|
||||
return nil, fmt.Errorf("cannot create request to %q: %w", url, err)
|
||||
}
|
||||
|
||||
params := req.URL.Query()
|
||||
@@ -60,14 +60,14 @@ func (c *Client) Explore(ctx context.Context, f Filter, tenantID string, start,
|
||||
if err != nil {
|
||||
exploreRequestsErrorsTotal.Inc()
|
||||
exploreDuration.UpdateDuration(startTime)
|
||||
return nil, fmt.Errorf("series request failed: %s", err)
|
||||
return nil, fmt.Errorf("series request failed: %w", err)
|
||||
}
|
||||
|
||||
var response Response
|
||||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||
exploreRequestsErrorsTotal.Inc()
|
||||
exploreDuration.UpdateDuration(startTime)
|
||||
return nil, fmt.Errorf("cannot decode series response: %s", err)
|
||||
return nil, fmt.Errorf("cannot decode series response: %w", err)
|
||||
}
|
||||
exploreDuration.UpdateDuration(startTime)
|
||||
return response.MetricNames, resp.Body.Close()
|
||||
@@ -80,19 +80,19 @@ func (c *Client) ImportPipe(ctx context.Context, dstURL string, pr *io.PipeReade
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, dstURL, pr)
|
||||
if err != nil {
|
||||
importRequestsErrorsTotal.Inc()
|
||||
return fmt.Errorf("cannot create import request to %q: %s", c.Addr, err)
|
||||
return fmt.Errorf("cannot create import request to %q: %w", c.Addr, err)
|
||||
}
|
||||
|
||||
importResp, err := c.do(req, http.StatusNoContent)
|
||||
if err != nil {
|
||||
importRequestsErrorsTotal.Inc()
|
||||
importDuration.UpdateDuration(startTime)
|
||||
return fmt.Errorf("import request failed: %s", err)
|
||||
return fmt.Errorf("import request failed: %w", err)
|
||||
}
|
||||
if err := importResp.Body.Close(); err != nil {
|
||||
importRequestsErrorsTotal.Inc()
|
||||
importDuration.UpdateDuration(startTime)
|
||||
return fmt.Errorf("cannot close import response body: %s", err)
|
||||
return fmt.Errorf("cannot close import response body: %w", err)
|
||||
}
|
||||
importDuration.UpdateDuration(startTime)
|
||||
return nil
|
||||
@@ -105,7 +105,7 @@ func (c *Client) ExportPipe(ctx context.Context, url string, f Filter) (io.ReadC
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
exportRequestsErrorsTotal.Inc()
|
||||
return nil, fmt.Errorf("cannot create request to %q: %s", c.Addr, err)
|
||||
return nil, fmt.Errorf("cannot create request to %q: %w", c.Addr, err)
|
||||
}
|
||||
|
||||
params := req.URL.Query()
|
||||
@@ -136,7 +136,7 @@ func (c *Client) GetSourceTenants(ctx context.Context, f Filter) ([]string, erro
|
||||
u := fmt.Sprintf("%s/%s", c.Addr, nativeTenantsAddr)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create request to %q: %s", u, err)
|
||||
return nil, fmt.Errorf("cannot create request to %q: %w", u, err)
|
||||
}
|
||||
|
||||
params := req.URL.Query()
|
||||
@@ -150,18 +150,18 @@ func (c *Client) GetSourceTenants(ctx context.Context, f Filter) ([]string, erro
|
||||
|
||||
resp, err := c.do(req, http.StatusOK)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tenants request failed: %s", err)
|
||||
return nil, fmt.Errorf("tenants request failed: %w", err)
|
||||
}
|
||||
|
||||
var r struct {
|
||||
Tenants []string `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
|
||||
return nil, fmt.Errorf("cannot decode tenants response: %s", err)
|
||||
return nil, fmt.Errorf("cannot decode tenants response: %w", err)
|
||||
}
|
||||
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
return nil, fmt.Errorf("cannot close tenants response body: %s", err)
|
||||
return nil, fmt.Errorf("cannot close tenants response body: %w", err)
|
||||
}
|
||||
|
||||
return r.Tenants, nil
|
||||
@@ -180,7 +180,7 @@ func (c *Client) do(req *http.Request, expSC int) (*http.Response, error) {
|
||||
if resp.StatusCode != expSC {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body for status code %d: %s", resp.StatusCode, err)
|
||||
return nil, fmt.Errorf("failed to read response body for status code %d: %w", resp.StatusCode, err)
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected response code %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func (op *otsdbProcessor) run(ctx context.Context) error {
|
||||
q := fmt.Sprintf("%s/api/suggest?type=metrics&q=%s&max=%d", op.oc.Addr, filter, op.oc.Limit)
|
||||
m, err := op.oc.FindMetrics(q)
|
||||
if err != nil {
|
||||
return fmt.Errorf("metric discovery failed for %q: %s", q, err)
|
||||
return fmt.Errorf("metric discovery failed for %q: %w", q, err)
|
||||
}
|
||||
metrics = append(metrics, m...)
|
||||
}
|
||||
@@ -76,7 +76,7 @@ func (op *otsdbProcessor) run(ctx context.Context) error {
|
||||
log.Printf("Starting work on %s", metric)
|
||||
serieslist, err := op.oc.FindSeries(metric)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't retrieve series list for %s : %s", metric, err)
|
||||
return fmt.Errorf("couldn't retrieve series list for %s: %w", metric, err)
|
||||
}
|
||||
/*
|
||||
Create channels for collecting/processing series and errors
|
||||
@@ -95,7 +95,7 @@ func (op *otsdbProcessor) run(ctx context.Context) error {
|
||||
for s := range seriesCh {
|
||||
if err := op.do(s); err != nil {
|
||||
otsdbErrorsTotal.Inc()
|
||||
errCh <- fmt.Errorf("couldn't retrieve series for %s : %s", metric, err)
|
||||
errCh <- fmt.Errorf("couldn't retrieve series for %s: %w", metric, err)
|
||||
return
|
||||
}
|
||||
otsdbSeriesProcessed.Inc()
|
||||
@@ -112,7 +112,7 @@ func (op *otsdbProcessor) run(ctx context.Context) error {
|
||||
// check for any lingering errors on the query side
|
||||
for otsdbErr := range errCh {
|
||||
if runErr == nil {
|
||||
runErr = fmt.Errorf("import process failed: \n%s", otsdbErr)
|
||||
runErr = fmt.Errorf("import process failed:\n%w", otsdbErr)
|
||||
}
|
||||
}
|
||||
bar.Finish()
|
||||
@@ -125,7 +125,7 @@ func (op *otsdbProcessor) run(ctx context.Context) error {
|
||||
for vmErr := range op.im.Errors() {
|
||||
if vmErr.Err != nil {
|
||||
otsdbErrorsTotal.Inc()
|
||||
return fmt.Errorf("import process failed: %s", wrapErr(vmErr, op.isVerbose))
|
||||
return fmt.Errorf("import process failed: %w", wrapErr(vmErr, op.isVerbose))
|
||||
}
|
||||
}
|
||||
log.Println("Import finished!")
|
||||
@@ -141,12 +141,12 @@ func (op *otsdbProcessor) sendQueries(ctx context.Context, serieslist []opentsdb
|
||||
for _, tr := range rt.QueryRanges {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("context canceled: %s", ctx.Err())
|
||||
return fmt.Errorf("context canceled: %w", ctx.Err())
|
||||
case otsdbErr := <-errCh:
|
||||
otsdbErrorsTotal.Inc()
|
||||
return fmt.Errorf("opentsdb error: %s", otsdbErr)
|
||||
return fmt.Errorf("opentsdb error: %w", otsdbErr)
|
||||
case vmErr := <-op.im.Errors():
|
||||
return fmt.Errorf("import process failed: %s", wrapErr(vmErr, op.isVerbose))
|
||||
return fmt.Errorf("import process failed: %w", wrapErr(vmErr, op.isVerbose))
|
||||
case seriesCh <- queryObj{
|
||||
Tr: tr, StartTime: startTime,
|
||||
Series: series, Rt: opentsdb.RetentionMeta{
|
||||
@@ -166,7 +166,7 @@ func (op *otsdbProcessor) do(s queryObj) error {
|
||||
end := s.StartTime - s.Tr.End
|
||||
data, err := op.oc.GetData(s.Series, s.Rt, start, end, op.oc.MsecsTime)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to collect data for %v in %v:%v :: %v", s.Series, s.Rt, s.Tr, err)
|
||||
return fmt.Errorf("failed to collect data for %v in %v:%v :: %w", s.Series, s.Rt, s.Tr, err)
|
||||
}
|
||||
if len(data.Timestamps) < 1 || len(data.Values) < 1 {
|
||||
log.Printf("no data found for %v in %v:%v...skipping", s.Series, s.Rt, s.Tr)
|
||||
|
||||
@@ -106,7 +106,7 @@ func (c Client) FindMetrics(q string) ([]string, error) {
|
||||
|
||||
resp, err := c.c.Get(q)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to send GET request to %q: %s", q, err)
|
||||
return nil, fmt.Errorf("failed to send GET request to %q: %w", q, err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode != 200 {
|
||||
@@ -114,12 +114,12 @@ func (c Client) FindMetrics(q string) ([]string, error) {
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not retrieve metric data from %q: %s", q, err)
|
||||
return nil, fmt.Errorf("could not retrieve metric data from %q: %w", q, err)
|
||||
}
|
||||
var metriclist []string
|
||||
err = json.Unmarshal(body, &metriclist)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response from %q: %s", q, err)
|
||||
return nil, fmt.Errorf("failed to read response from %q: %w", q, err)
|
||||
}
|
||||
return metriclist, nil
|
||||
}
|
||||
@@ -130,7 +130,7 @@ func (c Client) FindSeries(metric string) ([]Meta, error) {
|
||||
q := fmt.Sprintf("%s/api/search/lookup?m=%s&limit=%d", c.Addr, metric, c.Limit)
|
||||
resp, err := c.c.Get(q)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to send GET request to %q: %s", q, err)
|
||||
return nil, fmt.Errorf("failed to send GET request to %q: %w", q, err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode != 200 {
|
||||
@@ -138,12 +138,12 @@ func (c Client) FindSeries(metric string) ([]Meta, error) {
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not retrieve series data from %q: %s", q, err)
|
||||
return nil, fmt.Errorf("could not retrieve series data from %q: %w", q, err)
|
||||
}
|
||||
var results MetaResults
|
||||
err = json.Unmarshal(body, &results)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response from %q: %s", q, err)
|
||||
return nil, fmt.Errorf("failed to read response from %q: %w", q, err)
|
||||
}
|
||||
return results.Results, nil
|
||||
}
|
||||
@@ -183,7 +183,7 @@ func (c Client) GetData(series Meta, rt RetentionMeta, start int64, end int64, m
|
||||
q := fmt.Sprintf("%s/api/query?%s", c.Addr, queryStr)
|
||||
resp, err := c.c.Get(q)
|
||||
if err != nil {
|
||||
return Metric{}, fmt.Errorf("failed to send GET request to %q: %s", q, err)
|
||||
return Metric{}, fmt.Errorf("failed to send GET request to %q: %w", q, err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
/*
|
||||
@@ -303,7 +303,7 @@ func NewClient(cfg Config) (*Client, error) {
|
||||
for _, r := range cfg.Retentions {
|
||||
ret, err := convertRetention(r, offsetSecs, cfg.MsecsTime)
|
||||
if err != nil {
|
||||
return &Client{}, fmt.Errorf("couldn't parse retention %q :: %v", r, err)
|
||||
return &Client{}, fmt.Errorf("couldn't parse retention %q :: %w", r, err)
|
||||
}
|
||||
retentions = append(retentions, ret)
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ func convertRetention(retention string, offset int64, msecTime bool) (Retention,
|
||||
}
|
||||
queryLengthDuration, err := convertDuration(chunks[2])
|
||||
if err != nil {
|
||||
return Retention{}, fmt.Errorf("invalid ttl (second order) duration string: %q: %s", chunks[2], err)
|
||||
return Retention{}, fmt.Errorf("invalid ttl (second order) duration string: %q: %w", chunks[2], err)
|
||||
}
|
||||
// set ttl in milliseconds, unless we aren't using millisecond time in OpenTSDB...then use seconds
|
||||
queryLength := queryLengthDuration.Milliseconds()
|
||||
@@ -110,7 +110,7 @@ func convertRetention(retention string, offset int64, msecTime bool) (Retention,
|
||||
|
||||
aggTimeDuration, err := convertDuration(aggregates[1])
|
||||
if err != nil {
|
||||
return Retention{}, fmt.Errorf("invalid aggregation time duration string: %q: %s", aggregates[1], err)
|
||||
return Retention{}, fmt.Errorf("invalid aggregation time duration string: %q: %w", aggregates[1], err)
|
||||
}
|
||||
aggTime := aggTimeDuration.Milliseconds()
|
||||
if !msecTime {
|
||||
@@ -119,7 +119,7 @@ func convertRetention(retention string, offset int64, msecTime bool) (Retention,
|
||||
|
||||
rowLengthDuration, err := convertDuration(chunks[1])
|
||||
if err != nil {
|
||||
return Retention{}, fmt.Errorf("invalid row length (first order) duration string: %q: %s", chunks[1], err)
|
||||
return Retention{}, fmt.Errorf("invalid row length (first order) duration string: %q: %w", chunks[1], err)
|
||||
}
|
||||
// set length of each row in milliseconds, unless we aren't using millisecond time in OpenTSDB...then use seconds
|
||||
rowLength := rowLengthDuration.Milliseconds()
|
||||
|
||||
@@ -46,7 +46,7 @@ type prometheusProcessor struct {
|
||||
func (pp *prometheusProcessor) run(ctx context.Context) error {
|
||||
blocks, err := pp.cl.Explore()
|
||||
if err != nil {
|
||||
return fmt.Errorf("explore failed: %s", err)
|
||||
return fmt.Errorf("explore failed: %w", err)
|
||||
}
|
||||
if len(blocks) < 1 {
|
||||
return fmt.Errorf("found no blocks to import")
|
||||
@@ -57,7 +57,7 @@ func (pp *prometheusProcessor) run(ctx context.Context) error {
|
||||
}
|
||||
|
||||
if err := pp.processBlocks(ctx, blocks); err != nil {
|
||||
return fmt.Errorf("migration failed: %s", err)
|
||||
return fmt.Errorf("migration failed: %w", err)
|
||||
}
|
||||
|
||||
log.Println("Import finished!")
|
||||
@@ -68,7 +68,7 @@ func (pp *prometheusProcessor) run(ctx context.Context) error {
|
||||
func (pp *prometheusProcessor) do(ctx context.Context, b tsdb.BlockReader) error {
|
||||
css, err := pp.cl.Read(ctx, b)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read block: %s", err)
|
||||
return fmt.Errorf("failed to read block: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := css.Close(); err != nil {
|
||||
@@ -146,7 +146,7 @@ func (pp *prometheusProcessor) processBlocks(ctx context.Context, blocks []tsdb.
|
||||
for br := range blockReadersCh {
|
||||
if err := pp.do(ctx, br); err != nil {
|
||||
promErrorsTotal.Inc()
|
||||
errCh <- fmt.Errorf("cannot read block %q: %s", br.Meta().ULID, err)
|
||||
errCh <- fmt.Errorf("cannot read block %q: %w", br.Meta().ULID, err)
|
||||
return
|
||||
}
|
||||
if cb, ok := br.(io.Closer); ok {
|
||||
@@ -164,11 +164,11 @@ func (pp *prometheusProcessor) processBlocks(ctx context.Context, blocks []tsdb.
|
||||
select {
|
||||
case promErr := <-errCh:
|
||||
close(blockReadersCh)
|
||||
return fmt.Errorf("prometheus error: %s", promErr)
|
||||
return fmt.Errorf("prometheus error: %w", promErr)
|
||||
case vmErr := <-pp.im.Errors():
|
||||
close(blockReadersCh)
|
||||
promErrorsTotal.Inc()
|
||||
return fmt.Errorf("import process failed: %s", wrapErr(vmErr, pp.isVerbose))
|
||||
return fmt.Errorf("import process failed: %w", wrapErr(vmErr, pp.isVerbose))
|
||||
case blockReadersCh <- br:
|
||||
}
|
||||
}
|
||||
@@ -182,11 +182,11 @@ func (pp *prometheusProcessor) processBlocks(ctx context.Context, blocks []tsdb.
|
||||
for vmErr := range pp.im.Errors() {
|
||||
if vmErr.Err != nil {
|
||||
promErrorsTotal.Inc()
|
||||
return fmt.Errorf("import process failed: %s", wrapErr(vmErr, pp.isVerbose))
|
||||
return fmt.Errorf("import process failed: %w", wrapErr(vmErr, pp.isVerbose))
|
||||
}
|
||||
}
|
||||
for err := range errCh {
|
||||
return fmt.Errorf("import process failed: %s", err)
|
||||
return fmt.Errorf("import process failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -59,12 +59,12 @@ func (f filter) inRange(minV, maxV int64) bool {
|
||||
func NewClient(cfg Config) (*Client, error) {
|
||||
db, err := tsdb.OpenDBReadOnly(cfg.Snapshot, cfg.TemporaryDir, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open snapshot %q: %s", cfg.Snapshot, err)
|
||||
return nil, fmt.Errorf("failed to open snapshot %q: %w", cfg.Snapshot, err)
|
||||
}
|
||||
c := &Client{DBReadOnly: db}
|
||||
timeMin, timeMax, err := parseTime(cfg.Filter.TimeMin, cfg.Filter.TimeMax)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse time in filter: %s", err)
|
||||
return nil, fmt.Errorf("failed to parse time in filter: %w", err)
|
||||
}
|
||||
c.filter = filter{
|
||||
min: timeMin,
|
||||
@@ -83,7 +83,7 @@ func NewClient(cfg Config) (*Client, error) {
|
||||
func (c *Client) Explore() ([]tsdb.BlockReader, error) {
|
||||
blocks, err := c.Blocks()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch blocks: %s", err)
|
||||
return nil, fmt.Errorf("failed to fetch blocks: %w", err)
|
||||
}
|
||||
s := &vmctlutil.Stats{
|
||||
Filtered: c.filter.min != 0 || c.filter.max != 0 || c.filter.label != "",
|
||||
@@ -142,14 +142,14 @@ func parseTime(start, end string) (int64, int64, error) {
|
||||
if start != "" {
|
||||
v, err := time.Parse(time.RFC3339, start)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("failed to parse %q: %s", start, err)
|
||||
return 0, 0, fmt.Errorf("failed to parse %q: %w", start, err)
|
||||
}
|
||||
s = v.UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
if end != "" {
|
||||
v, err := time.Parse(time.RFC3339, end)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("failed to parse %q: %s", end, err)
|
||||
return 0, 0, fmt.Errorf("failed to parse %q: %w", end, err)
|
||||
}
|
||||
e = v.UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func (rrp *remoteReadProcessor) run(ctx context.Context) error {
|
||||
|
||||
ranges, err := stepper.SplitDateRange(*rrp.filter.timeStart, *rrp.filter.timeEnd, rrp.filter.chunk, rrp.filter.timeReverse)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create date ranges for the given time filters: %v", err)
|
||||
return fmt.Errorf("failed to create date ranges for the given time filters: %w", err)
|
||||
}
|
||||
|
||||
question := fmt.Sprintf("Selected time range %q - %q will be split into %d ranges according to %q step. Continue?",
|
||||
@@ -74,7 +74,7 @@ func (rrp *remoteReadProcessor) run(ctx context.Context) error {
|
||||
for r := range rangeC {
|
||||
if err := rrp.do(ctx, r); err != nil {
|
||||
remoteReadErrorsTotal.Inc()
|
||||
errCh <- fmt.Errorf("request failed for: %s", err)
|
||||
errCh <- fmt.Errorf("request failed for: %w", err)
|
||||
return
|
||||
}
|
||||
remoteReadRangesProcessed.Inc()
|
||||
@@ -86,10 +86,10 @@ func (rrp *remoteReadProcessor) run(ctx context.Context) error {
|
||||
for _, r := range ranges {
|
||||
select {
|
||||
case infErr := <-errCh:
|
||||
return fmt.Errorf("remote read error: %s", infErr)
|
||||
return fmt.Errorf("remote read error: %w", infErr)
|
||||
case vmErr := <-rrp.dst.Errors():
|
||||
remoteReadErrorsTotal.Inc()
|
||||
return fmt.Errorf("import process failed: %s", wrapErr(vmErr, rrp.isVerbose))
|
||||
return fmt.Errorf("import process failed: %w", wrapErr(vmErr, rrp.isVerbose))
|
||||
case rangeC <- &remoteread.Filter{
|
||||
StartTimestampMs: r[0].UnixMilli(),
|
||||
EndTimestampMs: r[1].UnixMilli(),
|
||||
@@ -105,11 +105,11 @@ func (rrp *remoteReadProcessor) run(ctx context.Context) error {
|
||||
for vmErr := range rrp.dst.Errors() {
|
||||
if vmErr.Err != nil {
|
||||
remoteReadErrorsTotal.Inc()
|
||||
return fmt.Errorf("import process failed: %s", wrapErr(vmErr, rrp.isVerbose))
|
||||
return fmt.Errorf("import process failed: %w", wrapErr(vmErr, rrp.isVerbose))
|
||||
}
|
||||
}
|
||||
for err := range errCh {
|
||||
return fmt.Errorf("import process failed: %s", err)
|
||||
return fmt.Errorf("import process failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -119,7 +119,7 @@ func (rrp *remoteReadProcessor) do(ctx context.Context, filter *remoteread.Filte
|
||||
return rrp.src.Read(ctx, filter, func(series *vm.TimeSeries) error {
|
||||
if err := rrp.dst.Input(series); err != nil {
|
||||
return fmt.Errorf(
|
||||
"failed to read data for time range start: %d, end: %d, %s",
|
||||
"failed to read data for time range start: %d, end: %d: %w",
|
||||
filter.StartTimestampMs, filter.EndTimestampMs, err)
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -157,7 +157,7 @@ func (c *Client) Read(ctx context.Context, filter *Filter, streamCb StreamCallba
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return fmt.Errorf("fetch request has ben cancelled")
|
||||
}
|
||||
return fmt.Errorf("error while fetching data from remote storage: %s", err)
|
||||
return fmt.Errorf("error while fetching data from remote storage: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func (f filter) inRange(minV, maxV int64) bool {
|
||||
func NewClient(cfg Config) (*Client, error) {
|
||||
minTime, maxTime, err := parseTime(cfg.Filter.TimeMin, cfg.Filter.TimeMax)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse time in filter: %s", err)
|
||||
return nil, fmt.Errorf("failed to parse time in filter: %w", err)
|
||||
}
|
||||
return &Client{
|
||||
snapshotPath: cfg.Snapshot,
|
||||
@@ -183,14 +183,14 @@ func parseTime(start, end string) (int64, int64, error) {
|
||||
if start != "" {
|
||||
v, err := time.Parse(time.RFC3339, start)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("failed to parse %q: %s", start, err)
|
||||
return 0, 0, fmt.Errorf("failed to parse %q: %w", start, err)
|
||||
}
|
||||
s = v.UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
if end != "" {
|
||||
v, err := time.Parse(time.RFC3339, end)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("failed to parse %q: %s", end, err)
|
||||
return 0, 0, fmt.Errorf("failed to parse %q: %w", end, err)
|
||||
}
|
||||
e = v.UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func (tp *thanosProcessor) run(ctx context.Context) error {
|
||||
// Use the first aggregate type to explore blocks (block list is the same for all types)
|
||||
blocks, err := tp.cl.Explore(tp.aggrTypes[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("explore failed: %s", err)
|
||||
return fmt.Errorf("explore failed: %w", err)
|
||||
}
|
||||
if len(blocks) < 1 {
|
||||
return fmt.Errorf("found no blocks to import")
|
||||
@@ -84,7 +84,7 @@ func (tp *thanosProcessor) run(ctx context.Context) error {
|
||||
log.Println("Processing raw blocks (resolution=0)...")
|
||||
stats, err := tp.processBlocks(rawBlocks, thanos.AggrTypeNone, bar)
|
||||
if err != nil {
|
||||
return fmt.Errorf("migration failed for raw blocks: %s", err)
|
||||
return fmt.Errorf("migration failed for raw blocks: %w", err)
|
||||
}
|
||||
phases = append(phases, phaseStats{
|
||||
name: "raw",
|
||||
@@ -108,7 +108,7 @@ func (tp *thanosProcessor) run(ctx context.Context) error {
|
||||
|
||||
aggrBlocks, err := tp.cl.Explore(aggrType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("explore failed for aggr type %s: %s", aggrType, err)
|
||||
return fmt.Errorf("explore failed for aggr type %s: %w", aggrType, err)
|
||||
}
|
||||
|
||||
var downsampledOnly []thanos.BlockInfo
|
||||
@@ -128,7 +128,7 @@ func (tp *thanosProcessor) run(ctx context.Context) error {
|
||||
stats, err := tp.processBlocks(downsampledOnly, aggrType, bar)
|
||||
thanos.CloseBlocks(aggrBlocks)
|
||||
if err != nil {
|
||||
return fmt.Errorf("migration failed for aggr type %s: %s", aggrType, err)
|
||||
return fmt.Errorf("migration failed for aggr type %s: %w", aggrType, err)
|
||||
}
|
||||
phases = append(phases, phaseStats{
|
||||
name: aggrType.String(),
|
||||
@@ -153,7 +153,7 @@ func (tp *thanosProcessor) run(ctx context.Context) error {
|
||||
for vmErr := range tp.im.Errors() {
|
||||
if vmErr.Err != nil {
|
||||
thanosErrorsTotal.Inc()
|
||||
return fmt.Errorf("import process failed: %s", wrapErr(vmErr, tp.isVerbose))
|
||||
return fmt.Errorf("import process failed: %w", wrapErr(vmErr, tp.isVerbose))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ func (tp *thanosProcessor) processBlocks(blocks []thanos.BlockInfo, aggrType tha
|
||||
seriesCount, samplesCount, err := tp.do(bi, aggrType)
|
||||
if err != nil {
|
||||
thanosErrorsTotal.Inc()
|
||||
errCh <- fmt.Errorf("read failed for block %q with aggr %s: %s", bi.Block.Meta().ULID, aggrType, err)
|
||||
errCh <- fmt.Errorf("read failed for block %q with aggr %s: %w", bi.Block.Meta().ULID, aggrType, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -209,12 +209,12 @@ func (tp *thanosProcessor) processBlocks(blocks []thanos.BlockInfo, aggrType tha
|
||||
case thanosErr := <-errCh:
|
||||
close(blockReadersCh)
|
||||
wg.Wait()
|
||||
return processBlocksStats{}, fmt.Errorf("thanos error: %s", thanosErr)
|
||||
return processBlocksStats{}, fmt.Errorf("thanos error: %w", thanosErr)
|
||||
case vmErr := <-tp.im.Errors():
|
||||
close(blockReadersCh)
|
||||
wg.Wait()
|
||||
thanosErrorsTotal.Inc()
|
||||
return processBlocksStats{}, fmt.Errorf("import process failed: %s", wrapErr(vmErr, tp.isVerbose))
|
||||
return processBlocksStats{}, fmt.Errorf("import process failed: %w", wrapErr(vmErr, tp.isVerbose))
|
||||
case blockReadersCh <- bi:
|
||||
}
|
||||
}
|
||||
@@ -223,7 +223,7 @@ func (tp *thanosProcessor) processBlocks(blocks []thanos.BlockInfo, aggrType tha
|
||||
wg.Wait()
|
||||
close(errCh)
|
||||
for err := range errCh {
|
||||
return processBlocksStats{}, fmt.Errorf("import process failed: %s", err)
|
||||
return processBlocksStats{}, fmt.Errorf("import process failed: %w", err)
|
||||
}
|
||||
|
||||
return processBlocksStats{
|
||||
@@ -236,7 +236,7 @@ func (tp *thanosProcessor) processBlocks(blocks []thanos.BlockInfo, aggrType tha
|
||||
func (tp *thanosProcessor) do(bi thanos.BlockInfo, aggrType thanos.AggrType) (uint64, uint64, error) {
|
||||
ss, err := tp.cl.Read(bi)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("failed to read block: %s", err)
|
||||
return 0, 0, fmt.Errorf("failed to read block: %w", err)
|
||||
}
|
||||
defer ss.Close() // Ensure querier is closed even on early returns
|
||||
|
||||
|
||||
@@ -74,9 +74,9 @@ func wrapErr(vmErr *vm.ImportError, verbose bool) error {
|
||||
verboseMsg = "(enable `--verbose` output to get more details)"
|
||||
}
|
||||
if vmErr.Err == nil {
|
||||
return fmt.Errorf("%s\n\tLatest delivered batch for timestamps range %d - %d %s\n%s",
|
||||
return fmt.Errorf("%w\n\tLatest delivered batch for timestamps range %d - %d %s\n%s",
|
||||
vmErr.Err, minTS, maxTS, verboseMsg, errTS)
|
||||
}
|
||||
return fmt.Errorf("%s\n\tImporting batch failed for timestamps range %d - %d %s\n%s",
|
||||
return fmt.Errorf("%w\n\tImporting batch failed for timestamps range %d - %d %s\n%s",
|
||||
vmErr.Err, minTS, maxTS, verboseMsg, errTS)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/auth"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/backoff"
|
||||
@@ -27,6 +28,8 @@ type Config struct {
|
||||
// --httpListenAddr value for single node version
|
||||
// --httpListenAddr value of vmselect component for cluster version
|
||||
Addr string
|
||||
|
||||
AuthCfg *auth.Config
|
||||
// Transport allows specifying custom http.Transport
|
||||
Transport *http.Transport
|
||||
// Concurrency defines number of worker
|
||||
@@ -40,10 +43,6 @@ type Config struct {
|
||||
// BatchSize defines how many samples
|
||||
// importer collects before sending the import request
|
||||
BatchSize int
|
||||
// User name for basic auth
|
||||
User string
|
||||
// Password for basic auth
|
||||
Password string
|
||||
// SignificantFigures defines the number of significant figures to leave
|
||||
// in metric values before importing.
|
||||
// Zero value saves all the significant decimal places
|
||||
@@ -65,11 +64,10 @@ type Config struct {
|
||||
// see https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#how-to-import-time-series-data
|
||||
type Importer struct {
|
||||
addr string
|
||||
authCfg *auth.Config
|
||||
client *http.Client
|
||||
importPath string
|
||||
compress bool
|
||||
user string
|
||||
password string
|
||||
|
||||
close chan struct{}
|
||||
input chan *TimeSeries
|
||||
@@ -148,8 +146,7 @@ func NewImporter(ctx context.Context, cfg Config) (*Importer, error) {
|
||||
client: client,
|
||||
importPath: importPath,
|
||||
compress: cfg.Compress,
|
||||
user: cfg.User,
|
||||
password: cfg.Password,
|
||||
authCfg: cfg.AuthCfg,
|
||||
rl: limiter.NewLimiter(cfg.RateLimit),
|
||||
close: make(chan struct{}),
|
||||
input: make(chan *TimeSeries, cfg.Concurrency*4),
|
||||
@@ -163,7 +160,7 @@ func NewImporter(ctx context.Context, cfg Config) (*Importer, error) {
|
||||
importDuration: metrics.GetOrCreateHistogram(`vmctl_importer_request_duration_seconds`),
|
||||
}
|
||||
if err := im.Ping(); err != nil {
|
||||
return nil, fmt.Errorf("ping to %q failed: %s", addr, err)
|
||||
return nil, fmt.Errorf("ping to %q failed: %w", addr, err)
|
||||
}
|
||||
|
||||
if cfg.BatchSize < 1 {
|
||||
@@ -289,7 +286,7 @@ func (im *Importer) flush(ctx context.Context, b []*TimeSeries) error {
|
||||
retryableFunc := func() error { return im.Import(b) }
|
||||
attempts, err := im.backoff.Retry(ctx, retryableFunc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("import failed with %d retries: %s", attempts, err)
|
||||
return fmt.Errorf("import failed with %d retries: %w", attempts, err)
|
||||
}
|
||||
im.s.Lock()
|
||||
im.s.retries = attempts
|
||||
@@ -302,10 +299,10 @@ func (im *Importer) Ping() error {
|
||||
url := fmt.Sprintf("%s/health", im.addr)
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create request to %q: %s", im.addr, err)
|
||||
return fmt.Errorf("cannot create request to %q: %w", im.addr, err)
|
||||
}
|
||||
if im.user != "" {
|
||||
req.SetBasicAuth(im.user, im.password)
|
||||
if im.authCfg != nil {
|
||||
im.authCfg.SetHeaders(req, true)
|
||||
}
|
||||
resp, err := im.client.Do(req)
|
||||
if err != nil {
|
||||
@@ -332,10 +329,10 @@ func (im *Importer) Import(tsBatch []*TimeSeries) error {
|
||||
req, err := http.NewRequest(http.MethodPost, im.importPath, pr)
|
||||
if err != nil {
|
||||
im.importRequestsErrorsTotal.Inc()
|
||||
return fmt.Errorf("cannot create request to %q: %s", im.addr, err)
|
||||
return fmt.Errorf("cannot create request to %q: %w", im.addr, err)
|
||||
}
|
||||
if im.user != "" {
|
||||
req.SetBasicAuth(im.user, im.password)
|
||||
if im.authCfg != nil {
|
||||
im.authCfg.SetHeaders(req, true)
|
||||
}
|
||||
if im.compress {
|
||||
req.Header.Set("Content-Encoding", "gzip")
|
||||
@@ -352,7 +349,7 @@ func (im *Importer) Import(tsBatch []*TimeSeries) error {
|
||||
zw, err := gzip.NewWriterLevel(w, 1)
|
||||
if err != nil {
|
||||
im.importRequestsErrorsTotal.Inc()
|
||||
return fmt.Errorf("unexpected error when creating gzip writer: %s", err)
|
||||
return fmt.Errorf("unexpected error when creating gzip writer: %w", err)
|
||||
}
|
||||
w = zw
|
||||
}
|
||||
@@ -411,7 +408,7 @@ var ErrBadRequest = errors.New("bad request")
|
||||
func (im *Importer) do(req *http.Request) error {
|
||||
resp, err := im.client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unexpected error when performing request: %s", err)
|
||||
return fmt.Errorf("unexpected error when performing request: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
@@ -419,7 +416,7 @@ func (im *Importer) do(req *http.Request) error {
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read response body for status code %d: %s", resp.StatusCode, err)
|
||||
return fmt.Errorf("failed to read response body for status code %d: %w", resp.StatusCode, err)
|
||||
}
|
||||
if resp.StatusCode == http.StatusBadRequest {
|
||||
return fmt.Errorf("%w: unexpected response code %d: %s", ErrBadRequest, resp.StatusCode, string(body))
|
||||
|
||||
@@ -55,14 +55,14 @@ func (p *vmNativeProcessor) run(ctx context.Context) error {
|
||||
|
||||
start, err := vmctlutil.ParseTime(p.filter.TimeStart)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse %s, provided: %s, error: %w", vmNativeFilterTimeStart, p.filter.TimeStart, err)
|
||||
return fmt.Errorf("failed to parse %s, provided: %s: %w", vmNativeFilterTimeStart, p.filter.TimeStart, err)
|
||||
}
|
||||
|
||||
end := time.Now().In(start.Location())
|
||||
if p.filter.TimeEnd != "" {
|
||||
end, err = vmctlutil.ParseTime(p.filter.TimeEnd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse %s, provided: %s, error: %w", vmNativeFilterTimeEnd, p.filter.TimeEnd, err)
|
||||
return fmt.Errorf("failed to parse %s, provided: %s: %w", vmNativeFilterTimeEnd, p.filter.TimeEnd, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ func (p *vmNativeProcessor) run(ctx context.Context) error {
|
||||
err := p.runBackfilling(ctx, tenantID, ranges)
|
||||
if err != nil {
|
||||
migrationErrorsTotal.Inc()
|
||||
return fmt.Errorf("migration failed: %s", err)
|
||||
return fmt.Errorf("migration failed: %w", err)
|
||||
}
|
||||
|
||||
if p.interCluster {
|
||||
@@ -157,7 +157,7 @@ func (p *vmNativeProcessor) runSingle(ctx context.Context, f native.Filter, srcU
|
||||
}
|
||||
default:
|
||||
}
|
||||
return fmt.Errorf("failed to write into %q: %s", p.dst.Addr, err)
|
||||
return fmt.Errorf("failed to write into %q: %w", p.dst.Addr, err)
|
||||
}
|
||||
|
||||
p.s.Lock()
|
||||
@@ -184,7 +184,7 @@ func (p *vmNativeProcessor) runBackfilling(ctx context.Context, tenantID string,
|
||||
|
||||
importAddr, err := vm.AddExtraLabelsToImportPath(importAddr, p.dst.ExtraLabels)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add labels to import path: %s", err)
|
||||
return fmt.Errorf("failed to add labels to import path: %w", err)
|
||||
}
|
||||
dstURL := fmt.Sprintf("%s/%s", p.dst.Addr, importAddr)
|
||||
|
||||
@@ -222,7 +222,7 @@ func (p *vmNativeProcessor) runBackfilling(ctx context.Context, tenantID string,
|
||||
format = fmt.Sprintf(nativeWithBackoffTpl, barPrefix)
|
||||
metricsMap, err = p.explore(ctx, p.src, tenantID, ranges)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to explore metric names: %s", err)
|
||||
return fmt.Errorf("failed to explore metric names: %w", err)
|
||||
}
|
||||
if len(metricsMap) == 0 {
|
||||
errMsg := "no metrics found"
|
||||
@@ -295,7 +295,7 @@ func (p *vmNativeProcessor) runBackfilling(ctx context.Context, tenantID string,
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("context canceled")
|
||||
case infErr := <-errCh:
|
||||
return fmt.Errorf("export/import error: %s", infErr)
|
||||
return fmt.Errorf("export/import error: %w", infErr)
|
||||
case filterCh <- native.Filter{
|
||||
Match: match,
|
||||
TimeStart: times[0].Format(time.RFC3339),
|
||||
@@ -313,7 +313,7 @@ func (p *vmNativeProcessor) runBackfilling(ctx context.Context, tenantID string,
|
||||
close(errCh)
|
||||
|
||||
for err := range errCh {
|
||||
return fmt.Errorf("import process failed: %s", err)
|
||||
return fmt.Errorf("import process failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -405,7 +405,16 @@ func buildMatchWithFilter(filter string, metricName string) (string, error) {
|
||||
if len(tf.Key) == 0 {
|
||||
continue
|
||||
}
|
||||
a = append(a, tf.String())
|
||||
switch {
|
||||
case tf.IsNegative && tf.IsRegexp:
|
||||
a = append(a, fmt.Sprintf("%s!~%q", tf.Key, tf.Value))
|
||||
case tf.IsNegative:
|
||||
a = append(a, fmt.Sprintf("%s!=%q", tf.Key, tf.Value))
|
||||
case tf.IsRegexp:
|
||||
a = append(a, fmt.Sprintf("%s=~%q", tf.Key, tf.Value))
|
||||
default:
|
||||
a = append(a, fmt.Sprintf("%s=%q", tf.Key, tf.Value))
|
||||
}
|
||||
}
|
||||
a = append(a, nameFilter)
|
||||
filters = append(filters, strings.Join(a, ","))
|
||||
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
nethttputil "net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -29,6 +27,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timerpool"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/vmalertproxy"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -38,7 +37,10 @@ var (
|
||||
resetCacheAuthKey = flagutil.NewPassword("search.resetCacheAuthKey", "Optional authKey for resetting rollup cache via /internal/resetRollupResultCache call. It could be passed via authKey query arg. It overrides -httpAuth.*")
|
||||
logSlowQueryDuration = flag.Duration("search.logSlowQueryDuration", 5*time.Second, "Log queries with execution time exceeding this value. Zero disables slow query logging. "+
|
||||
"See also -search.logQueryMemoryUsage")
|
||||
vmalertProxyURL = flag.String("vmalert.proxyURL", "", "Optional URL for proxying requests to vmalert. For example, if -vmalert.proxyURL=http://vmalert:8880 , then alerting API requests such as /api/v1/rules from Grafana will be proxied to http://vmalert:8880/api/v1/rules")
|
||||
|
||||
vmalertProxyURL = flag.String("vmalert.proxyURL", "", "Optional URL for proxying requests to vmalert. For example, if -vmalert.proxyURL=http://vmalert:8880 , "+
|
||||
"then alerting API requests such as /api/v1/rules from Grafana will be proxied to http://vmalert:8880/api/v1/rules . "+
|
||||
"See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmalert")
|
||||
)
|
||||
|
||||
var slowQueries = metrics.NewCounter(`vm_slow_queries_total`)
|
||||
@@ -55,8 +57,8 @@ func Init(vmselectMaxConcurrentRequests int, vmselectMaxQueueDuration time.Durat
|
||||
concurrencyLimitCh = make(chan struct{}, maxConcurrentRequests)
|
||||
|
||||
initVMUIConfig()
|
||||
initVMAlertProxy()
|
||||
|
||||
vmalertproxy.Init(*vmalertProxyURL)
|
||||
flagutil.RegisterSecretFlag("vmalert.proxyURL")
|
||||
}
|
||||
|
||||
@@ -514,10 +516,11 @@ func handleStaticAndSimpleRequests(w http.ResponseWriter, r *http.Request, path
|
||||
if len(*vmalertProxyURL) == 0 {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, "%s", `{"status":"error","msg":"for accessing vmalert flag '-vmalert.proxyURL' must be configured"}`)
|
||||
fmt.Fprintf(w, "%s", `{"status":"error","msg":"the '-vmalert.proxyURL' command-line must be configured; `+
|
||||
`see https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmalert"}`)
|
||||
return true
|
||||
}
|
||||
proxyVMAlertRequests(w, r, path)
|
||||
vmalertproxy.HandleRequest(w, r, path)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -555,7 +558,7 @@ func handleStaticAndSimpleRequests(w http.ResponseWriter, r *http.Request, path
|
||||
case "/api/v1/rules", "/rules":
|
||||
rulesRequests.Inc()
|
||||
if len(*vmalertProxyURL) > 0 {
|
||||
proxyVMAlertRequests(w, r, path)
|
||||
vmalertproxy.HandleRequest(w, r, path)
|
||||
return true
|
||||
}
|
||||
// Return dumb placeholder for https://prometheus.io/docs/prometheus/latest/querying/api/#rules
|
||||
@@ -565,7 +568,7 @@ func handleStaticAndSimpleRequests(w http.ResponseWriter, r *http.Request, path
|
||||
case "/api/v1/alerts", "/alerts":
|
||||
alertsRequests.Inc()
|
||||
if len(*vmalertProxyURL) > 0 {
|
||||
proxyVMAlertRequests(w, r, path)
|
||||
vmalertproxy.HandleRequest(w, r, path)
|
||||
return true
|
||||
}
|
||||
// Return dumb placeholder for https://prometheus.io/docs/prometheus/latest/querying/api/#alerts
|
||||
@@ -575,7 +578,7 @@ func handleStaticAndSimpleRequests(w http.ResponseWriter, r *http.Request, path
|
||||
case "/api/v1/notifiers", "/notifiers":
|
||||
notifiersRequests.Inc()
|
||||
if len(*vmalertProxyURL) > 0 {
|
||||
proxyVMAlertRequests(w, r, path)
|
||||
vmalertproxy.HandleRequest(w, r, path)
|
||||
return true
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -722,48 +725,7 @@ var (
|
||||
metricNamesStatsResetErrors = metrics.NewCounter(`vm_http_request_errors_total{path="/api/v1/admin/status/metric_names_stats/reset"}`)
|
||||
)
|
||||
|
||||
func proxyVMAlertRequests(w http.ResponseWriter, r *http.Request, path string) {
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err == nil || err == http.ErrAbortHandler {
|
||||
// Suppress http.ErrAbortHandler panic.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1353
|
||||
return
|
||||
}
|
||||
// Forward other panics to the caller.
|
||||
panic(err)
|
||||
}()
|
||||
req := r.Clone(r.Context())
|
||||
req.URL.Path = strings.TrimPrefix(path, "prometheus")
|
||||
req.Host = vmalertProxyHost
|
||||
|
||||
if strings.HasPrefix(r.Header.Get(`User-Agent`), `Grafana`) {
|
||||
// Grafana currently supports only Prometheus-style alerts. If other alert types
|
||||
// (e.g. logs or traces) are returned, it may fail with "Error loading alerts".
|
||||
//
|
||||
// Grafana queries the vmalert API directly, bypassing the VictoriaMetrics datasource,
|
||||
// so query params (such as datasource_type) cannot be enforced on the Grafana side.
|
||||
//
|
||||
// To ensure compatibility, we detect Grafana requests via the User-Agent and enforce
|
||||
// `datasource_type=prometheus`.
|
||||
//
|
||||
// See:
|
||||
// - https://github.com/VictoriaMetrics/victoriametrics-datasource/issues/329#issuecomment-3847585443
|
||||
// - https://github.com/VictoriaMetrics/victoriametrics-datasource/issues/59
|
||||
q := req.URL.Query()
|
||||
q.Set("datasource_type", "prometheus")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
req.RequestURI = ""
|
||||
}
|
||||
|
||||
vmalertProxy.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
var (
|
||||
vmalertProxyHost string
|
||||
vmalertProxy *nethttputil.ReverseProxy
|
||||
vmuiConfig string
|
||||
)
|
||||
var vmuiConfig string
|
||||
|
||||
func initVMUIConfig() {
|
||||
var cfg struct {
|
||||
@@ -795,16 +757,3 @@ func initVMUIConfig() {
|
||||
}
|
||||
vmuiConfig = string(data)
|
||||
}
|
||||
|
||||
// initVMAlertProxy must be called after flag.Parse(), since it uses command-line flags.
|
||||
func initVMAlertProxy() {
|
||||
if len(*vmalertProxyURL) == 0 {
|
||||
return
|
||||
}
|
||||
proxyURL, err := url.Parse(*vmalertProxyURL)
|
||||
if err != nil {
|
||||
logger.Fatalf("cannot parse -vmalert.proxyURL=%q: %s", *vmalertProxyURL, err)
|
||||
}
|
||||
vmalertProxyHost = proxyURL.Host
|
||||
vmalertProxy = nethttputil.NewSingleHostReverseProxy(proxyURL)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ See https://prometheus.io/docs/prometheus/latest/querying/api/#querying-metric-m
|
||||
currentItem := 0
|
||||
%}
|
||||
{% for _, row := range result %}
|
||||
"{%s string(row.MetricFamilyName) %}": [
|
||||
{%q= string(row.MetricFamilyName) %}: [
|
||||
{
|
||||
"type": {%q= row.Type.String() %},
|
||||
{% if len(row.Unit) > 0 -%}
|
||||
|
||||
@@ -35,12 +35,10 @@ func StreamMetadataResponse(qw422016 *qt422016.Writer, result []*metricsmetadata
|
||||
|
||||
//line app/vmselect/prometheus/metadata_response.qtpl:17
|
||||
for _, row := range result {
|
||||
//line app/vmselect/prometheus/metadata_response.qtpl:17
|
||||
qw422016.N().S(`"`)
|
||||
//line app/vmselect/prometheus/metadata_response.qtpl:18
|
||||
qw422016.E().S(string(row.MetricFamilyName))
|
||||
qw422016.N().Q(string(row.MetricFamilyName))
|
||||
//line app/vmselect/prometheus/metadata_response.qtpl:18
|
||||
qw422016.N().S(`": [{"type":`)
|
||||
qw422016.N().S(`: [{"type":`)
|
||||
//line app/vmselect/prometheus/metadata_response.qtpl:20
|
||||
qw422016.N().Q(row.Type.String())
|
||||
//line app/vmselect/prometheus/metadata_response.qtpl:20
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/netutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||
@@ -525,6 +526,7 @@ func DeleteHandler(startTime time.Time, r *http.Request) error {
|
||||
if deletedCount > 0 {
|
||||
promql.ResetRollupResultCache()
|
||||
}
|
||||
logger.Infof("/api/v1/admin/tsdb/delete_series has been called for %q. Deleted %d series.", sq.FiltersString(), deletedCount)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
197
app/vmselect/vmui/assets/index-CusQvJzs.js
Normal file
197
app/vmselect/vmui/assets/index-CusQvJzs.js
Normal file
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(e&&(t=e(e=0)),t),s=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),c=(e,n)=>{let r={};for(var i in e)t(r,i,{get:e[i],enumerable:!0});return n||t(r,Symbol.toStringTag,{value:`Module`}),r},l=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},u=(n,r,a)=>(a=n==null?{}:e(i(n)),l(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n)),d=e=>a.call(e,`module.exports`)?e[`module.exports`]:l(t({},`__esModule`,{value:!0}),e);export{u as a,d as i,o as n,c as r,s as t};
|
||||
1
app/vmselect/vmui/assets/rolldown-runtime-Cyuzqnbw.js
Normal file
1
app/vmselect/vmui/assets/rolldown-runtime-Cyuzqnbw.js
Normal file
@@ -0,0 +1 @@
|
||||
var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(e&&(t=e(e=0)),t),s=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),c=(e,n)=>{let r={};for(var i in e)t(r,i,{get:e[i],enumerable:!0});return n||t(r,Symbol.toStringTag,{value:`Module`}),r},l=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},u=(n,r,a)=>(a=n==null?{}:e(i(n)),l(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n)),d=e=>a.call(e,`module.exports`)?e[`module.exports`]:l(t({},`__esModule`,{value:!0}),e);export{u as a,d as i,o as n,c as r,s as t};
|
||||
78
app/vmselect/vmui/assets/vendor-B83wxFqK.js
Normal file
78
app/vmselect/vmui/assets/vendor-B83wxFqK.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -37,9 +37,9 @@
|
||||
<meta property="og:title" content="UI for VictoriaMetrics">
|
||||
<meta property="og:url" content="https://victoriametrics.com/">
|
||||
<meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data">
|
||||
<script type="module" crossorigin src="./assets/index-CoGukb-x.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/rolldown-runtime-COnpUsM8.js">
|
||||
<link rel="modulepreload" crossorigin href="./assets/vendor-C8Kwp93_.js">
|
||||
<script type="module" crossorigin src="./assets/index-CusQvJzs.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/rolldown-runtime-Cyuzqnbw.js">
|
||||
<link rel="modulepreload" crossorigin href="./assets/vendor-B83wxFqK.js">
|
||||
<link rel="stylesheet" crossorigin href="./assets/vendor-CnsZ1jie.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-BBUnmLOr.css">
|
||||
</head>
|
||||
|
||||
@@ -10,8 +10,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||
@@ -23,6 +21,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/stringsutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/vminsertapi"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/vmselectapi"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -153,7 +152,7 @@ func Init(vmselectMaxConcurrentRequests int, resetCacheIfNeeded func(mrs []stora
|
||||
LogNewSeries: *logNewSeries,
|
||||
}
|
||||
strg := storage.MustOpenStorage(*storageDataPath, opts)
|
||||
vmStorage = newVMStorageSingleNode(strg, vmselectMaxConcurrentRequests, resetCacheIfNeeded)
|
||||
vmStorage = newVMStorage(strg, vmselectMaxConcurrentRequests, resetCacheIfNeeded)
|
||||
|
||||
var m storage.Metrics
|
||||
strg.UpdateMetrics(&m)
|
||||
@@ -175,15 +174,15 @@ func Init(vmselectMaxConcurrentRequests int, resetCacheIfNeeded func(mrs []stora
|
||||
GetSearch = vmStorage.GetSearch
|
||||
PutSearch = vmStorage.PutSearch
|
||||
RequestHandler = vmStorage.requestHandler
|
||||
DebugFlush = vmStorage.vms.s.DebugFlush
|
||||
DebugFlush = vmStorage.s.DebugFlush
|
||||
}
|
||||
|
||||
var storageMetrics *metrics.Set
|
||||
|
||||
var (
|
||||
// vmStorageSingleNode is an instance of vmstorage used by vminsert and
|
||||
// vmStorage is an instance of vmstorage used by vminsert and
|
||||
// vmselect for writing and reading data.
|
||||
vmStorage *VMStorageSingleNode
|
||||
vmStorage *VMStorage
|
||||
VMInsertAPI vminsertapi.API
|
||||
VMSelectAPI vmselectapi.API
|
||||
GetSearch func(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline uint64) (*storage.Search, int, error)
|
||||
@@ -209,15 +208,12 @@ func Stop() {
|
||||
logger.Infof("the vmstorage has been stopped")
|
||||
}
|
||||
|
||||
func (vmssn *VMStorageSingleNode) requestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
vmssn.wg.Add(1)
|
||||
defer vmssn.wg.Done()
|
||||
return vmssn.vms.requestHandler(w, r)
|
||||
}
|
||||
|
||||
// requestHandler is a storage request handler.
|
||||
// TODO(@rtm0): Move to a separate file, request_handler.go
|
||||
func (vms *VMStorage) requestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
vms.wg.Add(1)
|
||||
defer vms.wg.Done()
|
||||
|
||||
path := r.URL.Path
|
||||
if path == "/internal/force_merge" {
|
||||
if !httpserver.CheckAuthFlag(w, r, forceMergeAuthKey) {
|
||||
@@ -257,7 +253,7 @@ func (vms *VMStorage) requestHandler(w http.ResponseWriter, r *http.Request) boo
|
||||
dealine, err = strconv.Atoi(deadlineStr)
|
||||
if err != nil {
|
||||
logger.Errorf("cannot parse `seconds` arg %q: %s", deadlineStr, err)
|
||||
jsonResponseError(w, fmt.Errorf("cannot parse `seconds` arg %q: %s", deadlineStr, err))
|
||||
jsonResponseError(w, fmt.Errorf("cannot parse `seconds` arg %q: %w", deadlineStr, err))
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -361,15 +357,11 @@ var (
|
||||
snapshotsDeleteAllErrorsTotal = metrics.NewCounter(`vm_http_request_errors_total{path="/snapshot/delete_all"}`)
|
||||
)
|
||||
|
||||
// TODO(@rtm0): Move to metrics.go.
|
||||
func (vmssn *VMStorageSingleNode) writeStorageMetrics(w io.Writer) {
|
||||
vmssn.wg.Add(1)
|
||||
defer vmssn.wg.Done()
|
||||
vmssn.vms.writeStorageMetrics(w)
|
||||
}
|
||||
|
||||
// TODO(@rtm0): Move to metrics.go.
|
||||
func (vms *VMStorage) writeStorageMetrics(w io.Writer) {
|
||||
vms.wg.Add(1)
|
||||
defer vms.wg.Done()
|
||||
|
||||
strg := vms.s
|
||||
var m storage.Metrics
|
||||
strg.UpdateMetrics(&m)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package vmstorage
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"sync"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage/metricnamestats"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage/metricsmetadata"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/syncwg"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/vmselectapi"
|
||||
)
|
||||
@@ -34,7 +36,7 @@ var (
|
||||
// newVMStorage creates a new instance of of VMStorage.
|
||||
//
|
||||
// The created VMStorage instance takes ownership of s.
|
||||
func newVMStorage(s *storage.Storage, vmselectMaxConcurrentRequests int) *VMStorage {
|
||||
func newVMStorage(s *storage.Storage, vmselectMaxConcurrentRequests int, resetCacheIfNeeded func(mrs []storage.MetricRow)) *VMStorage {
|
||||
if err := encoding.CheckPrecisionBits(uint8(*precisionBits)); err != nil {
|
||||
logger.Fatalf("invalid -precisionBits=%d: %s", *precisionBits, err)
|
||||
}
|
||||
@@ -49,6 +51,8 @@ func newVMStorage(s *storage.Storage, vmselectMaxConcurrentRequests int) *VMStor
|
||||
maxUniqueTimeseries: *maxUniqueTimeseries,
|
||||
maxUniqueTimeSeriesCalculated: maxUniqueTimeseriesCalculated,
|
||||
staleSnapshotsRemoverCh: make(chan struct{}),
|
||||
wg: syncwg.WaitGroup{},
|
||||
resetCacheIfNeeded: resetCacheIfNeeded,
|
||||
}
|
||||
vms.initStaleSnapshotsRemover()
|
||||
return vms
|
||||
@@ -78,6 +82,17 @@ type VMStorage struct {
|
||||
maxUniqueTimeSeriesCalculated int
|
||||
staleSnapshotsRemoverCh chan struct{}
|
||||
staleSnapshotsRemoverWG sync.WaitGroup
|
||||
|
||||
// wg is used to wrap every storage call into wg.Add(1) ... wg.Done()
|
||||
// for proper graceful shutdown when Stop is called.
|
||||
//
|
||||
// Use syncwg instead of sync, since Add is called from concurrent
|
||||
// goroutines.
|
||||
wg syncwg.WaitGroup
|
||||
|
||||
// resetCacheIfNeeded is a callback for automatic resetting of response
|
||||
// cache if needed.
|
||||
resetCacheIfNeeded func(mrs []storage.MetricRow)
|
||||
}
|
||||
|
||||
func (vms *VMStorage) initStaleSnapshotsRemover() {
|
||||
@@ -103,6 +118,7 @@ func (vms *VMStorage) initStaleSnapshotsRemover() {
|
||||
func (vms *VMStorage) Stop() {
|
||||
close(vms.staleSnapshotsRemoverCh)
|
||||
vms.staleSnapshotsRemoverWG.Wait()
|
||||
vms.wg.WaitAndBlock()
|
||||
vms.s.MustClose()
|
||||
}
|
||||
|
||||
@@ -111,6 +127,14 @@ func (vms *VMStorage) Stop() {
|
||||
// The caller should limit the number of concurrent calls to WriteRows() in
|
||||
// order to limit memory usage.
|
||||
func (vms *VMStorage) WriteRows(rows []storage.MetricRow) error {
|
||||
vms.wg.Add(1)
|
||||
defer vms.wg.Done()
|
||||
|
||||
if vms.s.IsReadOnly() {
|
||||
return errReadOnly
|
||||
}
|
||||
vms.resetCacheIfNeeded(rows)
|
||||
|
||||
vms.s.AddRows(rows, uint8(*precisionBits))
|
||||
return nil
|
||||
}
|
||||
@@ -120,26 +144,41 @@ func (vms *VMStorage) WriteRows(rows []storage.MetricRow) error {
|
||||
// The caller should limit the number of concurrent calls to WriteMetadata() in
|
||||
// order to limit memory usage.
|
||||
func (vms *VMStorage) WriteMetadata(rows []metricsmetadata.Row) error {
|
||||
vms.wg.Add(1)
|
||||
defer vms.wg.Done()
|
||||
|
||||
if vms.s.IsReadOnly() {
|
||||
return errReadOnly
|
||||
}
|
||||
vms.s.AddMetadataRows(rows)
|
||||
return nil
|
||||
}
|
||||
|
||||
var errReadOnly = errors.New("the storage is in read-only mode; check -storage.minFreeDiskSpaceBytes command-line flag value")
|
||||
|
||||
// IsReadOnly returns true is the storage is in read-only mode.
|
||||
func (vms *VMStorage) IsReadOnly() bool {
|
||||
vms.wg.Add(1)
|
||||
defer vms.wg.Done()
|
||||
return vms.s.IsReadOnly()
|
||||
}
|
||||
|
||||
func (vms *VMStorage) InitSearch(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline uint64) (vmselectapi.BlockIterator, error) {
|
||||
vms.wg.Add(1)
|
||||
|
||||
tr := sq.GetTimeRange()
|
||||
maxMetrics := vms.getMaxMetrics(sq.MaxMetrics)
|
||||
tfss, err := vms.setupTfss(qt, sq, tr, maxMetrics, deadline)
|
||||
if err != nil {
|
||||
vms.wg.Done()
|
||||
return nil, err
|
||||
}
|
||||
if len(tfss) == 0 {
|
||||
vms.wg.Done()
|
||||
return nil, fmt.Errorf("missing tag filters")
|
||||
}
|
||||
bi := getBlockIterator()
|
||||
bi.wgDone = vms.wg.Done
|
||||
bi.sr.Init(qt, vms.s, tfss, tr, maxMetrics, deadline)
|
||||
if err := bi.sr.Error(); err != nil {
|
||||
bi.MustClose()
|
||||
@@ -161,8 +200,9 @@ func (vms *VMStorage) getMaxMetrics(searchQueryLimit int) int {
|
||||
|
||||
// blockIterator implements vmselectapi.BlockIterator
|
||||
type blockIterator struct {
|
||||
sr storage.Search
|
||||
mb storage.MetricBlock
|
||||
sr storage.Search
|
||||
mb storage.MetricBlock
|
||||
wgDone func()
|
||||
}
|
||||
|
||||
var blockIteratorsPool sync.Pool
|
||||
@@ -171,6 +211,8 @@ func (bi *blockIterator) MustClose() {
|
||||
bi.sr.MustClose()
|
||||
bi.mb.MetricName = nil
|
||||
bi.mb.Block.Reset()
|
||||
bi.wgDone()
|
||||
bi.wgDone = nil
|
||||
blockIteratorsPool.Put(bi)
|
||||
}
|
||||
|
||||
@@ -197,8 +239,63 @@ func (bi *blockIterator) Error() error {
|
||||
return bi.sr.Error()
|
||||
}
|
||||
|
||||
// GetSearch sets up an instance of storage search and returns it to the caller
|
||||
// along with the max series count that the search can return.
|
||||
//
|
||||
// This method is not part of the vmselectapi.API and must only be used by
|
||||
// vmsingle HTTP handlers.
|
||||
//
|
||||
// Callers of this method must call PutSearch() once the search instance is not
|
||||
// needed anymore.
|
||||
func (vms *VMStorage) GetSearch(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline uint64) (*storage.Search, int, error) {
|
||||
vms.wg.Add(1)
|
||||
|
||||
tr := sq.GetTimeRange()
|
||||
maxMetrics := vms.getMaxMetrics(sq.MaxMetrics)
|
||||
tfss, err := vms.setupTfss(qt, sq, tr, maxMetrics, deadline)
|
||||
if err != nil {
|
||||
vms.wg.Done()
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
sr := getSearch()
|
||||
maxSeriesCount := sr.Init(qt, vms.s, tfss, tr, sq.MaxMetrics, deadline)
|
||||
return sr, maxSeriesCount, nil
|
||||
}
|
||||
|
||||
// PutSearch resets the search once it is not needed anymore and puts it aside
|
||||
// for future reuse.
|
||||
//
|
||||
// This method is not part of the vmselectapi.API and must only be used by
|
||||
// vmsingle HTTP handlers.
|
||||
//
|
||||
// The method must only be used on search instances that have been created with
|
||||
// GetSearch().
|
||||
func (vms *VMStorage) PutSearch(sr *storage.Search) {
|
||||
putSearch(sr)
|
||||
vms.wg.Done()
|
||||
}
|
||||
|
||||
func getSearch() *storage.Search {
|
||||
v := ssPool.Get()
|
||||
if v == nil {
|
||||
return &storage.Search{}
|
||||
}
|
||||
return v.(*storage.Search)
|
||||
}
|
||||
|
||||
func putSearch(sr *storage.Search) {
|
||||
sr.MustClose()
|
||||
ssPool.Put(sr)
|
||||
}
|
||||
|
||||
var ssPool sync.Pool
|
||||
|
||||
// SearchMetricNames returns metric names for the given tfss on the given tr.
|
||||
func (vms *VMStorage) SearchMetricNames(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline uint64) ([]string, error) {
|
||||
vms.wg.Add(1)
|
||||
defer vms.wg.Done()
|
||||
|
||||
tr := sq.GetTimeRange()
|
||||
maxMetrics := sq.MaxMetrics
|
||||
if maxMetrics <= 0 {
|
||||
@@ -219,6 +316,9 @@ func (vms *VMStorage) SearchMetricNames(qt *querytracer.Tracer, sq *storage.Sear
|
||||
// SearchLabelValues searches for label values for the given labelName, tfss and
|
||||
// tr.
|
||||
func (vms *VMStorage) LabelValues(qt *querytracer.Tracer, sq *storage.SearchQuery, labelName string, maxLabelValues int, deadline uint64) ([]string, error) {
|
||||
vms.wg.Add(1)
|
||||
defer vms.wg.Done()
|
||||
|
||||
tr := sq.GetTimeRange()
|
||||
if maxLabelValues <= 0 || maxLabelValues > *maxTagValues {
|
||||
maxLabelValues = *maxTagValues
|
||||
@@ -244,6 +344,9 @@ func (vms *VMStorage) LabelValues(qt *querytracer.Tracer, sq *storage.SearchQuer
|
||||
// similar APIs.
|
||||
func (vms *VMStorage) TagValueSuffixes(qt *querytracer.Tracer, _, _ uint32, tr storage.TimeRange, tagKey, tagValuePrefix string, delimiter byte,
|
||||
maxSuffixes int, deadline uint64) ([]string, error) {
|
||||
vms.wg.Add(1)
|
||||
defer vms.wg.Done()
|
||||
|
||||
if maxSuffixes <= 0 || maxSuffixes > *maxTagValueSuffixesPerSearch {
|
||||
maxSuffixes = *maxTagValueSuffixesPerSearch
|
||||
}
|
||||
@@ -260,6 +363,9 @@ func (vms *VMStorage) TagValueSuffixes(qt *querytracer.Tracer, _, _ uint32, tr s
|
||||
|
||||
// SearchLabelNames searches for tag keys matching the given tfss on tr.
|
||||
func (vms *VMStorage) LabelNames(qt *querytracer.Tracer, sq *storage.SearchQuery, maxLabelNames int, deadline uint64) ([]string, error) {
|
||||
vms.wg.Add(1)
|
||||
defer vms.wg.Done()
|
||||
|
||||
tr := sq.GetTimeRange()
|
||||
if maxLabelNames <= 0 || maxLabelNames > *maxTagKeys {
|
||||
maxLabelNames = *maxTagKeys
|
||||
@@ -278,6 +384,8 @@ func (vms *VMStorage) LabelNames(qt *querytracer.Tracer, sq *storage.SearchQuery
|
||||
}
|
||||
|
||||
func (vms *VMStorage) SeriesCount(_ *querytracer.Tracer, _, _ uint32, deadline uint64) (uint64, error) {
|
||||
vms.wg.Add(1)
|
||||
defer vms.wg.Done()
|
||||
return vms.s.GetSeriesCount(deadline)
|
||||
}
|
||||
|
||||
@@ -287,6 +395,9 @@ func (vms *VMStorage) Tenants(_ *querytracer.Tracer, _ storage.TimeRange, _ uint
|
||||
|
||||
// GetTSDBStatus returns TSDB status for given filters on the given date.
|
||||
func (vms *VMStorage) TSDBStatus(qt *querytracer.Tracer, sq *storage.SearchQuery, focusLabel string, topN int, deadline uint64) (*storage.TSDBStatus, error) {
|
||||
vms.wg.Add(1)
|
||||
defer vms.wg.Done()
|
||||
|
||||
tr := sq.GetTimeRange()
|
||||
maxMetrics := sq.MaxMetrics
|
||||
if maxMetrics <= 0 {
|
||||
@@ -306,6 +417,9 @@ func (vms *VMStorage) TSDBStatus(qt *querytracer.Tracer, sq *storage.SearchQuery
|
||||
//
|
||||
// Returns the number of deleted series.
|
||||
func (vms *VMStorage) DeleteSeries(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline uint64) (int, error) {
|
||||
vms.wg.Add(1)
|
||||
defer vms.wg.Done()
|
||||
|
||||
tr := sq.GetTimeRange()
|
||||
maxMetrics := sq.MaxMetrics
|
||||
if maxMetrics <= 0 {
|
||||
@@ -324,17 +438,26 @@ func (vms *VMStorage) DeleteSeries(qt *querytracer.Tracer, sq *storage.SearchQue
|
||||
}
|
||||
|
||||
func (vms *VMStorage) RegisterMetricNames(qt *querytracer.Tracer, mrs []storage.MetricRow, _ uint64) error {
|
||||
vms.wg.Add(1)
|
||||
defer vms.wg.Done()
|
||||
|
||||
vms.s.RegisterMetricNames(qt, mrs)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMetricNamesUsageStats returns metric name usage stats.
|
||||
func (vms *VMStorage) GetMetricNamesUsageStats(qt *querytracer.Tracer, _ *storage.TenantToken, limit, le int, matchPattern string, _ uint64) (metricnamestats.StatsResult, error) {
|
||||
vms.wg.Add(1)
|
||||
defer vms.wg.Done()
|
||||
|
||||
return vms.s.GetMetricNamesStats(qt, limit, le, matchPattern), nil
|
||||
}
|
||||
|
||||
// ResetMetricNamesStats resets state for metric names usage tracker
|
||||
func (vms *VMStorage) ResetMetricNamesUsageStats(qt *querytracer.Tracer, _ uint64) error {
|
||||
vms.wg.Add(1)
|
||||
defer vms.wg.Done()
|
||||
|
||||
vms.s.ResetMetricNamesStats(qt)
|
||||
return nil
|
||||
}
|
||||
@@ -371,6 +494,8 @@ func (vms *VMStorage) setupTfss(qt *querytracer.Tracer, sq *storage.SearchQuery,
|
||||
}
|
||||
|
||||
func (vms *VMStorage) GetMetadataRecords(qt *querytracer.Tracer, _ *storage.TenantToken, limit int, metricName string, _ uint64) ([]*metricsmetadata.Row, error) {
|
||||
vms.wg.Add(1)
|
||||
defer vms.wg.Done()
|
||||
return vms.s.GetMetadataRows(qt, limit, metricName), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
package vmstorage
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage/metricnamestats"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage/metricsmetadata"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/syncwg"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/vmselectapi"
|
||||
)
|
||||
|
||||
// newVMStorageSingleNode creates a new instance of of VMStorage for vmsingle.
|
||||
func newVMStorageSingleNode(s *storage.Storage, maxConcurrentRequests int, resetCacheIfNeeded func(mrs []storage.MetricRow)) *VMStorageSingleNode {
|
||||
vms := newVMStorage(s, maxConcurrentRequests)
|
||||
return &VMStorageSingleNode{
|
||||
vms: vms,
|
||||
wg: syncwg.WaitGroup{},
|
||||
resetCacheIfNeeded: resetCacheIfNeeded,
|
||||
}
|
||||
}
|
||||
|
||||
type VMStorageSingleNode struct {
|
||||
vms *VMStorage
|
||||
|
||||
// wg is used to wrap every storage call into wg.Add(1) ... wg.Done()
|
||||
// for proper graceful shutdown when Stop is called.
|
||||
//
|
||||
// Use syncwg instead of sync, since Add is called from concurrent
|
||||
// goroutines.
|
||||
wg syncwg.WaitGroup
|
||||
|
||||
// resetCacheIfNeeded is a callback for automatic resetting of response
|
||||
// cache if needed.
|
||||
resetCacheIfNeeded func(mrs []storage.MetricRow)
|
||||
}
|
||||
|
||||
func (vmssn *VMStorageSingleNode) Stop() {
|
||||
vmssn.wg.WaitAndBlock()
|
||||
vmssn.vms.Stop()
|
||||
}
|
||||
|
||||
// WriteRows writes metric rows to the storage.
|
||||
//
|
||||
// Returns an error if the storage is in read-only mode.
|
||||
//
|
||||
// The caller should limit the number of concurrent calls to WriteRows() in
|
||||
// order to limit memory usage.
|
||||
func (vmssn *VMStorageSingleNode) WriteRows(rows []storage.MetricRow) error {
|
||||
vmssn.wg.Add(1)
|
||||
defer vmssn.wg.Done()
|
||||
|
||||
if vmssn.vms.IsReadOnly() {
|
||||
return errReadOnly
|
||||
}
|
||||
vmssn.resetCacheIfNeeded(rows)
|
||||
return vmssn.vms.WriteRows(rows)
|
||||
}
|
||||
|
||||
// WriteMetadata writes metrics metadata to storage.
|
||||
//
|
||||
// Returns an error if the storage is in read-only mode.
|
||||
//
|
||||
// The caller should limit the number of concurrent calls to WriteMetadata() in
|
||||
// order to limit memory usage.
|
||||
func (vmssn *VMStorageSingleNode) WriteMetadata(rows []metricsmetadata.Row) error {
|
||||
vmssn.wg.Add(1)
|
||||
defer vmssn.wg.Done()
|
||||
|
||||
if vmssn.vms.IsReadOnly() {
|
||||
return errReadOnly
|
||||
}
|
||||
return vmssn.vms.WriteMetadata(rows)
|
||||
}
|
||||
|
||||
var errReadOnly = errors.New("the storage is in read-only mode; check -storage.minFreeDiskSpaceBytes command-line flag value")
|
||||
|
||||
func (vmssn *VMStorageSingleNode) IsReadOnly() bool {
|
||||
vmssn.wg.Add(1)
|
||||
defer vmssn.wg.Done()
|
||||
return vmssn.vms.IsReadOnly()
|
||||
}
|
||||
|
||||
func (vmssn *VMStorageSingleNode) InitSearch(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline uint64) (vmselectapi.BlockIterator, error) {
|
||||
return nil, fmt.Errorf("not implemented in vmsingle")
|
||||
}
|
||||
|
||||
// GetSearch sets up an instance of storage search and returns it to the caller
|
||||
// along with the max series count that the search can return.
|
||||
//
|
||||
// This method is not part of the vmselectapi.API and must only be used by
|
||||
// vmsingle HTTP handlers.
|
||||
//
|
||||
// Callers of this method must call PutSearch() once the search instance is not
|
||||
// needed anymore.
|
||||
func (vmssn *VMStorageSingleNode) GetSearch(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline uint64) (*storage.Search, int, error) {
|
||||
vmssn.wg.Add(1)
|
||||
|
||||
tr := sq.GetTimeRange()
|
||||
maxMetrics := vmssn.vms.getMaxMetrics(sq.MaxMetrics)
|
||||
tfss, err := vmssn.vms.setupTfss(qt, sq, tr, maxMetrics, deadline)
|
||||
if err != nil {
|
||||
vmssn.wg.Done()
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
sr := getSearch()
|
||||
maxSeriesCount := sr.Init(qt, vmssn.vms.s, tfss, tr, sq.MaxMetrics, deadline)
|
||||
return sr, maxSeriesCount, nil
|
||||
}
|
||||
|
||||
// PutSearch resets the search once it is not needed anymore and puts it aside
|
||||
// for future reuse.
|
||||
//
|
||||
// This method is not part of the vmselectapi.API and must only be used by
|
||||
// vmsingle HTTP handlers.
|
||||
//
|
||||
// The method must only be used on search instances that have been created with
|
||||
// GetSearch().
|
||||
func (vmssn *VMStorageSingleNode) PutSearch(sr *storage.Search) {
|
||||
putSearch(sr)
|
||||
vmssn.wg.Done()
|
||||
}
|
||||
|
||||
func getSearch() *storage.Search {
|
||||
v := ssPool.Get()
|
||||
if v == nil {
|
||||
return &storage.Search{}
|
||||
}
|
||||
return v.(*storage.Search)
|
||||
}
|
||||
|
||||
func putSearch(sr *storage.Search) {
|
||||
sr.MustClose()
|
||||
ssPool.Put(sr)
|
||||
}
|
||||
|
||||
var ssPool sync.Pool
|
||||
|
||||
func (vmssn *VMStorageSingleNode) SearchMetricNames(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline uint64) ([]string, error) {
|
||||
vmssn.wg.Add(1)
|
||||
defer vmssn.wg.Done()
|
||||
return vmssn.vms.SearchMetricNames(qt, sq, deadline)
|
||||
}
|
||||
|
||||
func (vmssn *VMStorageSingleNode) LabelValues(qt *querytracer.Tracer, sq *storage.SearchQuery, labelName string, maxLabelValues int, deadline uint64) ([]string, error) {
|
||||
vmssn.wg.Add(1)
|
||||
defer vmssn.wg.Done()
|
||||
return vmssn.vms.LabelValues(qt, sq, labelName, maxLabelValues, deadline)
|
||||
}
|
||||
|
||||
func (vmssn *VMStorageSingleNode) TagValueSuffixes(qt *querytracer.Tracer, accountID, projectID uint32, tr storage.TimeRange, tagKey, tagValuePrefix string, delimiter byte, maxSuffixes int, deadline uint64) ([]string, error) {
|
||||
vmssn.wg.Add(1)
|
||||
defer vmssn.wg.Done()
|
||||
return vmssn.vms.TagValueSuffixes(qt, accountID, projectID, tr, tagKey, tagValuePrefix, delimiter, maxSuffixes, deadline)
|
||||
}
|
||||
|
||||
func (vmssn *VMStorageSingleNode) LabelNames(qt *querytracer.Tracer, sq *storage.SearchQuery, maxLabelNames int, deadline uint64) ([]string, error) {
|
||||
vmssn.wg.Add(1)
|
||||
defer vmssn.wg.Done()
|
||||
return vmssn.vms.LabelNames(qt, sq, maxLabelNames, deadline)
|
||||
}
|
||||
|
||||
func (vmssn *VMStorageSingleNode) SeriesCount(qt *querytracer.Tracer, accountID, projectID uint32, deadline uint64) (uint64, error) {
|
||||
vmssn.wg.Add(1)
|
||||
defer vmssn.wg.Done()
|
||||
return vmssn.vms.SeriesCount(qt, accountID, projectID, deadline)
|
||||
}
|
||||
|
||||
func (vmssn *VMStorageSingleNode) Tenants(qt *querytracer.Tracer, tr storage.TimeRange, deadline uint64) ([]string, error) {
|
||||
vmssn.wg.Add(1)
|
||||
defer vmssn.wg.Done()
|
||||
return vmssn.vms.Tenants(qt, tr, deadline)
|
||||
}
|
||||
|
||||
func (vmssn *VMStorageSingleNode) TSDBStatus(qt *querytracer.Tracer, sq *storage.SearchQuery, focusLabel string, topN int, deadline uint64) (*storage.TSDBStatus, error) {
|
||||
vmssn.wg.Add(1)
|
||||
defer vmssn.wg.Done()
|
||||
return vmssn.vms.TSDBStatus(qt, sq, focusLabel, topN, deadline)
|
||||
}
|
||||
|
||||
func (vmssn *VMStorageSingleNode) DeleteSeries(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline uint64) (int, error) {
|
||||
vmssn.wg.Add(1)
|
||||
defer vmssn.wg.Done()
|
||||
return vmssn.vms.DeleteSeries(qt, sq, deadline)
|
||||
}
|
||||
|
||||
func (vmssn *VMStorageSingleNode) RegisterMetricNames(qt *querytracer.Tracer, mrs []storage.MetricRow, deadline uint64) error {
|
||||
vmssn.wg.Add(1)
|
||||
defer vmssn.wg.Done()
|
||||
return vmssn.vms.RegisterMetricNames(qt, mrs, deadline)
|
||||
}
|
||||
|
||||
func (vmssn *VMStorageSingleNode) GetMetricNamesUsageStats(qt *querytracer.Tracer, tt *storage.TenantToken, limit, le int, matchPattern string, deadline uint64) (metricnamestats.StatsResult, error) {
|
||||
vmssn.wg.Add(1)
|
||||
defer vmssn.wg.Done()
|
||||
return vmssn.vms.GetMetricNamesUsageStats(qt, tt, limit, le, matchPattern, deadline)
|
||||
}
|
||||
|
||||
func (vmssn *VMStorageSingleNode) ResetMetricNamesUsageStats(qt *querytracer.Tracer, deadline uint64) error {
|
||||
vmssn.wg.Add(1)
|
||||
defer vmssn.wg.Done()
|
||||
return vmssn.vms.ResetMetricNamesUsageStats(qt, deadline)
|
||||
}
|
||||
|
||||
func (vmssn *VMStorageSingleNode) GetMetadataRecords(qt *querytracer.Tracer, tt *storage.TenantToken, limit int, metricName string, deadline uint64) ([]*metricsmetadata.Row, error) {
|
||||
vmssn.wg.Add(1)
|
||||
defer vmssn.wg.Done()
|
||||
return vmssn.vms.GetMetadataRecords(qt, tt, limit, metricName, deadline)
|
||||
}
|
||||
@@ -48,7 +48,7 @@ func TestGetMaxMetrics(t *testing.T) {
|
||||
t.Helper()
|
||||
*maxUniqueTimeseries = storageMaxUniqueTimeseries
|
||||
s := storage.MustOpenStorage(t.Name(), storage.OpenOptions{})
|
||||
vms := newVMStorage(s, maxConcurrentRequests)
|
||||
vms := newVMStorage(s, maxConcurrentRequests, func(mrs []storage.MetricRow) {})
|
||||
defer vms.Stop()
|
||||
maxMetrics := vms.getMaxMetrics(searchQueryLimit)
|
||||
if maxMetrics != expect {
|
||||
|
||||
@@ -5,7 +5,7 @@ import uPlot from "uplot";
|
||||
import Button from "../../Main/Button/Button";
|
||||
import { CloseIcon, DragIcon } from "../../Main/Icons";
|
||||
import { SeriesItemStatsFormatted } from "../../../types";
|
||||
import { STATS_ORDER } from "../../../constants/graph";
|
||||
import { STATS_ORDER_TOOLTIP } from "../../../constants/graph";
|
||||
|
||||
export interface ChartTooltipProps {
|
||||
u?: uPlot;
|
||||
@@ -164,7 +164,7 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
||||
</div>
|
||||
{statsFormatted && (
|
||||
<table className="vm-chart-tooltip-stats">
|
||||
{STATS_ORDER.map((key, i) => (
|
||||
{STATS_ORDER_TOOLTIP.map((key, i) => (
|
||||
<div
|
||||
className="vm-chart-tooltip-stats-row"
|
||||
key={i}
|
||||
|
||||
@@ -61,7 +61,7 @@ const LegendConfigs: FC<Props> = ({ data, isCompact }) => {
|
||||
label: "Hide Statistics",
|
||||
value: hideStats,
|
||||
onChange: onChangeStats,
|
||||
info: "If enabled, hides the display of min, median, and max values.",
|
||||
info: "If enabled, hides the display of min, median, max, and last values.",
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import "./style.scss";
|
||||
import classNames from "classnames";
|
||||
import { getFreeFields } from "./helpers";
|
||||
import useCopyToClipboard from "../../../../../hooks/useCopyToClipboard";
|
||||
import { STATS_ORDER } from "../../../../../constants/graph";
|
||||
import { STATS_ORDER_LEGEND } from "../../../../../constants/graph";
|
||||
import { useShowStats } from "../hooks/useShowStats";
|
||||
import { useLegendFormat } from "../hooks/useLegendFormat";
|
||||
import { getLabelAlias } from "../../../../../utils/metric";
|
||||
@@ -80,7 +80,7 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange, duplicateFields })
|
||||
</div>
|
||||
{!hideStats && showStats && (
|
||||
<div className="vm-legend-item-stats">
|
||||
{STATS_ORDER.map((key, i) => (
|
||||
{STATS_ORDER_LEGEND.map((key, i) => (
|
||||
<div
|
||||
className="vm-legend-item-stats-row"
|
||||
key={i}
|
||||
|
||||
@@ -4,11 +4,11 @@ import "./style.scss";
|
||||
import { LegendItemType } from "../../../../../types";
|
||||
import { MouseEvent } from "react";
|
||||
import classNames from "classnames";
|
||||
import { STATS_ORDER } from "../../../../../constants/graph";
|
||||
import { STATS_ORDER_LEGEND } from "../../../../../constants/graph";
|
||||
import { useShowStats } from "../hooks/useShowStats";
|
||||
import { getValueByPath } from "../../../../../utils/object";
|
||||
|
||||
const statsColumns = STATS_ORDER.map(k => ({
|
||||
const statsColumns = STATS_ORDER_LEGEND.map(k => ({
|
||||
key: `statsFormatted.${k}`,
|
||||
title: k
|
||||
}));
|
||||
|
||||
@@ -26,4 +26,5 @@ export const GRAPH_SIZES: GraphSize[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const STATS_ORDER: (keyof SeriesItemStatsFormatted)[] = ["min", "median", "max"];
|
||||
export const STATS_ORDER_LEGEND: (keyof SeriesItemStatsFormatted)[] = ["min", "median", "max", "last"];
|
||||
export const STATS_ORDER_TOOLTIP: (keyof SeriesItemStatsFormatted)[] = ["min", "median", "max"];
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface SeriesItemStatsFormatted {
|
||||
min: string,
|
||||
max: string,
|
||||
median: string,
|
||||
last: string,
|
||||
}
|
||||
|
||||
export interface SeriesItem extends Series {
|
||||
|
||||
@@ -53,6 +53,7 @@ const getSeriesStatistics = (d: MetricResult) => {
|
||||
min: formatPrettyNumber(min, min, max),
|
||||
max: formatPrettyNumber(max, min, max),
|
||||
median: formatPrettyNumber(median, min, max),
|
||||
last: formatPrettyNumber(values.at(-1), min, max),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -156,14 +156,14 @@ func readAllAndClose(t *testing.T, responseBody io.ReadCloser) string {
|
||||
//
|
||||
// This type is expected to be embedded by the apps that serve metrics.
|
||||
type metricsClient struct {
|
||||
metricsCli *Client
|
||||
url string
|
||||
cli *Client
|
||||
url string
|
||||
}
|
||||
|
||||
func newMetricsClient(cli *Client, addr string) *metricsClient {
|
||||
return &metricsClient{
|
||||
metricsCli: cli,
|
||||
url: fmt.Sprintf("http://%s/metrics", addr),
|
||||
cli: cli,
|
||||
url: fmt.Sprintf("http://%s/metrics", addr),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ func (c *metricsClient) GetIntMetric(t *testing.T, metricName string) int {
|
||||
func (c *metricsClient) GetMetric(t *testing.T, metricName string) float64 {
|
||||
t.Helper()
|
||||
|
||||
metrics, statusCode := c.metricsCli.Get(t, c.url, nil)
|
||||
metrics, statusCode := c.cli.Get(t, c.url, nil)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusOK)
|
||||
}
|
||||
@@ -205,7 +205,7 @@ func (c *metricsClient) GetMetricsByPrefix(t *testing.T, prefix string) []float6
|
||||
|
||||
values := []float64{}
|
||||
|
||||
metrics, statusCode := c.metricsCli.Get(t, c.url, nil)
|
||||
metrics, statusCode := c.cli.Get(t, c.url, nil)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusOK)
|
||||
}
|
||||
@@ -234,7 +234,7 @@ func (c *metricsClient) GetMetricsByRegexp(t *testing.T, re *regexp.Regexp) []fl
|
||||
|
||||
values := []float64{}
|
||||
|
||||
metrics, statusCode := c.metricsCli.Get(t, c.url, nil)
|
||||
metrics, statusCode := c.cli.Get(t, c.url, nil)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusOK)
|
||||
}
|
||||
@@ -270,7 +270,7 @@ func (c *metricsClient) rpcRowsSentTotal(t *testing.T) int {
|
||||
}
|
||||
|
||||
type vmselectClient struct {
|
||||
vmselectCli *Client
|
||||
cli *Client
|
||||
url func(op, path string, opts QueryOpts) string
|
||||
metricNamesStatsResetURL string
|
||||
tenantsURL string
|
||||
@@ -287,7 +287,7 @@ func (c *vmselectClient) PrometheusAPIV1Export(t *testing.T, query string, opts
|
||||
values := opts.asURLValues()
|
||||
values.Add("match[]", query)
|
||||
values.Add("format", "promapi")
|
||||
res, _ := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
res, _ := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
return NewPrometheusAPIV1QueryResponse(t, res)
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ func (c *vmselectClient) PrometheusAPIV1ExportNative(t *testing.T, query string,
|
||||
values := opts.asURLValues()
|
||||
values.Add("match[]", query)
|
||||
values.Add("format", "promapi")
|
||||
res, _ := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
res, _ := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
return []byte(res)
|
||||
}
|
||||
|
||||
@@ -315,7 +315,7 @@ func (c *vmselectClient) PrometheusAPIV1Query(t *testing.T, query string, opts Q
|
||||
url := c.url("select", "prometheus/api/v1/query", opts)
|
||||
values := opts.asURLValues()
|
||||
values.Add("query", query)
|
||||
res, _ := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
res, _ := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
return NewPrometheusAPIV1QueryResponse(t, res)
|
||||
}
|
||||
|
||||
@@ -329,7 +329,7 @@ func (c *vmselectClient) PrometheusAPIV1QueryRange(t *testing.T, query string, o
|
||||
url := c.url("select", "prometheus/api/v1/query_range", opts)
|
||||
values := opts.asURLValues()
|
||||
values.Add("query", query)
|
||||
res, _ := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
res, _ := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
return NewPrometheusAPIV1QueryResponse(t, res)
|
||||
}
|
||||
|
||||
@@ -342,7 +342,7 @@ func (c *vmselectClient) PrometheusAPIV1Series(t *testing.T, matchQuery string,
|
||||
url := c.url("select", "prometheus/api/v1/series", opts)
|
||||
values := opts.asURLValues()
|
||||
values.Add("match[]", matchQuery)
|
||||
res, _ := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
res, _ := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
return NewPrometheusAPIV1SeriesResponse(t, res)
|
||||
}
|
||||
|
||||
@@ -354,7 +354,7 @@ func (c *vmselectClient) PrometheusAPIV1SeriesCount(t *testing.T, opts QueryOpts
|
||||
t.Helper()
|
||||
url := c.url("select", "prometheus/api/v1/series/count", opts)
|
||||
values := opts.asURLValues()
|
||||
res, _ := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
res, _ := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
return NewPrometheusAPIV1SeriesCountResponse(t, res)
|
||||
}
|
||||
|
||||
@@ -367,7 +367,7 @@ func (c *vmselectClient) PrometheusAPIV1Labels(t *testing.T, matchQuery string,
|
||||
url := c.url("select", "prometheus/api/v1/labels", opts)
|
||||
values := opts.asURLValues()
|
||||
values.Add("match[]", matchQuery)
|
||||
res, _ := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
res, _ := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
return NewPrometheusAPIV1LabelsResponse(t, res)
|
||||
}
|
||||
|
||||
@@ -382,7 +382,7 @@ func (c *vmselectClient) PrometheusAPIV1LabelValues(t *testing.T, labelName, mat
|
||||
url := c.url("select", path, opts)
|
||||
values := opts.asURLValues()
|
||||
values.Add("match[]", matchQuery)
|
||||
res, _ := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
res, _ := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
return NewPrometheusAPIV1LabelValuesResponse(t, res)
|
||||
}
|
||||
|
||||
@@ -394,7 +394,7 @@ func (c *vmselectClient) PrometheusAPIV1Metadata(t *testing.T, metric string, li
|
||||
values := opts.asURLValues()
|
||||
values.Add("metric", metric)
|
||||
values.Add("limit", strconv.Itoa(limit))
|
||||
res, _ := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
res, _ := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
return NewPrometheusAPIV1Metadata(t, res)
|
||||
}
|
||||
|
||||
@@ -408,7 +408,7 @@ func (c *vmselectClient) PrometheusAPIV1AdminTSDBDeleteSeries(t *testing.T, matc
|
||||
url := c.url("delete", "prometheus/api/v1/admin/tsdb/delete_series", opts)
|
||||
values := opts.asURLValues()
|
||||
values.Add("match[]", matchQuery)
|
||||
res, statusCode := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
res, statusCode := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
if statusCode != http.StatusNoContent {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", statusCode, http.StatusNoContent, res)
|
||||
}
|
||||
@@ -426,7 +426,7 @@ func (c *vmselectClient) PrometheusAPIV1StatusMetricNamesStats(t *testing.T, lim
|
||||
values.Add("limit", limit)
|
||||
values.Add("le", le)
|
||||
values.Add("match_pattern", matchPattern)
|
||||
res, statusCode := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
res, statusCode := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", statusCode, http.StatusOK, res)
|
||||
}
|
||||
@@ -455,7 +455,7 @@ func (c *vmselectClient) PrometheusAPIV1StatusTSDB(t *testing.T, matchQuery stri
|
||||
addNonEmpty("match[]", matchQuery)
|
||||
addNonEmpty("topN", topN)
|
||||
addNonEmpty("date", date)
|
||||
res, statusCode := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
res, statusCode := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", statusCode, http.StatusOK, res)
|
||||
}
|
||||
@@ -476,7 +476,7 @@ func (c *vmselectClient) GraphiteMetricsIndex(t *testing.T, opts QueryOpts) Grap
|
||||
t.Helper()
|
||||
|
||||
url := c.url("select", "graphite/metrics/index.json", opts)
|
||||
res, statusCode := c.vmselectCli.Get(t, url, opts.Headers)
|
||||
res, statusCode := c.cli.Get(t, url, opts.Headers)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", statusCode, http.StatusOK, res)
|
||||
}
|
||||
@@ -499,7 +499,7 @@ func (c *vmselectClient) GraphiteMetricsFind(t *testing.T, query string, opts Qu
|
||||
url := c.url("select", "graphite/metrics/find", opts)
|
||||
values := opts.asURLValues()
|
||||
values.Add("query", query)
|
||||
resText, statusCode := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
resText, statusCode := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", statusCode, http.StatusOK, resText)
|
||||
}
|
||||
@@ -522,7 +522,7 @@ func (c *vmselectClient) GraphiteMetricsExpand(t *testing.T, query string, opts
|
||||
url := c.url("select", "graphite/metrics/expand", opts)
|
||||
values := opts.asURLValues()
|
||||
values.Add("query", query)
|
||||
resText, statusCode := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
resText, statusCode := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", statusCode, http.StatusOK, resText)
|
||||
}
|
||||
@@ -546,7 +546,7 @@ func (c *vmselectClient) GraphiteRender(t *testing.T, target string, opts QueryO
|
||||
values := opts.asURLValues()
|
||||
values.Add("format", "json")
|
||||
values.Add("target", target)
|
||||
resText, statusCode := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
resText, statusCode := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", statusCode, http.StatusOK, resText)
|
||||
}
|
||||
@@ -567,7 +567,7 @@ func (c *vmselectClient) GraphiteTagsTagSeries(t *testing.T, record string, opts
|
||||
url := c.url("select", "graphite/tags/tagSeries", opts)
|
||||
values := opts.asURLValues()
|
||||
values.Add("path", record)
|
||||
_, statusCode := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
_, statusCode := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
if got, want := statusCode, http.StatusNotImplemented; got != want {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", got, want)
|
||||
}
|
||||
@@ -584,7 +584,7 @@ func (c *vmselectClient) GraphiteTagsTagMultiSeries(t *testing.T, records []stri
|
||||
for _, rec := range records {
|
||||
values.Add("path", rec)
|
||||
}
|
||||
_, statusCode := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
_, statusCode := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
if got, want := statusCode, http.StatusNotImplemented; got != want {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", got, want)
|
||||
}
|
||||
@@ -598,7 +598,7 @@ func (c *vmselectClient) GraphiteTagsTagMultiSeries(t *testing.T, records []stri
|
||||
func (c *vmselectClient) PrometheusAPIV1AdminStatusMetricNamesStatsReset(t *testing.T, opts QueryOpts) {
|
||||
t.Helper()
|
||||
values := opts.asURLValues()
|
||||
res, statusCode := c.vmselectCli.PostForm(t, c.metricNamesStatsResetURL, values, opts.Headers)
|
||||
res, statusCode := c.cli.PostForm(t, c.metricNamesStatsResetURL, values, opts.Headers)
|
||||
if statusCode != http.StatusNoContent {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", statusCode, http.StatusNoContent, res)
|
||||
}
|
||||
@@ -608,7 +608,7 @@ func (c *vmselectClient) PrometheusAPIV1AdminStatusMetricNamesStatsReset(t *test
|
||||
// /admin/tenants endpoint.
|
||||
func (c *vmselectClient) APIV1AdminTenants(t *testing.T, opts QueryOpts) *AdminTenantsResponse {
|
||||
t.Helper()
|
||||
res, statusCode := c.vmselectCli.Get(t, c.tenantsURL, opts.Headers)
|
||||
res, statusCode := c.cli.Get(t, c.tenantsURL, opts.Headers)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", statusCode, http.StatusOK, res)
|
||||
}
|
||||
@@ -622,7 +622,7 @@ func (c *vmselectClient) APIV1AdminTenants(t *testing.T, opts QueryOpts) *AdminT
|
||||
}
|
||||
|
||||
type vminsertClient struct {
|
||||
vminsertCli *Client
|
||||
cli *Client
|
||||
url func(op, path string, opts QueryOpts) string
|
||||
openTSDBURL func(op, path string, opts QueryOpts) string
|
||||
graphiteListenAddr string
|
||||
@@ -647,7 +647,7 @@ func (c *vminsertClient) PrometheusAPIV1ImportCSV(t *testing.T, records []string
|
||||
headers := opts.getHeaders()
|
||||
headers.Set("Content-Type", "text/plain")
|
||||
c.sendBlocking(t, len(records), func() {
|
||||
_, statusCode := c.vminsertCli.Post(t, url, data, headers)
|
||||
_, statusCode := c.cli.Post(t, url, data, headers)
|
||||
if statusCode != http.StatusNoContent {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusNoContent)
|
||||
}
|
||||
@@ -671,7 +671,7 @@ func (c *vminsertClient) PrometheusAPIV1ImportNative(t *testing.T, data []byte,
|
||||
headers := opts.getHeaders()
|
||||
headers.Set("Content-Type", "text/plain")
|
||||
c.sendBlocking(t, 1, func() {
|
||||
_, statusCode := c.vminsertCli.Post(t, url, data, headers)
|
||||
_, statusCode := c.cli.Post(t, url, data, headers)
|
||||
if statusCode != http.StatusNoContent {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusNoContent)
|
||||
}
|
||||
@@ -693,7 +693,7 @@ func (c *vminsertClient) PrometheusAPIV1Write(t *testing.T, wr prompb.WriteReque
|
||||
headers := opts.getHeaders()
|
||||
headers.Set("Content-Type", "application/x-protobuf")
|
||||
c.sendBlocking(t, recordsCount, func() {
|
||||
_, statusCode := c.vminsertCli.Post(t, url, data, headers)
|
||||
_, statusCode := c.cli.Post(t, url, data, headers)
|
||||
if statusCode != http.StatusNoContent {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusNoContent)
|
||||
}
|
||||
@@ -745,7 +745,7 @@ func (c *vminsertClient) PrometheusAPIV1ImportPrometheus(t *testing.T, records [
|
||||
headers := opts.getHeaders()
|
||||
headers.Set("Content-Type", "text/plain")
|
||||
c.sendBlocking(t, recordsCount, func() {
|
||||
_, statusCode := c.vminsertCli.Post(t, url, data, headers)
|
||||
_, statusCode := c.cli.Post(t, url, data, headers)
|
||||
if statusCode != http.StatusNoContent {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusNoContent)
|
||||
}
|
||||
@@ -771,7 +771,7 @@ func (c *vminsertClient) InfluxWrite(t *testing.T, records []string, opts QueryO
|
||||
headers.Set("Content-Type", "text/plain")
|
||||
c.sendBlocking(t, len(records), func() {
|
||||
t.Helper()
|
||||
_, statusCode := c.vminsertCli.Post(t, url, data, headers)
|
||||
_, statusCode := c.cli.Post(t, url, data, headers)
|
||||
if statusCode != http.StatusNoContent {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusNoContent)
|
||||
}
|
||||
@@ -805,7 +805,7 @@ func (c *vminsertClient) OpentelemetryV1Metrics(t *testing.T, md otlppb.MetricsD
|
||||
headers := opts.getHeaders()
|
||||
headers.Set("Content-Type", "application/x-protobuf")
|
||||
c.sendBlocking(t, recordsCount, func() {
|
||||
_, statusCode := c.vminsertCli.Post(t, url, data, headers)
|
||||
_, statusCode := c.cli.Post(t, url, data, headers)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusOK)
|
||||
}
|
||||
@@ -830,7 +830,7 @@ func (c *vminsertClient) OpenTSDBAPIPut(t *testing.T, records []string, opts Que
|
||||
headers := opts.getHeaders()
|
||||
headers.Set("Content-Type", "application/json")
|
||||
c.sendBlocking(t, len(records), func() {
|
||||
_, statusCode := c.vminsertCli.Post(t, url, data, headers)
|
||||
_, statusCode := c.cli.Post(t, url, data, headers)
|
||||
if statusCode != http.StatusNoContent {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusNoContent)
|
||||
}
|
||||
@@ -853,7 +853,7 @@ func (c *vminsertClient) ZabbixConnectorHistory(t *testing.T, records []string,
|
||||
headers := opts.getHeaders()
|
||||
headers.Set("Content-Type", "application/json")
|
||||
c.sendBlocking(t, len(records), func() {
|
||||
_, statusCode := c.vminsertCli.Post(t, url, data, headers)
|
||||
_, statusCode := c.cli.Post(t, url, data, headers)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusOK)
|
||||
}
|
||||
@@ -867,11 +867,11 @@ func (c *vminsertClient) ZabbixConnectorHistory(t *testing.T, records []string,
|
||||
// See https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#ingesting
|
||||
func (c *vminsertClient) GraphiteWrite(t *testing.T, records []string, _ QueryOpts) {
|
||||
t.Helper()
|
||||
c.vminsertCli.Write(t, c.graphiteListenAddr, records)
|
||||
c.cli.Write(t, c.graphiteListenAddr, records)
|
||||
}
|
||||
|
||||
type vmstorageClient struct {
|
||||
vmstorageCli *Client
|
||||
cli *Client
|
||||
httpListenAddr string
|
||||
}
|
||||
|
||||
@@ -881,7 +881,7 @@ func (c *vmstorageClient) ForceFlush(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
url := fmt.Sprintf("http://%s/internal/force_flush", c.httpListenAddr)
|
||||
_, statusCode := c.vmstorageCli.Get(t, url, nil)
|
||||
_, statusCode := c.cli.Get(t, url, nil)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusOK)
|
||||
}
|
||||
@@ -892,7 +892,7 @@ func (c *vmstorageClient) ForceMerge(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
url := fmt.Sprintf("http://%s/internal/force_merge", c.httpListenAddr)
|
||||
_, statusCode := c.vmstorageCli.Get(t, url, nil)
|
||||
_, statusCode := c.cli.Get(t, url, nil)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusOK)
|
||||
}
|
||||
@@ -905,7 +905,7 @@ func (c *vmstorageClient) ForceMerge(t *testing.T) {
|
||||
func (c *vmstorageClient) SnapshotCreate(t *testing.T) *SnapshotCreateResponse {
|
||||
t.Helper()
|
||||
|
||||
data, statusCode := c.vmstorageCli.Post(t, c.SnapshotCreateURL(), nil, nil)
|
||||
data, statusCode := c.cli.Post(t, c.SnapshotCreateURL(), nil, nil)
|
||||
if got, want := statusCode, http.StatusOK; got != want {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", got, want, data)
|
||||
}
|
||||
@@ -931,7 +931,7 @@ func (c *vmstorageClient) APIV1AdminTSDBSnapshot(t *testing.T) *APIV1AdminTSDBSn
|
||||
t.Helper()
|
||||
|
||||
url := fmt.Sprintf("http://%s/api/v1/admin/tsdb/snapshot", c.httpListenAddr)
|
||||
data, statusCode := c.vmstorageCli.Post(t, url, nil, nil)
|
||||
data, statusCode := c.cli.Post(t, url, nil, nil)
|
||||
if got, want := statusCode, http.StatusOK; got != want {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", got, want, data)
|
||||
}
|
||||
@@ -952,7 +952,7 @@ func (c *vmstorageClient) SnapshotList(t *testing.T) *SnapshotListResponse {
|
||||
t.Helper()
|
||||
|
||||
url := fmt.Sprintf("http://%s/snapshot/list", c.httpListenAddr)
|
||||
data, statusCode := c.vmstorageCli.Get(t, url, nil)
|
||||
data, statusCode := c.cli.Get(t, url, nil)
|
||||
if got, want := statusCode, http.StatusOK; got != want {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", got, want, data)
|
||||
}
|
||||
@@ -973,7 +973,7 @@ func (c *vmstorageClient) SnapshotDelete(t *testing.T, snapshotName string) *Sna
|
||||
t.Helper()
|
||||
|
||||
url := fmt.Sprintf("http://%s/snapshot/delete?snapshot=%s", c.httpListenAddr, snapshotName)
|
||||
data, statusCode := c.vmstorageCli.Delete(t, url)
|
||||
data, statusCode := c.cli.Delete(t, url)
|
||||
wantStatusCodes := map[int]bool{
|
||||
http.StatusOK: true,
|
||||
http.StatusInternalServerError: true,
|
||||
@@ -998,7 +998,7 @@ func (c *vmstorageClient) SnapshotDeleteAll(t *testing.T) *SnapshotDeleteAllResp
|
||||
t.Helper()
|
||||
|
||||
url := fmt.Sprintf("http://%s/snapshot/delete_all", c.httpListenAddr)
|
||||
data, statusCode := c.vmstorageCli.Post(t, url, nil, nil)
|
||||
data, statusCode := c.cli.Post(t, url, nil, nil)
|
||||
if got, want := statusCode, http.StatusOK; got != want {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", got, want, data)
|
||||
}
|
||||
|
||||
@@ -45,11 +45,13 @@ func TestSingleMetricsMetadata(t *testing.T) {
|
||||
{Labels: []prompb.Label{{Name: "__name__", Value: "metric_name_4"}}, Samples: []prompb.Sample{{Value: 40, Timestamp: ingestTimestamp}}},
|
||||
{Labels: []prompb.Label{{Name: "__name__", Value: "metric_name_5"}}, Samples: []prompb.Sample{{Value: 40, Timestamp: ingestTimestamp}}},
|
||||
{Labels: []prompb.Label{{Name: "__name__", Value: "metric_name_6"}}, Samples: []prompb.Sample{{Value: 40, Timestamp: ingestTimestamp}}},
|
||||
{Labels: []prompb.Label{{Name: "__name__", Value: `metric_name_7_!@"_suffix`}}, Samples: []prompb.Sample{{Value: 40, Timestamp: ingestTimestamp}}},
|
||||
},
|
||||
Metadata: []prompb.MetricMetadata{
|
||||
{MetricFamilyName: "metric_name_4", Help: "some help message", Type: prompb.MetricTypeSummary},
|
||||
{MetricFamilyName: "metric_name_5", Help: "some help message", Type: prompb.MetricTypeSummary},
|
||||
{MetricFamilyName: "metric_name_6", Help: "some help message", Type: prompb.MetricTypeStateset},
|
||||
{MetricFamilyName: `metric_name_7_!@"_suffix`, Help: "some help message", Type: prompb.MetricTypeStateset},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -59,12 +61,13 @@ func TestSingleMetricsMetadata(t *testing.T) {
|
||||
expected := &apptest.PrometheusAPIV1Metadata{
|
||||
Status: "success",
|
||||
Data: map[string][]apptest.MetadataEntry{
|
||||
"metric_name_1": {{Help: "some help message", Type: "gauge"}},
|
||||
"metric_name_2": {{Help: "some help message", Type: "counter"}},
|
||||
"metric_name_3": {{Help: "some help message", Type: "gauge"}},
|
||||
"metric_name_4": {{Help: "some help message", Type: "summary"}},
|
||||
"metric_name_5": {{Help: "some help message", Type: "summary"}},
|
||||
"metric_name_6": {{Help: "some help message", Type: "stateset"}},
|
||||
"metric_name_1": {{Help: "some help message", Type: "gauge"}},
|
||||
"metric_name_2": {{Help: "some help message", Type: "counter"}},
|
||||
"metric_name_3": {{Help: "some help message", Type: "gauge"}},
|
||||
"metric_name_4": {{Help: "some help message", Type: "summary"}},
|
||||
"metric_name_5": {{Help: "some help message", Type: "summary"}},
|
||||
"metric_name_6": {{Help: "some help message", Type: "stateset"}},
|
||||
`metric_name_7_!@"_suffix`: {{Help: "some help message", Type: "stateset"}},
|
||||
},
|
||||
}
|
||||
gotStats := sut.PrometheusAPIV1Metadata(t, "", 0, apptest.QueryOpts{})
|
||||
@@ -154,11 +157,13 @@ func TestClusterMetricsMetadata(t *testing.T) {
|
||||
{Labels: []prompb.Label{{Name: "__name__", Value: "metric_name_4"}}, Samples: []prompb.Sample{{Value: 40, Timestamp: ingestTimestamp}}},
|
||||
{Labels: []prompb.Label{{Name: "__name__", Value: "metric_name_5"}}, Samples: []prompb.Sample{{Value: 40, Timestamp: ingestTimestamp}}},
|
||||
{Labels: []prompb.Label{{Name: "__name__", Value: "metric_name_6"}}, Samples: []prompb.Sample{{Value: 40, Timestamp: ingestTimestamp}}},
|
||||
{Labels: []prompb.Label{{Name: "__name__", Value: `metric_name_7_!@"_suffix`}}, Samples: []prompb.Sample{{Value: 40, Timestamp: ingestTimestamp}}},
|
||||
},
|
||||
Metadata: []prompb.MetricMetadata{
|
||||
{MetricFamilyName: "metric_name_4", Help: "some help message", Type: prompb.MetricTypeSummary},
|
||||
{MetricFamilyName: "metric_name_5", Help: "some help message", Type: prompb.MetricTypeSummary},
|
||||
{MetricFamilyName: "metric_name_6", Help: "some help message", Type: prompb.MetricTypeStateset},
|
||||
{MetricFamilyName: `metric_name_7_!@"_suffix`, Help: "some help message", Type: prompb.MetricTypeStateset},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -171,12 +176,13 @@ func TestClusterMetricsMetadata(t *testing.T) {
|
||||
expected := &apptest.PrometheusAPIV1Metadata{
|
||||
Status: "success",
|
||||
Data: map[string][]apptest.MetadataEntry{
|
||||
"metric_name_1": {{Help: "some help message", Type: "gauge"}},
|
||||
"metric_name_2": {{Help: "some help message", Type: "counter"}},
|
||||
"metric_name_3": {{Help: "some help message", Type: "gauge"}},
|
||||
"metric_name_4": {{Help: "some help message", Type: "summary"}},
|
||||
"metric_name_5": {{Help: "some help message", Type: "summary"}},
|
||||
"metric_name_6": {{Help: "some help message", Type: "stateset"}},
|
||||
"metric_name_1": {{Help: "some help message", Type: "gauge"}},
|
||||
"metric_name_2": {{Help: "some help message", Type: "counter"}},
|
||||
"metric_name_3": {{Help: "some help message", Type: "gauge"}},
|
||||
"metric_name_4": {{Help: "some help message", Type: "summary"}},
|
||||
"metric_name_5": {{Help: "some help message", Type: "summary"}},
|
||||
"metric_name_6": {{Help: "some help message", Type: "stateset"}},
|
||||
`metric_name_7_!@"_suffix`: {{Help: "some help message", Type: "stateset"}},
|
||||
},
|
||||
}
|
||||
gotStats := vmselect.PrometheusAPIV1Metadata(t, "", 0, apptest.QueryOpts{Tenant: tenantID})
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -332,13 +333,11 @@ func TestSingleVMAgentDropOnOverload(t *testing.T) {
|
||||
vmagent.APIV1ImportPrometheusNoWaitFlush(t, []string{
|
||||
"foo_bar 1 1652169600000", // 2022-05-10T08:00:00Z
|
||||
}, apptest.QueryOpts{})
|
||||
|
||||
waitFor(
|
||||
func() bool {
|
||||
return vmagent.RemoteWriteRequests(t, url1) == 1 && vmagent.RemoteWriteRequests(t, url2) == 1
|
||||
},
|
||||
)
|
||||
|
||||
// Send 2 more requests, the first RW endpoint should receive everything, the second should add them to the queue
|
||||
// since worker is busy with the first request.
|
||||
for i := range 2 {
|
||||
@@ -641,3 +640,116 @@ func TestSingleVMAgentMultitenancy(t *testing.T) {
|
||||
t.Fatalf("expected vmagent_tenant_inserted_rows_total to have value 1 for accountID=5, projectID=0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSingleVMAgentPriorizeRecentData(t *testing.T) {
|
||||
tc := apptest.NewTestCase(t)
|
||||
defer tc.Stop()
|
||||
|
||||
remoteWriteSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer remoteWriteSrv.Close()
|
||||
|
||||
var mustRW2ReturnError atomic.Bool
|
||||
mustRW2ReturnError.Store(true)
|
||||
|
||||
remoteWriteSrv2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if mustRW2ReturnError.Load() {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer remoteWriteSrv2.Close()
|
||||
|
||||
vmagent := tc.MustStartDefaultRWVmagent("vmagent", []string{
|
||||
fmt.Sprintf(`-remoteWrite.url=%s/api/v1/write`, remoteWriteSrv.URL),
|
||||
fmt.Sprintf(`-remoteWrite.url=%s/api/v1/write`, remoteWriteSrv2.URL),
|
||||
"-remoteWrite.disableOnDiskQueue=true",
|
||||
// use only 1 worker to get a full queue faster
|
||||
"-remoteWrite.queues=1",
|
||||
"-remoteWrite.flushInterval=1ms",
|
||||
"-remoteWrite.inmemoryQueues=1",
|
||||
// fastqueue size is roughly memory.Allowed() / len(urls) / *maxRowsPerBlock / 100
|
||||
// Use very large maxRowsPerBlock to get fastqueue of minimal length(2).
|
||||
// See initRemoteWriteCtxs function in remotewrite.go for details.
|
||||
"-remoteWrite.maxRowsPerBlock=1000000000",
|
||||
"-remoteWrite.tmpDataPath=" + tc.Dir() + "/vmagent",
|
||||
|
||||
// Delay retry logic to avoid race conditions with waitFor assertions.
|
||||
// It improves the test stability on resource-constrained runners.
|
||||
"-remoteWrite.retryMinInterval=3s",
|
||||
"-remoteWrite.retryMaxTime=3s",
|
||||
})
|
||||
|
||||
const (
|
||||
retries = 20
|
||||
period = 200 * time.Millisecond
|
||||
)
|
||||
|
||||
waitFor := func(f func() bool) {
|
||||
t.Helper()
|
||||
for range retries {
|
||||
if f() {
|
||||
return
|
||||
}
|
||||
time.Sleep(period)
|
||||
}
|
||||
t.Fatalf("timed out waiting for retry #%d", retries)
|
||||
}
|
||||
|
||||
// Real remote write URLs are hidden in metrics
|
||||
url1 := "1:secret-url"
|
||||
url2 := "2:secret-url"
|
||||
|
||||
// Wait until first request got flushed to remote write server
|
||||
vmagent.APIV1ImportPrometheusNoWaitFlush(t, []string{
|
||||
"foo_bar 1 1652169600000", // 2022-05-10T08:00:00Z
|
||||
}, apptest.QueryOpts{})
|
||||
waitFor(
|
||||
func() bool {
|
||||
return vmagent.RemoteWriteRequests(t, url1) == 1 && vmagent.RemoteWriteRequests(t, url2) == 1
|
||||
},
|
||||
)
|
||||
// Wait until second request got flushed to remote write server
|
||||
// since there are 2 independent queues (general and in-memory) with minimal capacity of 1
|
||||
vmagent.APIV1ImportPrometheusNoWaitFlush(t, []string{
|
||||
"foo_bar 1 1652169600000", // 2022-05-10T08:00:00Z
|
||||
}, apptest.QueryOpts{})
|
||||
waitFor(
|
||||
func() bool {
|
||||
return vmagent.RemoteWriteRequests(t, url1) == 2 && vmagent.RemoteWriteRequests(t, url2) == 2
|
||||
},
|
||||
)
|
||||
// Send 2 more requests, the first RW endpoint should receive everything, the second should add them to the queue
|
||||
// since worker is busy with the first request.
|
||||
for i := range 2 {
|
||||
vmagent.APIV1ImportPrometheusNoWaitFlush(t, []string{
|
||||
"foo_bar 1 1652169600000", // 2022-05-10T08:00:00Z
|
||||
}, apptest.QueryOpts{})
|
||||
|
||||
waitFor(
|
||||
func() bool {
|
||||
return vmagent.RemoteWriteRequests(t, url1) == 3+i && vmagent.RemoteWritePendingInmemoryBlocks(t, url2) == 1+i
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Send one more request.
|
||||
vmagent.APIV1ImportPrometheusNoWaitFlush(t, []string{
|
||||
"foo_bar 1 1652169600000", // 2022-05-10T08:00:00Z
|
||||
}, apptest.QueryOpts{})
|
||||
|
||||
waitFor(
|
||||
func() bool {
|
||||
return vmagent.RemoteWriteRequests(t, url1) == 5 && vmagent.RemoteWriteSamplesDropped(t, url2) > 0
|
||||
},
|
||||
)
|
||||
mustRW2ReturnError.Store(false)
|
||||
// ensure that inmemory data correctly flushed to the remote write
|
||||
waitFor(
|
||||
func() bool {
|
||||
return vmagent.RemoteWritePendingInmemoryBlocks(t, url2) == 0
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ type vminsertRuntimeValues struct {
|
||||
func newVminsert(app *app, cli *Client, rt vminsertRuntimeValues) *Vminsert {
|
||||
metricsClient := newMetricsClient(cli, rt.httpListenAddr)
|
||||
vminsertClient := &vminsertClient{
|
||||
vminsertCli: cli,
|
||||
cli: cli,
|
||||
url: func(op, path string, opts QueryOpts) string {
|
||||
return getClusterPath(rt.httpListenAddr, op, path, opts)
|
||||
},
|
||||
|
||||
@@ -48,7 +48,7 @@ func newVmselect(app *app, cli *Client, rt vmselectRuntimeValues) *Vmselect {
|
||||
app: app,
|
||||
metricsClient: newMetricsClient(cli, rt.httpListenAddr),
|
||||
vmselectClient: &vmselectClient{
|
||||
vmselectCli: cli,
|
||||
cli: cli,
|
||||
url: func(op, path string, opts QueryOpts) string {
|
||||
return getClusterPath(rt.httpListenAddr, op, path, opts)
|
||||
},
|
||||
|
||||
@@ -58,11 +58,11 @@ func newVmsingle(app *app, cli *Client, rt vmsingleRuntimeValues) *Vmsingle {
|
||||
app: app,
|
||||
metricsClient: newMetricsClient(cli, rt.httpListenAddr),
|
||||
vmstorageClient: &vmstorageClient{
|
||||
vmstorageCli: cli,
|
||||
cli: cli,
|
||||
httpListenAddr: rt.httpListenAddr,
|
||||
},
|
||||
vmselectClient: &vmselectClient{
|
||||
vmselectCli: cli,
|
||||
cli: cli,
|
||||
url: func(op, path string, opts QueryOpts) string {
|
||||
return fmt.Sprintf("http://%s/%s", rt.httpListenAddr, path)
|
||||
},
|
||||
@@ -70,7 +70,7 @@ func newVmsingle(app *app, cli *Client, rt vmsingleRuntimeValues) *Vmsingle {
|
||||
tenantsURL: "vmsingle-does-not-serve-tenants",
|
||||
},
|
||||
vminsertClient: &vminsertClient{
|
||||
vminsertCli: cli,
|
||||
cli: cli,
|
||||
url: func(_, path string, _ QueryOpts) string {
|
||||
return fmt.Sprintf("http://%s/%s", rt.httpListenAddr, path)
|
||||
},
|
||||
|
||||
@@ -63,7 +63,7 @@ func newVmstorage(app *app, cli *Client, rt vmstorageRuntimeValues) *Vmstorage {
|
||||
app: app,
|
||||
metricsClient: newMetricsClient(cli, rt.httpListenAddr),
|
||||
vmstorageClient: &vmstorageClient{
|
||||
vmstorageCli: cli,
|
||||
cli: cli,
|
||||
httpListenAddr: rt.httpListenAddr,
|
||||
},
|
||||
storageDataPath: rt.storageDataPath,
|
||||
|
||||
@@ -6201,7 +6201,7 @@
|
||||
"type": "victoriametrics-metrics-datasource",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "The rate of ignored samples during aggregation. \nStream aggregation will drop samples with NaN values, or samples with too old timestamps. See https://docs.victoriametrics.com/victoriametrics/stream-aggregation/#ignoring-old-samples ",
|
||||
"description": "The rate of dropped samples during aggregation. \nStream aggregation will drop samples with NaN values, too old timestamps or samples identified as duplicates during deduplication. See https://docs.victoriametrics.com/victoriametrics/stream-aggregation/#ignoring-old-samples ",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -6282,14 +6282,14 @@
|
||||
"uid": "$ds"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "sum(rate(vm_streamaggr_ignored_samples_total{job=~\"$job\",instance=~\"$instance\", url=~\"$url\"}[$__rate_interval]) > 0) without (instance, pod)",
|
||||
"expr": "sum(rate({__name__=~\"vm_streamaggr_ignored_samples_total|vm_streamaggr_dedup_dropped_samples_total\", job=~\"$job\",instance=~\"$instance\", url=~\"$url\"}[$__rate_interval]) > 0) without (instance, pod)",
|
||||
"instant": false,
|
||||
"legendFormat": "__auto",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Ignored samples ($instance)",
|
||||
"title": "Dropped samples ($instance)",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6200,7 +6200,7 @@
|
||||
"type": "prometheus",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "The rate of ignored samples during aggregation. \nStream aggregation will drop samples with NaN values, or samples with too old timestamps. See https://docs.victoriametrics.com/victoriametrics/stream-aggregation/#ignoring-old-samples ",
|
||||
"description": "The rate of dropped samples during aggregation. \nStream aggregation will drop samples with NaN values, too old timestamps or samples identified as duplicates during deduplication. See https://docs.victoriametrics.com/victoriametrics/stream-aggregation/#ignoring-old-samples ",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -6281,14 +6281,14 @@
|
||||
"uid": "$ds"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "sum(rate(vm_streamaggr_ignored_samples_total{job=~\"$job\",instance=~\"$instance\", url=~\"$url\"}[$__rate_interval]) > 0) without (instance, pod)",
|
||||
"expr": "sum(rate({__name__=~\"vm_streamaggr_ignored_samples_total|vm_streamaggr_dedup_dropped_samples_total\", job=~\"$job\",instance=~\"$instance\", url=~\"$url\"}[$__rate_interval]) > 0) without (instance, pod)",
|
||||
"instant": false,
|
||||
"legendFormat": "__auto",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Ignored samples ($instance)",
|
||||
"title": "Dropped samples ($instance)",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -59,7 +59,7 @@ services:
|
||||
- '--external.alert.source=explore?orgId=1&left=["now-1h","now","VictoriaMetrics",{"expr": },{"mode":"Metrics"},{"ui":[true,true,true,"none"]}]'
|
||||
restart: always
|
||||
vmanomaly:
|
||||
image: victoriametrics/vmanomaly:v1.29.4
|
||||
image: victoriametrics/vmanomaly:v1.29.6
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"content": "If you don't observe any data initially, please wait a few minutes for it to appear. \n\nUpon the first running the guide (if there is not enough node_exporter monitoring data collected in your system), you may notice a significant number of false positive anomalies found. The predictions will become more accurate with at least two weeks' (full `fit_window`) worth of data provided to vmanomaly.\n\nEach row displays information for a distinct mode. The query used for anomaly detection is `sum(rate(node_cpu_seconds_total[5m])) by (mode, instance, job)`.\n",
|
||||
"mode": "markdown"
|
||||
},
|
||||
"pluginVersion": "10.2.1",
|
||||
"pluginVersion": "12.2.0",
|
||||
"title": "Overview",
|
||||
"type": "text"
|
||||
},
|
||||
|
||||
@@ -14,6 +14,28 @@ aliases:
|
||||
---
|
||||
Please find the changelog for VictoriaMetrics Anomaly Detection below.
|
||||
|
||||
## v1.29.6
|
||||
Released: 2026-06-17
|
||||
|
||||
- BUGFIX: Fixed `VLogsReader` startup and query execution when `tenant_id` is omitted or provided in short account-only form such as `"0"`. Omitted or empty tenant IDs are treated as single-node/no-tenant mode, and account-only tenant IDs are expanded to `accountID:0` before adding VictoriaLogs `AccountID`/`ProjectID` params or VM tenant labels.
|
||||
|
||||
- BUGFIX: Hardened [`OnlineMADModel`](https://docs.victoriametrics.com/anomaly-detection/components/models/#online-mad) anomaly scoring for perfectly constant time series (all values identical). The model now keeps a small deterministic prediction interval when the learned MAD is zero, so values deviating from an unknown constant baseline can produce `anomaly_score > 1` (previously, all anomaly scores were `0`).
|
||||
|
||||
## v1.29.5
|
||||
Released: 2026-06-11
|
||||
|
||||
- UI: Updated [vmanomaly UI](https://docs.victoriametrics.com/anomaly-detection/ui/) from [v1.7.0](https://docs.victoriametrics.com/anomaly-detection/ui/#v170) to [v1.7.1](https://docs.victoriametrics.com/anomaly-detection/ui/#v171), see respective [release notes](https://docs.victoriametrics.com/anomaly-detection/ui/#v171) for details.
|
||||
|
||||
- IMPROVEMENT: Redesigned [hot reload](https://docs.victoriametrics.com/anomaly-detection/components/#hot-reload) config change detection to content-based polling with configurable `-configCheckInterval`, improving reliability for Kubernetes ConfigMap symlink rotations and other filesystems where event delivery can be inconsistent.
|
||||
|
||||
- IMPROVEMENT: Refined config validation errors for broken or invalid config sections, so startup and reload failures point to the affected section more clearly (e.g. YAML indentation typos).
|
||||
|
||||
- IMPROVEMENT: Tightened config validation for [`PeriodicScheduler`](https://docs.victoriametrics.com/anomaly-detection/components/scheduler/#periodic-scheduler) `infer_every` and [`IsolationForestModel`](https://docs.victoriametrics.com/anomaly-detection/components/models/#isolation-forest-multivariate) `contamination`, including clearer handling of missing scheduler intervals, numeric contamination strings, and invalid non-finite values.
|
||||
|
||||
- BUGFIX: Fixed a multiprocessing startup issue with `settings.n_workers > 1` that could leave scheduled data fetch or successive inference jobs stuck and repeatedly skipped by internal scheduler.
|
||||
|
||||
- BUGFIX: Bounded [`VmReader`](https://docs.victoriametrics.com/anomaly-detection/components/reader/#vm-reader) and [`VLogsReader`](https://docs.victoriametrics.com/anomaly-detection/components/reader/#victorialogs-reader) data fetch and post-fetch processing waits so stalled datasource reads or multiprocessing dataframe creation no longer keep [`PeriodicScheduler`](https://docs.victoriametrics.com/anomaly-detection/components/scheduler/#periodic-scheduler) `data_fetch` jobs running indefinitely. Previously, such stuck jobs could keep internal scheduler's `max_instances=1` slot per (scheduler, query) pair occupied, causing future data fetch, fit, or infer runs to be skipped until vmanomaly was restarted. The config validator now also warns when the configured reader timeout budget can exceed the connected scheduler interval.
|
||||
|
||||
## v1.29.4
|
||||
Released: 2026-05-15
|
||||
|
||||
@@ -652,7 +674,7 @@ Released: 2024-02-15
|
||||
|
||||
## v1.9.2
|
||||
Released: 2024-01-29
|
||||
- BUGFIX: now multivariate models (like [`IsolationForestMultivariateModel`](https://docs.victoriametrics.com/anomaly-detection/components/models/#isolation-foresthttpsenwikipediaorgwikiisolation_forest-multivariate)) are properly handled throughout fit/infer phases.
|
||||
- BUGFIX: now multivariate models (like [`IsolationForestMultivariateModel`](https://docs.victoriametrics.com/anomaly-detection/components/models/#isolation-forest-multivariate)) are properly handled throughout fit/infer phases.
|
||||
|
||||
|
||||
## v1.9.1
|
||||
|
||||
@@ -423,7 +423,7 @@ services:
|
||||
# ...
|
||||
vmanomaly:
|
||||
container_name: vmanomaly
|
||||
image: victoriametrics/vmanomaly:v1.29.4
|
||||
image: victoriametrics/vmanomaly:v1.29.6
|
||||
# ...
|
||||
restart: always
|
||||
volumes:
|
||||
@@ -641,7 +641,7 @@ options:
|
||||
Here’s an example of using the config splitter to divide configurations based on the `extra_filters` argument from the reader section:
|
||||
|
||||
```sh
|
||||
docker pull victoriametrics/vmanomaly:v1.29.4 && docker image tag victoriametrics/vmanomaly:v1.29.4 vmanomaly
|
||||
docker pull victoriametrics/vmanomaly:v1.29.6 && docker image tag victoriametrics/vmanomaly:v1.29.6 vmanomaly
|
||||
```
|
||||
|
||||
```sh
|
||||
|
||||
@@ -45,8 +45,7 @@ There are 2 types of compatibility to consider when migrating in stateful mode:
|
||||
|
||||
| Group start | Group end | Compatibility | Notes |
|
||||
|---------|--------- |------------|-------|
|
||||
| [v1.29.3](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1293) | Latest* | Fully Compatible | Just a placeholder for new releases |
|
||||
| [v1.29.1](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1291) | [v1.29.3](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1293) | Fully Compatible | - |
|
||||
| [v1.29.1](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1291) | [v1.29.6](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1296) | Fully Compatible | - |
|
||||
| [v1.28.7](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1287) | [v1.29.0](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1290) | Partially compatible* | Dumped models of class [prophet](https://docs.victoriametrics.com/anomaly-detection/components/models/#prophet) and [seasonal quantile](https://docs.victoriametrics.com/anomaly-detection/components/models/#online-seasonal-quantile) have problems with loading to [v1.29.0](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1290) due to dropped `pytz` library. **Upgrading directly from v1.28.7 to [v1.29.1](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1291) with a fix is suggested** |
|
||||
| [v1.26.0](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1262) | [v1.28.7](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1287) | Fully Compatible | [v1.28.0](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1280) introduced [rolling](https://docs.victoriametrics.com/anomaly-detection/components/models/#rolling-models) model class drop in favor of [online](https://docs.victoriametrics.com/anomaly-detection/components/models/#online-models) models (`rolling_quantile` and `std` models), however, it does not impact compatibility, as artifacts were not produced by default for rolling models. Also, offline `mad` and `zscore` models are redirecting to their respective online counterparts since [v1.28.4](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1284). |
|
||||
| [v1.25.3](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1253) | [v1.26.0](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1270) | Partially Compatible* | [v1.25.3](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1253) introduced `forecast_at` argument for base [univariate](https://docs.victoriametrics.com/anomaly-detection/components/models/#univariate-models) and `Prophet` [models](https://docs.victoriametrics.com/anomaly-detection/components/models/#prophet), however, itself remains backward-reversible from newer states like [v1.26.2](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1262), [v1.27.0](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1270). (All models except `isolation_forest_multivariate` class will be dropped) |
|
||||
|
||||
@@ -37,29 +37,39 @@ The `vmanomaly` service supports a set of command-line arguments to configure it
|
||||
> Single-dashed command-line argument {{% available_from "v1.23.3" anomaly %}} format can be used, e.g. `-license.forceOffline` in addition to `--license.forceOffline`. This aligns better with other VictoriaMetrics ecosystem components. Mixing the two styles is also supported, e.g. `-license.forceOffline --loggerLevel INFO`.
|
||||
|
||||
```shellhelp
|
||||
usage: vmanomaly.py [-h] [--license STRING | --licenseFile PATH] [--license.forceOffline] [--loggerLevel {DEBUG,WARNING,FATAL,ERROR,INFO}] [--watch] [--dryRun] [--outputSpec PATH] config [config ...]
|
||||
usage: vmanomaly.py [-h] [--license STRING | --licenseFile PATH] [--license.forceOffline] [--loggerLevel {DEBUG,INFO,WARNING,ERROR,FATAL}] [--watch] [-configCheckInterval DURATION] [--dryRun] [--outputSpec PATH] config [config ...]
|
||||
|
||||
VictoriaMetrics Anomaly Detection Service
|
||||
|
||||
positional arguments:
|
||||
config YAML config file(s) or directories containing YAML files. Multiple files will recursively merge each other values so multiple configs can be combined. If a directory is provided,
|
||||
all `.yaml` files inside will be merged, without recursion. Default: vmanomaly.yaml is expected in the current directory.
|
||||
config YAML config file(s) or directories containing YAML files. Multiple files will recursively merge each other
|
||||
values so multiple configs can be combined. If a directory is provided, all `.yaml` files inside will be
|
||||
merged, without recursion. Default: vmanomaly.yaml is expected in the current directory.
|
||||
|
||||
options:
|
||||
-h Show this help message and exit
|
||||
--license STRING License key for VictoriaMetrics Enterprise. See https://victoriametrics.com/products/enterprise/trial/ to obtain a trial license.
|
||||
--licenseFile PATH Path to file with license key for VictoriaMetrics Enterprise. See https://victoriametrics.com/products/enterprise/trial/ to obtain a trial license.
|
||||
--license STRING License key for VictoriaMetrics Enterprise. See https://victoriametrics.com/products/enterprise/trial/ to
|
||||
obtain a trial license.
|
||||
--licenseFile PATH Path to file with license key for VictoriaMetrics Enterprise. See
|
||||
https://victoriametrics.com/products/enterprise/trial/ to obtain a trial license.
|
||||
--license.forceOffline
|
||||
Whether to force offline verification for VictoriaMetrics Enterprise license key, which has been passed either via -license or via -licenseFile command-line flag. The issued
|
||||
license key must support offline verification feature. Contact info@victoriametrics.com if you need offline license verification.
|
||||
--loggerLevel {DEBUG,WARNING,FATAL,ERROR,INFO}
|
||||
Minimum level to log. Possible values: DEBUG, INFO, WARNING, ERROR, FATAL.
|
||||
--watch Watch config files for changes and trigger hot reloads. Watches the specified config file or directory for modifications, deletions, or additions. Upon detecting changes,
|
||||
triggers config reload. If new config validation fails, continues with previous valid config and state.
|
||||
--dryRun Validate only: parse + merge all YAML(s) and run schema checks, then exit. Does not require a license to run. Does not expose metrics, or launch vmanomaly service(s).
|
||||
Whether to force offline verification for VictoriaMetrics Enterprise license key, which has been passed either
|
||||
via -license or via -licenseFile command-line flag. The issued license key must support offline verification
|
||||
feature. Contact info@victoriametrics.com if you need offline license verification.
|
||||
--loggerLevel {DEBUG,INFO,WARNING,ERROR,FATAL}
|
||||
Minimum level to log. Possible values: {'DEBUG', 'INFO', 'WARNING', 'ERROR', 'FATAL'}.
|
||||
--watch Watch config files for changes and trigger hot reloads. Watches the specified config file or directory for
|
||||
modifications, deletions, or additions. Upon detecting changes, triggers config reload. If new config
|
||||
validation fails, continues with previous valid config and state.
|
||||
-configCheckInterval DURATION
|
||||
Interval for checking watched config files for content changes. Default: 30s.
|
||||
--dryRun Validate only: parse + merge all YAML(s) and run schema checks, then exit. Does not require a license to run.
|
||||
Does not expose metrics, or launch vmanomaly service(s).
|
||||
--outputSpec PATH Target location of .yaml output spec.
|
||||
```
|
||||
|
||||
{{% available_from "v1.29.5" anomaly %}} When `--watch` is enabled, config changes are detected by fixed-interval content polling instead of filesystem event delivery. The polling frequency is controlled by `-configCheckInterval` (default: `30s`). The same option can also be passed as `--configCheckInterval`, `--config.check.interval`, `--config-check-interval`, `--config_check_interval`, or in key-value form such as `configCheckInterval=30s`.
|
||||
|
||||
You can specify these options when running `vmanomaly` to fine-tune logging levels or handle licensing configurations, as per your requirements.
|
||||
|
||||
### Licensing
|
||||
@@ -122,7 +132,7 @@ Below are the steps to get `vmanomaly` up and running inside a Docker container:
|
||||
1. Pull Docker image:
|
||||
|
||||
```sh
|
||||
docker pull victoriametrics/vmanomaly:v1.29.4
|
||||
docker pull victoriametrics/vmanomaly:v1.29.6
|
||||
```
|
||||
|
||||
2. Create the license file with your license key.
|
||||
@@ -142,7 +152,7 @@ docker run -it \
|
||||
-v ./license:/license \
|
||||
-v ./config.yaml:/config.yaml \
|
||||
-p 8490:8490 \
|
||||
victoriametrics/vmanomaly:v1.29.4 \
|
||||
victoriametrics/vmanomaly:v1.29.6 \
|
||||
/config.yaml \
|
||||
--licenseFile=/license \
|
||||
--loggerLevel=INFO \
|
||||
@@ -159,7 +169,7 @@ docker run -it \
|
||||
-e VMANOMALY_DATA_DUMPS_DIR=/tmp/vmanomaly/data \
|
||||
-e VMANOMALY_MODEL_DUMPS_DIR=/tmp/vmanomaly/models \
|
||||
-p 8490:8490 \
|
||||
victoriametrics/vmanomaly:v1.29.4 \
|
||||
victoriametrics/vmanomaly:v1.29.6 \
|
||||
/config.yaml \
|
||||
--licenseFile=/license \
|
||||
--loggerLevel=INFO \
|
||||
@@ -172,7 +182,7 @@ services:
|
||||
# ...
|
||||
vmanomaly:
|
||||
container_name: vmanomaly
|
||||
image: victoriametrics/vmanomaly:v1.29.4
|
||||
image: victoriametrics/vmanomaly:v1.29.6
|
||||
# ...
|
||||
restart: always
|
||||
volumes:
|
||||
|
||||
@@ -315,7 +315,7 @@ docker run -it --rm \
|
||||
-e VMANOMALY_MCP_SERVER_URL=http://mcp-vmanomaly:8081/mcp \
|
||||
-p 8080:8080 \
|
||||
-p 8490:8490 \
|
||||
victoriametrics/vmanomaly:v1.29.4 \
|
||||
victoriametrics/vmanomaly:v1.29.6 \
|
||||
vmanomaly_config.yaml
|
||||
```
|
||||
|
||||
@@ -640,6 +640,17 @@ If the **results** look good and the **model configuration should be deployed in
|
||||
|
||||
## Changelog
|
||||
|
||||
### v1.7.1
|
||||
Released: 2026-06-11
|
||||
|
||||
vmanomaly version: [v1.29.5](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1295)
|
||||
|
||||
- FEATURE: Added bulk Apply/Decline actions for [Copilot](#ai-assistance) chat suggestions.
|
||||
|
||||
- BUGFIX: Fixed modal windows closing when the mouse is released outside the window during text selection.
|
||||
|
||||
- BUGFIX: Fixed tooltip hover behavior so tooltips do not disappear while the cursor moves into the hover content.
|
||||
|
||||
### v1.7.0
|
||||
Released: 2026-05-15
|
||||
|
||||
|
||||
@@ -143,11 +143,14 @@ server:
|
||||
|
||||
> This feature is better used in conjunction with [stateful service](https://docs.victoriametrics.com/anomaly-detection/components/settings/#state-restoration) to preserve the state of the models and schedulers between restarts and reuse what can be reused, thus avoiding unnecessary re-training of models, re-initialization of schedulers and re-reading of data.
|
||||
|
||||
{{% available_from "v1.25.0" anomaly %}} Service supports hot reload of configuration files, which allows for automatic reloading of configurations on config files change filesystem events without the need of explicit service restart. This can be enabled via the `--watch` [CLI argument](https://docs.victoriametrics.com/anomaly-detection/quickstart/#command-line-arguments). `vmanomaly_config_reload_enabled` flag in [self-monitoring metrics](https://docs.victoriametrics.com/anomaly-detection/components/monitoring/#startup-metrics) will be set to 1 (if enabled) or 0 (if disabled).
|
||||
{{% available_from "v1.25.0" anomaly %}} Service supports hot reload of configuration files, which allows for automatic reloading of configurations on config files change without the need of explicit service restart. This can be enabled via the `--watch` [CLI argument](https://docs.victoriametrics.com/anomaly-detection/quickstart/#command-line-arguments). `vmanomaly_config_reload_enabled` flag in [self-monitoring metrics](https://docs.victoriametrics.com/anomaly-detection/components/monitoring/#startup-metrics) will be set to 1 (if enabled) or 0 (if disabled).
|
||||
|
||||
> [!NOTE]
|
||||
> {{% deprecated_from "v1.29.5" anomaly %}} File system event-based hot reload has been deprecated in favor of content-based polling with configurable `-configCheckInterval` due to reliability issues with Kubernetes ConfigMap symlink rotations and other filesystems where event delivery can be inconsistent. If you were using file system event-based hot reload, please switch to content-based polling by enabling `--watch` flag and configuring `-configCheckInterval` as needed.
|
||||
|
||||
### How it works
|
||||
|
||||
It works by watching for file system events, such as modifications, creations, or deletions of `.yml|.yaml` files in the specified directories. When a change is detected, the service will attempt to reload the configuration files, rebuild the [global config](https://docs.victoriametrics.com/anomaly-detection/scaling-vmanomaly/#global-configuration) and reinitialize the components. If the reload is successful, the `vmanomaly_config_reloads_total` metric will be incremented for `status="success"` label, otherwise it will be incremented with `status="failure"` label and a respective error message on config validation failure(s) will be logged.
|
||||
It works by checking watched `.yml|.yaml` file contents in the specified files or directories on the configured interval `-configCheckInterval` (default is `30s`) {{% available_from "v1.29.5" anomaly %}}. When a content change is detected, the service will attempt to reload the configuration files after the existing debounce window, rebuild the [global config](https://docs.victoriametrics.com/anomaly-detection/scaling-vmanomaly/#global-configuration) and reinitialize the components. If the reload is successful, the `vmanomaly_config_reloads_total` metric will be incremented for `status="success"` label, otherwise it will be incremented with `status="failure"` label and a respective error message on config validation failure(s) will be logged.
|
||||
|
||||
> If the reload fails, the service will log an error message indicating the reason for the failure, and the **previous configuration will remain active until a successful reload occurs** to preserve the service's stability. This means that if there are errors in the new configuration, the service will continue to operate with the last valid configuration until the issues are resolved.
|
||||
|
||||
|
||||
@@ -449,9 +449,9 @@ models:
|
||||
|
||||
> The `decay` argument works only in combination with [online models](#online-models) like [`ZScoreOnlineModel`](#online-z-score) or [`OnlineQuantileModel`](#online-seasonal-quantile).
|
||||
|
||||
The `decay` {{% available_from "v1.23.0" anomaly %}} argument is used to control the (exponential) **decay factor** for online models, which determines how quickly the model adapts to new data. It is a float value between `0.0` and `1.0`, where:
|
||||
- `1.0` means no decay (the model treats all data equally, without giving more weight to recent data). This is the default value for backward compatibility.
|
||||
- Less than `1.0` means that the model will give more weight to recent data, effectively "forgetting" older data over time.
|
||||
The `decay` {{% available_from "v1.23.0" anomaly %}} argument is used to control the (exponential) **decay factor** for online models, which determines how quickly the model adapts to new data. It is a positive float value from `(0.0, 1.0]` interval, where:
|
||||
- Value `1.0` means no decay (the model treats all data points equally, without giving more weight to recent ones). This is the default value for backward compatibility.
|
||||
- Values less than `1.0` mean that the model will give more weight to recent data, effectively "forgetting" older data over time.
|
||||
|
||||
Roughly speaking, for the recent N datapoints model processes `decay` = `d` means that these datapoints will contribute to the model as [1 - d^X] percent of total importance, for example decay of
|
||||
- `0.99` means that 100 recent datapoints will contribute as [1 - 0.99^100] = 63.23% of total importance
|
||||
@@ -998,7 +998,7 @@ Here we use Isolation Forest implementation from `scikit-learn` [library](https:
|
||||
|
||||
* `class` (string) - model class name `"model.isolation_forest.IsolationForestMultivariateModel"` (or `isolation_forest_multivariate` with class alias support {{% available_from "v1.13.0" anomaly %}})
|
||||
|
||||
* `contamination` (float or string, optional) - The amount of contamination of the data set, i.e. the proportion of outliers in the data set. Used when fitting to define the threshold on the scores of the samples. Default value - "auto". Should be either `"auto"` or be in the range (0.0, 0.5].
|
||||
* `contamination` (float or string, optional) - The amount of contamination of the data set, i.e. the proportion of outliers in the data set. Used when fitting to define the threshold on the scores of the samples. Default value - "auto". Should be either `"auto"` or be in the range (0.0, 0.5]. {{% available_from "v1.29.5" anomaly %}} Numeric strings, such as `"0.01"`, are accepted, while invalid non-finite values, such as `nan`, `inf`, and `-inf`, are rejected during config validation.
|
||||
|
||||
* `seasonal_features` (list of string) - List of seasonality to encode through [cyclical encoding](https://towardsdatascience.com/cyclical-features-encoding-its-about-time-ce23581845ca), i.e. `dow` (day of week). **Introduced in [1.12.0](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1120)**.
|
||||
- Empty by default for backward compatibility.
|
||||
@@ -1265,7 +1265,7 @@ monitoring:
|
||||
Let's pull the docker image for `vmanomaly`:
|
||||
|
||||
```sh
|
||||
docker pull victoriametrics/vmanomaly:v1.29.4
|
||||
docker pull victoriametrics/vmanomaly:v1.29.6
|
||||
```
|
||||
|
||||
Now we can run the docker container putting as volumes both config and model file:
|
||||
@@ -1279,7 +1279,7 @@ docker run -it \
|
||||
-v $(PWD)/license:/license \
|
||||
-v $(PWD)/custom_model.py:/vmanomaly/model/custom.py \
|
||||
-v $(PWD)/custom.yaml:/config.yaml \
|
||||
victoriametrics/vmanomaly:v1.29.4 /config.yaml \
|
||||
victoriametrics/vmanomaly:v1.29.6 /config.yaml \
|
||||
--licenseFile=/license
|
||||
--watch
|
||||
```
|
||||
|
||||
@@ -10,12 +10,12 @@ sitemap:
|
||||
|
||||
- To use *vmanomaly*, part of the enterprise package, a license key is required. Obtain your key [here](https://victoriametrics.com/products/enterprise/trial/) for this tutorial or for enterprise use.
|
||||
- In the tutorial, we'll be using the following VictoriaMetrics components:
|
||||
- [VictoriaMetrics Single-Node](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) (v1.137.0)
|
||||
- [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/) (v1.137.0)
|
||||
- [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/) (v1.137.0)
|
||||
- [Grafana](https://grafana.com/) (v.10.2.1)
|
||||
- [VictoriaMetrics Single-Node](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) (v1.145.0)
|
||||
- [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/) (v1.145.0)
|
||||
- [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/) (v1.145.0)
|
||||
- [Grafana](https://grafana.com/) (v12.2.0)
|
||||
- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/)
|
||||
- [Node exporter](https://github.com/prometheus/node_exporter#node-exporter) (v1.7.0) and [Alertmanager](https://prometheus.io/docs/alerting/latest/alertmanager/) (v0.27.0)
|
||||
- [Node exporter](https://github.com/prometheus/node_exporter#node-exporter) (v1.9.1) and [Alertmanager](https://prometheus.io/docs/alerting/latest/alertmanager/) (v0.28.1)
|
||||
|
||||

|
||||
|
||||
@@ -323,7 +323,7 @@ Let's wrap it all up together into the `docker-compose.yml` file.
|
||||
services:
|
||||
vmagent:
|
||||
container_name: vmagent
|
||||
image: victoriametrics/vmagent:v1.137.0
|
||||
image: victoriametrics/vmagent:v1.145.0
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
@@ -340,7 +340,7 @@ services:
|
||||
|
||||
victoriametrics:
|
||||
container_name: victoriametrics
|
||||
image: victoriametrics/victoria-metrics:v1.137.0
|
||||
image: victoriametrics/victoria-metrics:v1.145.0
|
||||
ports:
|
||||
- 8428:8428
|
||||
volumes:
|
||||
@@ -356,7 +356,7 @@ services:
|
||||
|
||||
grafana:
|
||||
container_name: grafana
|
||||
image: grafana/grafana-oss:10.2.1
|
||||
image: grafana/grafana:12.2.0
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
@@ -373,7 +373,7 @@ services:
|
||||
|
||||
vmalert:
|
||||
container_name: vmalert
|
||||
image: victoriametrics/vmalert:v1.137.0
|
||||
image: victoriametrics/vmalert:v1.145.0
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
@@ -395,7 +395,7 @@ services:
|
||||
restart: always
|
||||
vmanomaly:
|
||||
container_name: vmanomaly
|
||||
image: victoriametrics/vmanomaly:v1.29.4
|
||||
image: victoriametrics/vmanomaly:v1.29.6
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
@@ -412,7 +412,7 @@ services:
|
||||
- "--licenseFile=/license"
|
||||
alertmanager:
|
||||
container_name: alertmanager
|
||||
image: prom/alertmanager:v0.27.0
|
||||
image: prom/alertmanager:v0.28.1
|
||||
volumes:
|
||||
- ./alertmanager.yml:/config/alertmanager.yml
|
||||
command:
|
||||
@@ -424,7 +424,7 @@ services:
|
||||
restart: always
|
||||
|
||||
node-exporter:
|
||||
image: quay.io/prometheus/node-exporter:v1.7.0
|
||||
image: quay.io/prometheus/node-exporter:v1.9.1
|
||||
container_name: node-exporter
|
||||
ports:
|
||||
- 9100:9100
|
||||
|
||||
@@ -6,45 +6,348 @@ build:
|
||||
sitemap:
|
||||
disable: true
|
||||
---
|
||||
**Objective**
|
||||
|
||||
Setup Victoria Metrics Cluster with support of multiple retention periods within one installation.
|
||||
> [VictoriaMetrics Enterprise](https://docs.victoriametrics.com/victoriametrics/enterprise/) supports specifying multiple retentions for distinct sets of time series and tenants. If you are an Enterprise user, [configure multiple retentions directly through retention filters](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#retention-filters) instead of following this guide.
|
||||
|
||||
**Enterprise Solution**
|
||||
This guide explains how to set up multiple retentions using an [open-source VictoriaMetrics Cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/).
|
||||
|
||||
[VictoriaMetrics Enterprise](https://docs.victoriametrics.com/victoriametrics/enterprise/) supports specifying multiple retentions
|
||||
for distinct sets of time series and [tenants](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#multitenancy)
|
||||
via [retention filters](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#retention-filters).
|
||||
## Overview
|
||||
|
||||
**Open Source Solution**
|
||||
VictoriaMetrics retains metrics by default for **1 month**. You can change data retention with the [`-retentionPeriod` command-line flag](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#retention), but this value applies to **all time series stored** on a given `vmstorage` node and cannot be customized per tenant or per metric in the open source version.
|
||||
|
||||
Community version of VictoriaMetrics supports only one retention period per `vmstorage` node via [-retentionPeriod](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#retention) command-line flag.
|
||||
The core idea of this guide is to run **separate logic groups of storages** (or even clusters) with individual `-retentionPeriod` settings, while still providing a single unified write and read path via vmagent and vmselect.
|
||||
|
||||
A multi-retention setup can be implemented by dividing a [victoriametrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/) into logical groups with different retentions.
|
||||
## Multi-Retention Architecture
|
||||
|
||||
Example:
|
||||
Setup should handle 3 different retention groups 3months, 1year and 3 years.
|
||||
Solution contains 3 groups of vmstorages + vminserts and one group of vmselects. Routing is done by [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/)
|
||||
by [splitting data streams](https://docs.victoriametrics.com/victoriametrics/vmagent/#splitting-data-streams-among-multiple-systems).
|
||||
The [-retentionPeriod](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#retention) sets how long to keep the metrics.
|
||||
To support multiple retentions with the open source version of VictoriaMetrics cluster, you can split the cluster into several logical groups of storage nodes. Each group is configured with a different `-retentionPeriod` and receives only the data that must follow that retention.
|
||||
|
||||
The diagram below shows a proposed solution
|
||||
Each storage group is connected to a separate vminsert, while a shared vmselect layer queries across all storage groups so that dashboards and alerts continue to see a single unified VictoriaMetrics backend.
|
||||
|
||||

|
||||
|
||||
**Implementation Details**
|
||||
In the example used throughout this guide, the cluster is divided into three groups:
|
||||
|
||||
1. Groups of vminserts A know about only vmstorages A and this is explicitly specified via `-storageNode` [configuration](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#cluster-setup).
|
||||
1. Groups of vminserts B know about only vmstorages B and this is explicitly specified via `-storageNode` [configuration](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#cluster-setup).
|
||||
1. Groups of vminserts C know about only vmstorages C and this is explicitly specified via `-storageNode` [configuration](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#cluster-setup).
|
||||
1. vmselect reads data from all vmstorage nodes via `-storageNode` [configuration](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#cluster-setup)
|
||||
with [deduplication](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#deduplication) setting equal to vmagent's scrape interval or minimum interval between collected samples.
|
||||
1. vmagent routes incoming metrics to the given set of `vminsert` nodes using relabeling rules specified at `-remoteWrite.urlRelabelConfig` [configuration](https://docs.victoriametrics.com/victoriametrics/relabeling/).
|
||||
- Group A: 3-month retention.
|
||||
- Group B: 1-year retention.
|
||||
- Group C: 3-year retention.
|
||||
|
||||
**Multi-Tenant Setup**
|
||||
Metrics are routed to the appropriate vminsert group by splitting data streams in vmagent, so each time series is sent to exactly one retention group instead of being replicated to all groups. See [Deploying vmagent](https://docs.victoriametrics.com/guides/guide-vmcluster-multiple-retention-setup/#step3) for an example of label‑based routing that implements this split. An optional [vmauth](https://docs.victoriametrics.com/guides/guide-vmcluster-multiple-retention-setup/#additional-enhancements) layer can be added on top to restrict access to specific sub‑clusters or tenants while still keeping a unified write and read path.
|
||||
|
||||
Every group of vmstorages can handle one tenant or multiple one. Different groups can have overlapping tenants. As vmselect reads from all vmstorage nodes, the data is aggregated on its level.
|
||||
## Implementing Multi-Retention on Kubernetes
|
||||
|
||||
**Additional Enhancements**
|
||||
In this section, we'll install and configure the components for a multi-retention deployment of the VictoriaMetrics cluster. See [Kubernetes monitoring with VictoriaMetrics Cluster](https://docs.victoriametrics.com/guides/k8s-monitoring-via-vm-cluster/) for details on running VictoriaMetrics in Kubernetes.
|
||||
|
||||
You can set up [vmauth](https://docs.victoriametrics.com/victoriametrics/vmauth/) for routing data to the given vminsert group depending on the needed retention.
|
||||
Run the following command to add the VictoriaMetrics Helm repository:
|
||||
|
||||
```shell
|
||||
helm repo add vm https://victoriametrics.github.io/helm-charts/
|
||||
helm repo update
|
||||
```
|
||||
|
||||
### Step 1: Deploying storage groups {#step1}
|
||||
|
||||
We'll create three storage groups. Each has a different retention period and disk size. Read [Understand Your Setup Size](https://docs.victoriametrics.com/guides/understand-your-setup-size/) to estimate how much space you will need for each group. The following table is shown as an example:
|
||||
|
||||
|
||||
| Group | Retention Period | Total disk size |
|
||||
|--------------|------------------|-----------------------|
|
||||
| `vmcluster-a` | 3 months (`3M`) | 80 Gi |
|
||||
| `vmcluster-b` | 1 year (`1Y`) | 300 Gi |
|
||||
| `vmcluster-c` | 3 years (`3Y`) | 900 Gi |
|
||||
|
||||
Create a Helm values file for Group A.
|
||||
|
||||
```shell
|
||||
cat <<EOF > vmcluster-a.yaml
|
||||
vmstorage:
|
||||
enabled: true
|
||||
replicaCount: 1
|
||||
persistence:
|
||||
size: 80Gi
|
||||
extraArgs:
|
||||
retentionPeriod: 3M
|
||||
podLabels:
|
||||
retention-group: a
|
||||
|
||||
vminsert:
|
||||
enabled: true
|
||||
podLabels:
|
||||
retention-group: a
|
||||
|
||||
vmselect:
|
||||
enabled: false
|
||||
EOF
|
||||
```
|
||||
|
||||
The values file above creates vminsert and vmstorage services while turning off vmselect, which we'll deploy separately. The `retentionPeriod` flag configures how long data is kept in this group.
|
||||
|
||||
Create the values files for Group B and Group C:
|
||||
|
||||
```shell
|
||||
cat <<EOF > vmcluster-b.yaml
|
||||
vmstorage:
|
||||
enabled: true
|
||||
replicaCount: 1
|
||||
persistence:
|
||||
size: 300Gi
|
||||
extraArgs:
|
||||
retentionPeriod: 1y
|
||||
podLabels:
|
||||
retention-group: b
|
||||
|
||||
vminsert:
|
||||
enabled: true
|
||||
podLabels:
|
||||
retention-group: b
|
||||
|
||||
vmselect:
|
||||
enabled: false
|
||||
EOF
|
||||
|
||||
|
||||
cat <<EOF > vmcluster-c.yaml
|
||||
vmstorage:
|
||||
enabled: true
|
||||
replicaCount: 1
|
||||
persistence:
|
||||
size: 900Gi
|
||||
extraArgs:
|
||||
retentionPeriod: 3y
|
||||
podLabels:
|
||||
retention-group: c
|
||||
|
||||
vminsert:
|
||||
enabled: true
|
||||
podLabels:
|
||||
retention-group: c
|
||||
|
||||
vmselect:
|
||||
enabled: false
|
||||
EOF
|
||||
```
|
||||
|
||||
Deploy the three storage groups with:
|
||||
|
||||
```shell
|
||||
helm upgrade --install vmcluster-a vm/victoria-metrics-cluster -f vmcluster-a.yaml
|
||||
helm upgrade --install vmcluster-b vm/victoria-metrics-cluster -f vmcluster-b.yaml
|
||||
helm upgrade --install vmcluster-c vm/victoria-metrics-cluster -f vmcluster-c.yaml
|
||||
|
||||
# Wait for all storage pods to be ready
|
||||
kubectl rollout status statefulset -l app.kubernetes.io/instance=vmcluster-a
|
||||
kubectl rollout status statefulset -l app.kubernetes.io/instance=vmcluster-b
|
||||
kubectl rollout status statefulset -l app.kubernetes.io/instance=vmcluster-c
|
||||
```
|
||||
|
||||
### Step 2: Deploying vmselect {#step2}
|
||||
|
||||
Next, we'll deploy a vmselect service to route queries to the storage groups.
|
||||
|
||||
Create a Helm values file with:
|
||||
|
||||
```shell
|
||||
cat <<EOF >vmselect.yaml
|
||||
vmstorage:
|
||||
enabled: false
|
||||
|
||||
vminsert:
|
||||
enabled: false
|
||||
|
||||
vmselect:
|
||||
enabled: true
|
||||
replicaCount: 1
|
||||
suppressStorageFQDNsRender: true
|
||||
extraArgs:
|
||||
# Each list item is a single -storageNode flag. In this example, there is
|
||||
# one vmstorage pod per retention group, so each entry contains a single host.
|
||||
# If you run multiple pods per group, list them as comma-separated hosts
|
||||
# in the same -storageNode value.
|
||||
#
|
||||
# The FQDN format is:
|
||||
# <pod>.<svc>.default.svc
|
||||
# where pod = <release>-victoria-metrics-cluster-vmstorage-<N>
|
||||
# and svc = <release>-victoria-metrics-cluster-vmstorage
|
||||
storageNode:
|
||||
- "vmcluster-a-victoria-metrics-cluster-vmstorage-0.vmcluster-a-victoria-metrics-cluster-vmstorage.default.svc:8401"
|
||||
- "vmcluster-b-victoria-metrics-cluster-vmstorage-0.vmcluster-b-victoria-metrics-cluster-vmstorage.default.svc:8401"
|
||||
- "vmcluster-c-victoria-metrics-cluster-vmstorage-0.vmcluster-c-victoria-metrics-cluster-vmstorage.default.svc:8401"
|
||||
EOF
|
||||
```
|
||||
|
||||
Let's break down the file above:
|
||||
|
||||
- Deploys vmselect as a separate Helm release.
|
||||
- Disables vminsert and vmstorage as these services were already deployed in Step 1.
|
||||
- `suppressStorageFQDNsRender: true` turns off automatic FQDN generation for storage nodes. By default, the Helm chart auto-generates `-storageNodes` flags, but since `vmstorage` has been disabled, we need to supply them manually in `extraArgs`.
|
||||
- In `extraArgs.storageNode:` we define the vmstorage endpoints for queries. On querying, vmselect merges results across all the specified vmstorages to provide a unified view of the data.
|
||||
|
||||
Deploy the `vmselect` release with:
|
||||
|
||||
```shell
|
||||
helm upgrade --install vmselect vm/victoria-metrics-cluster -f vmselect.yaml
|
||||
```
|
||||
|
||||
### Step 3: Deploying vmagent {#step3}
|
||||
|
||||
We'll use `vmagent` to route incoming metrics to the correct retention group. For example, we can use a `retention` label for mapping metrics to storage groups in the following way:
|
||||
|
||||
| `retention` label | Storage Group |
|
||||
|-------------------|--------------|
|
||||
| `"3mo"` | `vmcluster-a` |
|
||||
| `"1yr"` | `vmcluster-b` |
|
||||
| `"3yr"` | `vmcluster-c` |
|
||||
|
||||
|
||||
Create the values file for vmagent:
|
||||
|
||||
```shell
|
||||
cat <<EOF >vmagent.yaml
|
||||
service:
|
||||
enabled: true
|
||||
remoteWrite:
|
||||
# Group A: receives metrics with retention="3mo"
|
||||
- url: http://vmcluster-a-victoria-metrics-cluster-vminsert:8480/insert/0/prometheus/api/v1/write
|
||||
urlRelabelConfig:
|
||||
- if: '{retention="3mo"}'
|
||||
action: keep
|
||||
# Group B: receives metrics with retention="1yr"
|
||||
- url: http://vmcluster-b-victoria-metrics-cluster-vminsert:8480/insert/0/prometheus/api/v1/write
|
||||
urlRelabelConfig:
|
||||
- if: '{retention="1yr"}'
|
||||
action: keep
|
||||
# Group C: receives metrics with retention="3yr"
|
||||
- url: http://vmcluster-c-victoria-metrics-cluster-vminsert:8480/insert/0/prometheus/api/v1/write
|
||||
urlRelabelConfig:
|
||||
- if: '{retention="3yr"}'
|
||||
action: keep
|
||||
EOF
|
||||
```
|
||||
|
||||
> Metrics without a matching `retention` label are silently dropped by the `keep` rules. You must ensure that every metric is labeled, or use a different routing configuration.
|
||||
|
||||
Now deploy the vmagent release:
|
||||
|
||||
```shell
|
||||
helm upgrade --install vmagent vm/victoria-metrics-agent -f vmagent.yaml
|
||||
```
|
||||
|
||||
Wait for vmagent to become ready:
|
||||
|
||||
```shell
|
||||
kubectl rollout status deploy/vmagent-victoria-metrics-agent
|
||||
```
|
||||
|
||||
### Step 4: Verification
|
||||
|
||||
We can send test data to verify that the data is flowing to the correct storage group.
|
||||
|
||||
First, port-forward vmagent and vmselect:
|
||||
|
||||
```shell
|
||||
VMAGENT_SVC=$(kubectl get svc -l app.kubernetes.io/instance=vmagent -o jsonpath='{.items[0].metadata.name}')
|
||||
kubectl port-forward "svc/$VMAGENT_SVC" 8429 &
|
||||
|
||||
VMSELECT_SVC=$(kubectl get svc -l app.kubernetes.io/instance=vmselect -o jsonpath='{.items[0].metadata.name}')
|
||||
kubectl port-forward "svc/$VMSELECT_SVC" 8481 &
|
||||
```
|
||||
|
||||
Send test metrics directly to vmagent's HTTP endpoint to exercise all three retention labels:
|
||||
|
||||
```shell
|
||||
POD=$(kubectl get pod -l app.kubernetes.io/instance=vmagent -o jsonpath='{.items[0].metadata.name}')
|
||||
|
||||
for retention in 3mo 1yr 3yr; do
|
||||
kubectl exec "$POD" -- wget -qO- --post-data="test_routing{retention=\"${retention}\"} 1.0" \
|
||||
"http://127.0.0.1:8429/api/v1/import/prometheus"
|
||||
done
|
||||
```
|
||||
|
||||
Query the data back from vmselect (it may take around 30-60 seconds for new data to be available for queries):
|
||||
|
||||
```shell
|
||||
for retention in 3mo 1yr 3yr; do
|
||||
echo "-> retention=${retention}"
|
||||
curl -s "http://localhost:8481/select/0/prometheus/api/v1/query" \
|
||||
--data-urlencode "query=test_routing{retention=\"${retention}\"}"
|
||||
echo
|
||||
done
|
||||
```
|
||||
|
||||
You can also check that vmagent is forwarding data to all three groups:
|
||||
|
||||
```shell
|
||||
curl -s http://localhost:8429/metrics | grep vmagent_remotewrite_blocks_sent_total
|
||||
```
|
||||
|
||||
Each `url="N:secret-url"` corresponds to one `remoteWrite` entry (N=1 for Group A, N=2 for Group B, N=3 for Group C). Non-zero values confirm data is flowing.
|
||||
|
||||
## Alternative Routing by Existing Labels
|
||||
|
||||
The example setup above relies on a synthetic `retention` label to exist in every incoming metric.
|
||||
|
||||
If having a `retention` label in every metric isn't practical, you can, as an alternative, rely on existing labels to map data to the correct storage group.
|
||||
|
||||
The following example configures vmagent to route metrics based on the `environment` and `team` labels:
|
||||
|
||||
```yaml
|
||||
# vmagent.yaml
|
||||
remoteWrite:
|
||||
# send dev and staging data to Group A
|
||||
- url: "http://vmcluster-a-victoria-metrics-cluster-vminsert:8480/insert/0/prometheus/api/v1/write"
|
||||
urlRelabelConfig:
|
||||
- if: {environment=~"dev|staging"}
|
||||
action: keep
|
||||
# send prod data to Group B
|
||||
- url: "http://vmcluster-b-victoria-metrics-cluster-vminsert:8480/insert/0/prometheus/api/v1/write"
|
||||
urlRelabelConfig:
|
||||
- if: {environment=~"prod|production"}
|
||||
action: keep
|
||||
# send data from Infra and SRE teams to Group C
|
||||
- url: "http://vmcluster-c-victoria-metrics-cluster-vminsert:8480/insert/0/prometheus/api/v1/write"
|
||||
urlRelabelConfig:
|
||||
- if: {team=~"infra|sre"}
|
||||
action: keep
|
||||
```
|
||||
|
||||
> Metrics that do not match any of the `keep` rules are dropped in the configuration above.
|
||||
|
||||
## Additional Enhancements
|
||||
|
||||
You can set up [vmauth](https://docs.victoriametrics.com/victoriametrics/vmauth/) to route data to the specified vminsert group based on the required retention or to restrict which data different users can query.
|
||||
|
||||
The following [`-auth.config`](https://docs.victoriametrics.com/victoriametrics/vmauth/#quick-start) example exposes the same vmselect backend via vmauth with two users using basic auth:
|
||||
|
||||
- `admin`: can query **all** data across all retention groups.
|
||||
- `dev`: can query **only** time series that have `team="dev"` label, enforced via the `extra_label` query argument.
|
||||
|
||||
```yaml
|
||||
users:
|
||||
# User with access to all data across all retention groups
|
||||
- username: "admin"
|
||||
password: "foo"
|
||||
url_map:
|
||||
- src_paths:
|
||||
- "/api/v1/query"
|
||||
- "/api/v1/query_range"
|
||||
- "/api/v1/series"
|
||||
- "/api/v1/labels"
|
||||
- "/api/v1/label/.+/values"
|
||||
# vmselect service that aggregates all vmstorage groups
|
||||
url_prefix: "http://vmselect-victoria-metrics-cluster-vmselect:8481/select/0/prometheus"
|
||||
|
||||
# User restricted to Dev team data only
|
||||
- username: "dev"
|
||||
password: "bar"
|
||||
url_map:
|
||||
- src_paths:
|
||||
- "/api/v1/query"
|
||||
- "/api/v1/query_range"
|
||||
- "/api/v1/series"
|
||||
- "/api/v1/labels"
|
||||
- "/api/v1/label/.+/values"
|
||||
# Same vmselect backend, but enforce label filter at query time
|
||||
# by adding extra_label=team=dev to every proxied request
|
||||
url_prefix: "http://vmselect-victoria-metrics-cluster-vmselect:8481/select/0/prometheus/?extra_label=team=dev"
|
||||
```
|
||||
|
||||
This is useful for restricting access by team, environment, or tenant without changing the underlying storage topology.
|
||||
|
||||
@@ -19,6 +19,7 @@ See also [case studies](https://docs.victoriametrics.com/victoriametrics/casestu
|
||||
* [Datanami: Why Roblox Picked VictoriaMetrics for Observability Data Overhaul](https://www.hpcwire.com/bigdatawire/2023/05/30/why-roblox-picked-victoriametrics-for-observability-data-overhaul/)
|
||||
* [Cloudflare: Introducing notifications for HTTP Traffic Anomalies](https://blog.cloudflare.com/introducing-http-traffic-anomalies-notifications/)
|
||||
* [Grammarly: Better, Faster, Cheaper: How Grammarly Improved Monitoring by Over 10x with VictoriaMetrics](https://www.grammarly.com/blog/engineering/monitoring-with-victoriametrics/)
|
||||
* [Xata: How we rebuilt PostgreSQL branch metrics on VictoriaMetrics, per cell](https://xata.io/blog/how-we-rebuilt-postgresql-branch-metrics-on-victoriametrics-per-cell)
|
||||
* [CERN: CMS monitoring R&D: Real-time monitoring and alerts](https://indico.cern.ch/event/877333/contributions/3696707/attachments/1972189/3281133/CMS_mon_RD_for_opInt.pdf)
|
||||
* [CERN: The CMS monitoring infrastructure and applications](https://arxiv.org/pdf/2007.03630.pdf)
|
||||
* [Forbes: The (Almost) Infinitely Scalable Open Source Monitoring Dream](https://www.forbes.com/sites/adrianbridgwater/2022/08/16/the-almost-infinitely-scalable-open-source-monitoring-dream/)
|
||||
|
||||
@@ -28,7 +28,7 @@ If you like VictoriaMetrics and want to contribute, then it would be great:
|
||||
## Issues
|
||||
|
||||
When making a new issue, make sure to create no duplicates. Use GitHub search to find whether similar issues exist already.
|
||||
The new issue should be written in English and contain concise description of the problem and environment where it exists.
|
||||
The new issue should be written in English and contain a concise description of the problem and the environment where it exists.
|
||||
We'd very much prefer to have a specific use-case included in the description, since it could have workaround or alternative solutions.
|
||||
|
||||
When looking for an issue to contribute, always prefer working on [bugs](https://github.com/VictoriaMetrics/VictoriaMetrics/issues?q=is%3Aopen+is%3Aissue+label%3Abug)
|
||||
@@ -48,7 +48,7 @@ We use [labels](https://docs.github.com/en/issues/using-labels-and-milestones-to
|
||||
1. `need more info`, assigned to issues that require elaboration from the issue creator.
|
||||
For example, if we weren't able to reproduce the reported bug based on the ticket description then we ask additional
|
||||
questions which could help to reproduce the issue and add `need more info` label. This label helps other maintainers
|
||||
to understand that this issue wasn't forgotten but waits for the feedback from user.
|
||||
to understand that this issue wasn't forgotten but waits for the feedback from the user.
|
||||
1. `completed`, assigned to issues that required code changes and those changes were merged to upstream, but not released yet.
|
||||
Once a release is made, maintainers go through all labeled issues, leave a comment about the new release, and close the issue.
|
||||
1. `vmui`, assigned to issues related to [vmui](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui) or [VictoriaLogs webui](https://docs.victoriametrics.com/victorialogs/querying/#web-ui)
|
||||
@@ -63,32 +63,31 @@ Pull requests requirements:
|
||||
1. Don't use `master` branch for making PRs, as it makes it impossible for reviewers to modify the changes.
|
||||
1. All commits need to be [signed](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits).
|
||||
1. Pull request title should be prefixed with `<dir>/<component>:` to show what component has been changed, i.e. `app/vmalert: fix...`.
|
||||
Pull request description should contain clear and concise description of what was done, why it is needed and for what purpose.
|
||||
Pull request description should contain a clear and concise description of what was done, why it is needed and for what purpose.
|
||||
Use clear language, so reviewers can quickly understand the change and its impact.
|
||||
1. A link to the issue(s) related to the change, if any. Use `Fixes [issue link]` if the PR resolves the issue, or `Related to [issue link]` for reference.
|
||||
1. Tests proving that the change is effective. Tests are expected for non-trivial new functionality or non-trivial modifications.
|
||||
Bug fixes must include tests unless a maintainer explicitly agrees otherwise.
|
||||
See [this style guide](https://itnext.io/f-tests-as-a-replacement-for-table-driven-tests-in-go-8814a8b19e9e) for tests.
|
||||
To run tests and code checks locally, execute commands `make test-full` and `make check-all`.
|
||||
See [this style guide](https://itnext.io/f-tests-as-a-replacement-for-table-driven-tests-in-go-8814a8b19e9e) for tests. See [this section](#testing) for how to run tests.
|
||||
1. Try to not extend the scope of the pull requests outside the issue, do not make unrelated changes.
|
||||
1. Update [docs](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/docs) if needed. For example, adding a new flag or changing behavior of existing flags or features
|
||||
1. Update [docs](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/docs) if needed. For example, adding a new flag or changing the behavior of existing flags or features
|
||||
requires reflecting these changes in the documentation. For new features add `{{%/* available_from "#" */%}}` shortcode to the documentation.
|
||||
It will be later automatically replaced with an actual release version.
|
||||
1. A line in the [changelog](https://docs.victoriametrics.com/victoriametrics/changelog/#tip) mentioning the change and related issue in a way
|
||||
that would be clear to other readers even if they don't have the full context.
|
||||
1. Avoid modifying code in the `/vendor` folder manually, even when the vendored package originates are from the VictoriaMetrics GitHub organization.
|
||||
1. Avoid modifying code in the `/vendor` folder manually, even when the vendored package originates from the VictoriaMetrics GitHub organization.
|
||||
For instance, VictoriaLogs vendors packages under the `/lib` folder from VictoriaMetrics, and VictoriaTraces vendors the `/lib/logstorage` package from VictoriaLogs.
|
||||
Submit a pull request to the upstream repository first. Afterward, a separate pull request can be opened to update the version of the vendored folder in downstream repository.
|
||||
Submit a pull request to the upstream repository first. Afterward, a separate pull request can be opened to update the version of the vendored folder in the downstream repository.
|
||||
* For common packages, the vendored package can be updated with this command: `go get <dependency>@vX.Y.Z`.
|
||||
* For VictoriaMetrics packages, use `go get <dependency>@canonical_commit_hash`.
|
||||
Finally, run `go mod tidy` and `go mod vendor` to update `go.mod`, `go.sum`, and `/vendor`.
|
||||
1. Ping reviewers who you think have the best expertise on the matter.
|
||||
|
||||
See good example of a [pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/6487).
|
||||
See a good example of a [pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/6487).
|
||||
|
||||
## Merging Pull Request
|
||||
|
||||
The person who merges the Pull Request is responsible for satisfying requirements below:
|
||||
The person who merges the Pull Request is responsible for satisfying the requirements below:
|
||||
|
||||
1. Make sure that PR satisfies [Pull Request checklist](https://docs.victoriametrics.com/victoriametrics/contributing/#pull-request-checklist),
|
||||
it is approved by at least one reviewer, all CI checks are green.
|
||||
@@ -97,9 +96,9 @@ The person who merges the Pull Request is responsible for satisfying requirement
|
||||
1. If applicable, cherry-pick the change to [LTS release lines](https://docs.victoriametrics.com/victoriametrics/lts-releases/)
|
||||
and mention in the PR comment what was or wasn't cherry-picked.
|
||||
1. Update related issues with a meaningful message of what has changed and when it will be
|
||||
released. _This helps users to understand the change without reading PR._
|
||||
released. _This helps users to understand the change without reading the PR._
|
||||
1. Add label `completed` to related issues.
|
||||
1. Do not close related tickets until release is made. If ticket was auto-closed by GitHub or user - re-open it.
|
||||
1. Do not close related tickets until the release is made. If the ticket was auto-closed by GitHub or a user - re-open it.
|
||||
|
||||
## KISS principle
|
||||
|
||||
@@ -115,9 +114,9 @@ We are open to third-party pull requests provided they follow [KISS design princ
|
||||
- Minimize the number of moving parts in the distributed system.
|
||||
- Avoid automated decisions, which may hurt cluster availability, consistency, performance or debuggability.
|
||||
|
||||
Adhering to `KISS` principle, simplifies the resulting code and architecture so it can be reviewed, understood and debugged by a wider audience.
|
||||
Adhering to the `KISS` principle, simplifies the resulting code and architecture so it can be reviewed, understood and debugged by a wider audience.
|
||||
|
||||
Due to `KISS`, [cluster version of VictoriaMetrics](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/) has none of the following "features" popular in distributed computing world:
|
||||
Due to `KISS`, [cluster version of VictoriaMetrics](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/) has none of the following "features" popular in distributed computing:
|
||||
|
||||
- Fragile gossip protocols. See [failed attempt in Thanos](https://github.com/improbable-eng/thanos/blob/030bc345c12c446962225221795f4973848caab5/docs/proposals/completed/201809_gossip-removal.md).
|
||||
- Hard-to-understand-and-implement-properly [Paxos protocols](https://www.quora.com/In-distributed-systems-what-is-a-simple-explanation-of-the-Paxos-algorithm).
|
||||
@@ -126,3 +125,17 @@ Due to `KISS`, [cluster version of VictoriaMetrics](https://docs.victoriametrics
|
||||
- Automatic cluster resizing, which may cost you a lot of money if improperly configured.
|
||||
- Automatic discovering and addition of new nodes in the cluster, which may mix data between dev and prod clusters :)
|
||||
- Automatic leader election, which may result in split brain disaster on network errors.
|
||||
|
||||
## Testing
|
||||
|
||||
We recommend running the following sequence of checks and tests before submitting a pull request:
|
||||
```sh
|
||||
# run static checks
|
||||
make check-all
|
||||
|
||||
# run unit test
|
||||
make test-full
|
||||
|
||||
# run integration tests
|
||||
make apptest
|
||||
```
|
||||
@@ -26,8 +26,30 @@ See also [LTS releases](https://docs.victoriametrics.com/victoriametrics/lts-rel
|
||||
|
||||
## tip
|
||||
|
||||
* BUGFIX: [stream aggregation](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/): fix issue with producing aggregated samples with identical timestamps between flushes. See PR [#10808](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10808) for details.
|
||||
* BUGFIX: [stream aggregation](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/): fix `increase` and `increase_prometheus` outputs producing inflated values when old samples update the baseline across interval boundaries with `ignore_old_samples: true` or `enable_windows: true`.
|
||||
* FEATURE: all VictoriaMetrics components: add `-http.header.disableServerHostname` command-line flag for disabling the `X-Server-Hostname` HTTP response header. See [#11067](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11067). Thanks to @zasdaym for contribution.
|
||||
* FEATURE: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) and `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): log calls to [/api/v1/admin/tsdb/delete_series](https://docs.victoriametrics.com/victoriametrics/url-examples/#apiv1admintsdbdelete_series) API handler. This should help to identify events of metrics deletion from the database. See [#11104](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11104).
|
||||
* FEATURE: [vmctl](https://docs.victoriametrics.com/victoriametrics/vmctl/): add `-vm-headers` and `-vm-bearer-token` flags for authenticating requests to the VictoriaMetrics import destination. The flags are available in `opentsdb`, `influx`, `remote-read`, `prometheus`, `mimir`, and `thanos` vmctl sub-commands. See [#8897](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8897).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui): add the `last` value to graph legend statistics. See [#10759](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10759).
|
||||
* FEATURE: [stream aggregation](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/): expose `vm_streamaggr_dedup_dropped_samples_total` to allow tracking dropped old samples during [deduplication](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/#deduplication).
|
||||
* FEATURE: [stream aggregation](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/): use the aggregation rule interval as the default [staleness_interval](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/#staleness) instead of `2*interval`, to reduce spikes when there are gaps between received samples. See [#11102](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11102).
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): add a new flag `-remoteWrite.inmemoryQueues` to prioritize recently ingested data over historical data stored at file-based [persistent queue](https://docs.victoriametrics.com/victoriametrics/vmagent/#on-disk-persistence-and-data-processing-order). See [#8833](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8833)
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): add `-promscrape.cluster.shardByLabels` command-line flag for selecting target labels used for sharding scrape targets among `vmagent` instances in cluster mode. See [#11044](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11044).
|
||||
* FEATURE: [vmctl](https://docs.victoriametrics.com/victoriametrics/vmctl/): add `-vm-headers` and `-vm-bearer-token` flags for authenticating requests to the VictoriaMetrics import destination. The flags are available in `opentsdb`, `influx`, `remote-read`, `prometheus`, `mimir`, and `thanos` vmctl sub-commands. See [#8897](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8897).
|
||||
* FEATURE: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) and `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): log calls to [/api/v1/admin/tsdb/delete_series](https://docs.victoriametrics.com/victoriametrics/url-examples/#apiv1admintsdbdelete_series) API handler. This should help to identify events of metrics deletion from the database. See [#11104](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11104).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui): add the `last` value to graph legend statistics. See [#10759](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10759).
|
||||
|
||||
* BUGFIX: [enterprise](https://docs.victoriametrics.com/enterprise/) [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) and `vmstorage` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): properly expose metric `vm_retention_filters_partitions_scheduled_rows`. See [#11138](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11138)
|
||||
* BUGFIX: [stream aggregation](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/): fix issue with producing aggregated samples with identical timestamps between flushes. See [#10808](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10808).
|
||||
* BUGFIX: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): fix potential corruption of remote-write metadata `Unit` values. See [#11120](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11120). Thanks for @fxrlv for the contribution.
|
||||
* BUGFIX: [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/),[vmauth](https://docs.victoriametrics.com/victoriametrics/vmauth/),[vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/) and [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/): fix rare unbounded shutdown delay when config reload takes longer than `-configCheckInterval`. See [#11107](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11107). Thanks to @PleasingFungus for contribution.
|
||||
* BUGFIX: [vmbackup](https://docs.victoriametrics.com/vmbackup/), [vmbackupmanager](https://docs.victoriametrics.com/victoriametrics/vmbackupmanager/): do not fail backup list if directory is absent while using `fs://` destination to align with other protocols. See [6c3c548d](https://github.com/VictoriaMetrics/VictoriaMetrics/commit/6c3c548ddb0385b749e731f52276f130e2a4e4a8).
|
||||
* BUGFIX: [vmctl](https://docs.victoriametrics.com/victoriametrics/vmctl/): push metrics to configured `-pushmetrics.url` on shutdown when migration fails. Previously, metrics were not pushed if vmctl exited with an error. See [#11081](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11081). Thanks to @zasdaym for contribution.
|
||||
* BUGFIX: [vmrestore](https://docs.victoriametrics.com/victoriametrics/vmrestore/): disallow restoring parts outside the configured `-storageDataPath` directory. See [710c920d](https://github.com/VictoriaMetrics/VictoriaMetrics/commit/710c920d6083327042a309e449fae4383617d817).
|
||||
* BUGFIX: `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): correctly apply long tenant filters. Previously, such filters could be truncated, causing tenants to be matched incorrectly. See [#11096](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11096). Thanks for @fxrlv for the contribution.
|
||||
* BUGFIX: `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): fix corrupted metrics metadata when a response contains multiple rows. See [#11115](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11115). Thanks for @fxrlv for the contribution.
|
||||
* BUGFIX: `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): don't cache empty responses for tenant IDs discovery during [multitenant queries](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#multitenant-reads). This problem was visible during integration tests when multitenant queries were executed before the first ingestion happened. See [#10982](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10982)
|
||||
* BUGFIX: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) and `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): properly escape `metricFamilyName` at metrics metadata response. See [#11129](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11129). Thanks for @fxrlv for the contribution.
|
||||
* BUGFIX: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) and `vmstorage` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): prevent more cases of panic during directory deletion on `NFS`-based mounts. See [#11060](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11060).
|
||||
|
||||
## [v1.145.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.145.0)
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ It is better to substitute the slow recording rule with the following [stream ag
|
||||
outputs: [rate_sum]
|
||||
```
|
||||
|
||||
> Field `interval` should be set to a value at least several times higher than the matched metrics collection interval.
|
||||
> It is recommended to set the `interval` field to a value at least 2 times the matched metrics collection interval.
|
||||
|
||||
This stream aggregation generates `http_request_duration_seconds_bucket:1m_without_instance_rate_sum` output series according to [output metric naming](#output-metric-names).
|
||||
Then these series can be used in [alerting rules](https://docs.victoriametrics.com/victoriametrics/vmalert/#alerting-rules):
|
||||
@@ -396,7 +396,7 @@ before sending them to the configured `-remoteWrite.url`. The deduplication can
|
||||
|
||||
Labels can be dropped before deduplication is applied. See [these docs](#dropping-unneeded-labels).
|
||||
|
||||
Stream aggregation deduplication is applied before aggregation rules, so duplicate samples are dropped before aggregation.
|
||||
Stream aggregation deduplication is applied before aggregation rules, so duplicate samples are dropped before aggregation. The dropped old samples can be tracked with the `vm_streamaggr_dedup_dropped_samples_total` metric.
|
||||
|
||||
# Relabeling
|
||||
|
||||
@@ -444,7 +444,9 @@ outside the current [aggregation interval](https://docs.victoriametrics.com/vict
|
||||
|
||||
- To enable [aggregation windows](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/#aggregation-windows).
|
||||
|
||||
The dropped old samples can be tracked with the `vm_streamaggr_ignored_samples_total{reason="too_old"}` metric.
|
||||
- To enable [deduplication](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/#deduplication).
|
||||
|
||||
The dropped old samples can be tracked with the `vm_streamaggr_ignored_samples_total{reason="too_old"}` and `vm_streamaggr_dedup_dropped_samples_total` metrics.
|
||||
|
||||
## Ignore aggregation intervals on start
|
||||
|
||||
@@ -642,9 +644,9 @@ See also [why you shouldn't put an aggregator behind a load balancer](https://do
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
- [Unexpected spikes for `total` or `increase` outputs](#staleness).
|
||||
- [Unexpected spikes for `total` or `increase` outputs](#data-delay-and-staleness).
|
||||
- [Excessively large values for `total*`, `increase*`, and `rate*` outputs](#counter-resets).
|
||||
- [Lower than expected values for `total_prometheus` and `increase_prometheus` outputs](#staleness).
|
||||
- [Lower than expected values for `total_prometheus` and `increase_prometheus` outputs](#data-delay-and-staleness).
|
||||
- [High memory usage and CPU usage](#high-resource-usage).
|
||||
- [Unexpected results in vmagent cluster mode](#cluster-mode).
|
||||
- [Inaccurate aggregation results for histograms](#aggregation-windows)
|
||||
@@ -677,11 +679,19 @@ the following settings:
|
||||
|
||||
If counter-specific outputs, such as `total*`, `rate*`, and `increase*`, produce values that are significantly higher than anticipated, then check the `vm_streamaggr_counter_resets_total` metric. This metric increments each time when [counter reset event](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#counter) happens and could be caused by duplication or collision of raw samples. If you observe duplication or collision, try solving this problem by either fixing the source of these metrics or by [deduplicating](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/#deduplication) these samples before aggregation.
|
||||
|
||||
## Staleness
|
||||
## Data delay and staleness {#staleness}
|
||||
|
||||
The following outputs track the last seen per-series values in order to properly calculate output values:
|
||||
Stream aggregation processes input samples in a streaming manner and flushes results once per specified `interval`. Because of this, aggregation results can be heavily affected by data delays (see `vm_streamaggr_samples_lag_seconds_bucket` metric).
|
||||
|
||||
In particular:
|
||||
1. Stream aggregation won't produce results if input samples are delayed for multiple aggregation intervals, causing gaps in the output.
|
||||
2. Delayed and out-of-order samples can inflate or skew correctness of aggregation results.
|
||||
|
||||
Dropping delayed samples can result in missed observations in the results, while keeping delayed samples may inflate the results. It is up to the user to decide what they prefer in the produced results:
|
||||
1. If you prefer consistency in aggregation results and do not want delayed data to affect the next aggregation window, drop all potentially delayed samples via [ignore_old_samples](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/#ignoring-old-samples).
|
||||
2. If you prefer to have the accumulated changes from delayed data reflected in aggregation windows after the delay, increase `staleness_interval` in the [stream aggregation config](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/configuration/#stream-aggregation-config).
|
||||
This is especially important for outputs that track the last seen per-series values in order to properly calculate output values:
|
||||
|
||||
- [histogram_bucket](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/configuration/#histogram_bucket)
|
||||
- [increase](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/configuration/#increase)
|
||||
- [increase_prometheus](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/configuration/#increase_prometheus)
|
||||
- [rate_avg](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/configuration/#rate_avg)
|
||||
@@ -689,21 +699,19 @@ The following outputs track the last seen per-series values in order to properly
|
||||
- [total](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/configuration/#total)
|
||||
- [total_prometheus](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/configuration/#total_prometheus)
|
||||
|
||||
The last seen per-series value is dropped if no new samples are received for the given time series during two consecutive aggregations
|
||||
intervals specified in [stream aggregation config](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/configuration/#stream-aggregation-config) via `interval` option.
|
||||
For these outputs, the last seen per-series value is dropped if no new samples are received for the given time series during consecutive aggregation intervals specified in the [stream aggregation config](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/configuration/#stream-aggregation-config) via `interval` option.
|
||||
If a new sample for the existing time series is received after that, then it is treated as the first sample for a new time series.
|
||||
This may lead to the following issues:
|
||||
This may lead to the following issues when data is delayed:
|
||||
|
||||
- Lower than expected results for [total_prometheus](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/configuration/#total_prometheus) and [increase_prometheus](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/configuration/#increase_prometheus) outputs,
|
||||
since they ignore the first sample in a new time series.
|
||||
- Unexpected spikes for [total](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/configuration/#total) and [increase](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/configuration/#increase) outputs, since they assume that new time series start from 0.
|
||||
- [total](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/configuration/#total) and [increase](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/configuration/#increase) may produce unexpected spikes, since they assume that a new time series starts from `0`.
|
||||
- [total_prometheus](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/configuration/#total_prometheus) and [increase_prometheus](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/configuration/#increase_prometheus) may produce lower than expected results, if you expect to see the accumulated changes reflected after the delay, since they ignore the first sample in a new time series.
|
||||
|
||||
These issues can be fixed in the following ways:
|
||||
These issues can be improved in the following ways:
|
||||
|
||||
- By increasing the `interval` option at [stream aggregation config](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/configuration/#stream-aggregation-config), so it covers the expected
|
||||
delays in data ingestion pipelines.
|
||||
- By specifying the `staleness_interval` option at [stream aggregation config](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/configuration/#stream-aggregation-config), so it covers the expected
|
||||
delays in data ingestion pipelines. By default, the `staleness_interval` is equal to `2 x interval`.
|
||||
delays in data ingestion pipelines. It is recommended to set `interval` to at least 2× the scrape or push interval of the input. Set it to a higher value if the input pipeline is prone to large delays.
|
||||
- By increasing the `staleness_interval` option in the [stream aggregation config](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/configuration/#stream-aggregation-config), so it covers the expected
|
||||
delays in data ingestion pipelines. By default, the `staleness_interval` is equal to `interval`.
|
||||
|
||||
## High resource usage
|
||||
|
||||
|
||||
@@ -66,6 +66,8 @@ specified individually per each `-remoteWrite.url`:
|
||||
|
||||
# interval is the interval for the aggregation.
|
||||
# The aggregated stats is sent to remote storage once per interval.
|
||||
# It is recommended to set `interval` to at least 2× the scrape or push interval of the input.
|
||||
# Set it to a higher value if the input pipeline is prone to large delays.
|
||||
#
|
||||
interval: 1m
|
||||
|
||||
@@ -94,7 +96,7 @@ specified individually per each `-remoteWrite.url`:
|
||||
# - total_prometheus
|
||||
# See https://docs.victoriametrics.com/victoriametrics/stream-aggregation/#staleness for more details.
|
||||
#
|
||||
# staleness_interval: 2m
|
||||
# staleness_interval: 1m
|
||||
|
||||
# ignore_first_sample_interval specifies the interval after which the agent begins sending samples.
|
||||
# By default, it is set to the staleness interval, and it helps reduce the initial sample load after an agent restart.
|
||||
@@ -291,9 +293,6 @@ The results of `histogram_bucket` is equal to the following [MetricsQL](https://
|
||||
sum(histogram_over_time(some_histogram_bucket[interval])) by (vmrange)
|
||||
```
|
||||
|
||||
Aggregating irregular and sporadic metrics (received from [Lambdas](https://aws.amazon.com/lambda/)
|
||||
or [Cloud Functions](https://cloud.google.com/functions)) can be controlled via [staleness_interval](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/#staleness) option.
|
||||
|
||||
See also:
|
||||
- [quantiles](#quantiles)
|
||||
- [avg](#avg)
|
||||
@@ -507,6 +506,19 @@ See also:
|
||||
- [count_samples](#count_samples)
|
||||
- [count_series](#count_series)
|
||||
|
||||
### `sum_samples_total`
|
||||
|
||||
`sum_samples_total` sums input delta values into a cumulative [counter](https://docs.victoriametrics.com/victoriametrics/keyconcepts/index.html#counter) and outputs the result at the given `interval`.
|
||||
`sum_samples_total` makes sense only for aggregating delta values from clients such as [StatsD counter](https://github.com/statsd/statsd/blob/master/docs/metric_types.md#counting).
|
||||
|
||||
The results of `sum_samples_total` is roughly equal to the following [MetricsQL](https://docs.victoriametrics.com/victoriametrics/metricsql/) query:
|
||||
|
||||
```metricsql
|
||||
sum(running_sum(some_delta_values))
|
||||
```
|
||||
|
||||
>Note: The aggregator will forget the cumulative counter if it has not seen input samples for `staleness_interval`(set to `interval` by default) per output result, so the output counter will start from `0` the next time it sees the input again. Increase the `staleness_interval` option if you want to extend the window to tolerate bigger gaps.
|
||||
|
||||
### total
|
||||
|
||||
`total` generates output [counter](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#counter) by summing the input counters over the given `interval`.
|
||||
|
||||
@@ -95,6 +95,8 @@ See the docs at https://docs.victoriametrics.com/victoriametrics/
|
||||
Disable compression of HTTP responses to save CPU resources. By default, compression is enabled to save network bandwidth
|
||||
-http.header.csp string
|
||||
Value for 'Content-Security-Policy' header, recommended: "default-src 'self'"
|
||||
-http.header.disableServerHostname
|
||||
Whether to disable 'X-Server-Hostname' header in HTTP responses
|
||||
-http.header.frameOptions string
|
||||
Value for 'X-Frame-Options' header
|
||||
-http.header.hsts string
|
||||
@@ -621,7 +623,7 @@ See the docs at https://docs.victoriametrics.com/victoriametrics/
|
||||
-version
|
||||
Show VictoriaMetrics version
|
||||
-vmalert.proxyURL string
|
||||
Optional URL for proxying requests to vmalert. For example, if -vmalert.proxyURL=http://vmalert:8880 , then alerting API requests such as /api/v1/rules from Grafana will be proxied to http://vmalert:8880/api/v1/rules
|
||||
Optional URL for proxying requests to vmalert. For example, if -vmalert.proxyURL=http://vmalert:8880 , then alerting API requests such as /api/v1/rules from Grafana will be proxied to http://vmalert:8880/api/v1/rules . See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmalert
|
||||
-vmui.customDashboardsPath string
|
||||
Optional path to vmui dashboards. See https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/app/vmui/packages/vmui/public/dashboards
|
||||
-vmui.defaultTimezone string
|
||||
|
||||
@@ -797,6 +797,12 @@ For example, the following commands spread scrape targets among a cluster of two
|
||||
The `-promscrape.cluster.memberNum` can be set to a StatefulSet pod name when `vmagent` runs in Kubernetes.
|
||||
The pod name must end with a number in the range `0 ... promscrape.cluster.membersCount-1`. For example, `-promscrape.cluster.memberNum=vmagent-0`.
|
||||
|
||||
By default, targets are sharded among `vmagent` instances by all target labels after relabeling.
|
||||
Use `-promscrape.cluster.shardByLabels` {{% available_from "v1.146.0" %}} to shard targets by specified labels instead.
|
||||
For example, with `-promscrape.cluster.shardByLabels=service`, the targets with the same `service` label value will be scraped by the same `vmagent` instance,
|
||||
which is useful when perform stream aggregation that requires all metrics with the same `service` label value to be processed on the same `vmagent` instance.
|
||||
If none of the specified labels are present in the target labels, then all target labels will be used for sharding.
|
||||
|
||||
By default, each scrape target is scraped only by a single `vmagent` instance in the cluster. If there is a need for replicating scrape targets among multiple `vmagent` instances,
|
||||
then `-promscrape.cluster.replicationFactor` command-line flag must be set to the desired number of replicas. For example, the following commands
|
||||
start a cluster of three `vmagent` instances, where two `vmagent` instances scrape each target:
|
||||
@@ -928,6 +934,29 @@ vmagent will generate the following persistent queue folders:
|
||||
2_0AAFDF53E314A72A
|
||||
```
|
||||
|
||||
### On-disk persistence and data processing order
|
||||
|
||||
By default, vmagent processes data in FIFO order. If data has been written to the on-disk queue,
|
||||
it must be flushed to the remote storage before newly ingested data can be forwarded there.
|
||||
During long outages, vmagent may accumulate large amounts of data in the file-based queue,
|
||||
which can introduce a significant lag between the moment data is collected by vmagent and the
|
||||
moment it becomes visible at the remote storage.
|
||||
|
||||
This behavior can be changed with the `-remoteWrite.inmemoryQueues` {{% available_from "v1.146.0" %}} command-line flag.
|
||||
When set to a non-zero value, vmagent starts the given number of additional workers,
|
||||
which send only recently ingested data from the in-memory queue, while the workers configured via `-remoteWrite.queues` drain the file-based backlog concurrently.
|
||||
This reduces the delivery lag for fresh samples after remote storage outages or slowdowns. The flag can be set individually per each `-remoteWrite.url`.
|
||||
|
||||
Note that these workers are started in addition to the workers configured via `-remoteWrite.queues`, so the total number of concurrent connections to
|
||||
the remote storage becomes the sum of both flags. Take this into account if the remote storage limits the number of concurrent requests.
|
||||
|
||||
This flag has the following possible limitations:
|
||||
|
||||
* Samples may arrive at the remote storage out of order, since recent data can be delivered before the older backlogged data.
|
||||
Do not use this option if the remote storage doesn't accept out-of-order samples.
|
||||
* Recent data isn't guaranteed to take the fast path: if the in-memory queue is full,
|
||||
newly ingested data is still written to the file-based queue and is delivered in FIFO order by the generic workers.
|
||||
|
||||
### Disabling On-disk persistence
|
||||
|
||||
There are cases when it is better to disable on-disk persistence for pending data on the `vmagent` side:
|
||||
|
||||
@@ -74,6 +74,8 @@ See the docs at https://docs.victoriametrics.com/victoriametrics/vmagent/ .
|
||||
Disable compression of HTTP responses to save CPU resources. By default, compression is enabled to save network bandwidth
|
||||
-http.header.csp string
|
||||
Value for 'Content-Security-Policy' header, recommended: "default-src 'self'"
|
||||
-http.header.disableServerHostname
|
||||
Whether to disable 'X-Server-Hostname' header in HTTP responses
|
||||
-http.header.frameOptions string
|
||||
Value for 'X-Frame-Options' header
|
||||
-http.header.hsts string
|
||||
|
||||
@@ -115,6 +115,8 @@ See the docs at https://docs.victoriametrics.com/victoriametrics/vmalert/ .
|
||||
Disable compression of HTTP responses to save CPU resources. By default, compression is enabled to save network bandwidth
|
||||
-http.header.csp string
|
||||
Value for 'Content-Security-Policy' header, recommended: "default-src 'self'"
|
||||
-http.header.disableServerHostname
|
||||
Whether to disable 'X-Server-Hostname' header in HTTP responses
|
||||
-http.header.frameOptions string
|
||||
Value for 'X-Frame-Options' header
|
||||
-http.header.hsts string
|
||||
|
||||
@@ -59,6 +59,8 @@ See the docs at https://docs.victoriametrics.com/victoriametrics/vmauth/ .
|
||||
Disable compression of HTTP responses to save CPU resources. By default, compression is enabled to save network bandwidth
|
||||
-http.header.csp string
|
||||
Value for 'Content-Security-Policy' header, recommended: "default-src 'self'"
|
||||
-http.header.disableServerHostname
|
||||
Whether to disable 'X-Server-Hostname' header in HTTP responses
|
||||
-http.header.frameOptions string
|
||||
Value for 'X-Frame-Options' header
|
||||
-http.header.hsts string
|
||||
|
||||
@@ -400,6 +400,8 @@ Run `vmbackup -help` in order to see all the available options:
|
||||
Disable compression of HTTP responses to save CPU resources. By default, compression is enabled to save network bandwidth
|
||||
-http.header.csp string
|
||||
Value for 'Content-Security-Policy' header, recommended: "default-src 'self'"
|
||||
-http.header.disableServerHostname
|
||||
Whether to disable 'X-Server-Hostname' header in HTTP responses
|
||||
-http.header.frameOptions string
|
||||
Value for 'X-Frame-Options' header
|
||||
-http.header.hsts string
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user