mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-06-03 17:12:48 +03:00
Compare commits
23 Commits
dependabot
...
issue-1102
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d14b0f95a | ||
|
|
7ef0733071 | ||
|
|
9d4c06210c | ||
|
|
347d2e0fef | ||
|
|
f3de1f4ac7 | ||
|
|
a9032ecd1d | ||
|
|
ce712f0bc9 | ||
|
|
23a3ff4174 | ||
|
|
b638f95aba | ||
|
|
2345b2b4ed | ||
|
|
b6edf40198 | ||
|
|
b220066049 | ||
|
|
9ea1770ba4 | ||
|
|
24d176fd2c | ||
|
|
e5c277237e | ||
|
|
0656a0a702 | ||
|
|
95977d272f | ||
|
|
2b9973b970 | ||
|
|
6797776820 | ||
|
|
d2e554fb2b | ||
|
|
a4ec77fc02 | ||
|
|
cbb3439526 | ||
|
|
b67007a975 |
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
@@ -22,8 +22,7 @@ on:
|
||||
- '!app/vmui/**'
|
||||
- '.github/workflows/build.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
cancel-in-progress: true
|
||||
@@ -32,6 +31,8 @@ concurrency:
|
||||
jobs:
|
||||
build:
|
||||
name: ${{ matrix.os }}-${{ matrix.arch }}
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
4
.github/workflows/changelog-linter.yml
vendored
4
.github/workflows/changelog-linter.yml
vendored
@@ -5,8 +5,12 @@ on:
|
||||
paths:
|
||||
- "docs/victoriametrics/changelog/CHANGELOG.md"
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
tip-lint:
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
4
.github/workflows/check-commit-signed.yml
vendored
4
.github/workflows/check-commit-signed.yml
vendored
@@ -3,8 +3,12 @@ name: check-commit-signed
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
check-commit-signed:
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
6
.github/workflows/check-licenses.yml
vendored
6
.github/workflows/check-licenses.yml
vendored
@@ -6,12 +6,14 @@ on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'vendor'
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Code checkout
|
||||
|
||||
2
.github/workflows/codeql-analysis-go.yml
vendored
2
.github/workflows/codeql-analysis-go.yml
vendored
@@ -18,6 +18,8 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
|
||||
8
.github/workflows/docs.yaml
vendored
8
.github/workflows/docs.yaml
vendored
@@ -7,12 +7,14 @@ on:
|
||||
- 'docs/**'
|
||||
- '.github/workflows/docs.yaml'
|
||||
workflow_dispatch: {}
|
||||
permissions:
|
||||
contents: read # This is required for actions/checkout and to commit back image update
|
||||
deployments: write
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Code checkout
|
||||
|
||||
9
.github/workflows/test.yml
vendored
9
.github/workflows/test.yml
vendored
@@ -18,8 +18,7 @@ on:
|
||||
- 'go.*'
|
||||
- '.github/workflows/main.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
cancel-in-progress: true
|
||||
@@ -29,6 +28,8 @@ concurrency:
|
||||
jobs:
|
||||
lint:
|
||||
name: lint
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Code checkout
|
||||
@@ -61,6 +62,8 @@ jobs:
|
||||
|
||||
unit:
|
||||
name: unit
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
@@ -90,6 +93,8 @@ jobs:
|
||||
|
||||
apptest:
|
||||
name: apptest
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: apptest
|
||||
|
||||
steps:
|
||||
|
||||
10
.github/workflows/vmui.yml
vendored
10
.github/workflows/vmui.yml
vendored
@@ -16,11 +16,7 @@ on:
|
||||
- 'app/vmui/packages/vmui/**'
|
||||
- '.github/workflows/vmui.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
pull-requests: read
|
||||
checks: write
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
cancel-in-progress: true
|
||||
@@ -29,6 +25,10 @@ concurrency:
|
||||
jobs:
|
||||
vmui-checks:
|
||||
name: VMUI Checks (lint, test, typecheck)
|
||||
permissions:
|
||||
checks: write
|
||||
contents: read
|
||||
pull-requests: read
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Code checkout
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/procutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/pushmetrics"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -30,21 +29,11 @@ var (
|
||||
useProxyProtocol = flagutil.NewArrayBool("httpListenAddr.useProxyProtocol", "Whether to use proxy protocol for connections accepted at the corresponding -httpListenAddr . "+
|
||||
"See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt . "+
|
||||
"With enabled proxy protocol http server cannot serve regular /metrics endpoint. Use -pushmetrics.url for metrics pushing")
|
||||
minScrapeInterval = flag.Duration("dedup.minScrapeInterval", 0, "Leave only the last sample in every time series per each discrete interval "+
|
||||
"equal to -dedup.minScrapeInterval > 0. See also -streamAggr.dedupInterval and https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#deduplication")
|
||||
dryRun = flag.Bool("dryRun", false, "Whether to check config files without running VictoriaMetrics. The following config files are checked: "+
|
||||
"-promscrape.config, -relabelConfig and -streamAggr.config. Unknown config entries aren't allowed in -promscrape.config by default. "+
|
||||
"This can be changed with -promscrape.config.strictParse=false command-line flag")
|
||||
inmemoryDataFlushInterval = flag.Duration("inmemoryDataFlushInterval", 5*time.Second, "The interval for guaranteed saving of in-memory data to disk. "+
|
||||
"The saved data survives unclean shutdowns such as OOM crash, hardware reset, SIGKILL, etc. "+
|
||||
"Bigger intervals may help increase the lifetime of flash storage with limited write cycles (e.g. Raspberry PI). "+
|
||||
"Smaller intervals increase disk IO load. Minimum supported value is 1s")
|
||||
maxIngestionRate = flag.Int("maxIngestionRate", 0, "The maximum number of samples vmsingle can receive per second. Data ingestion is paused when the limit is exceeded. "+
|
||||
"By default there are no limits on samples ingestion rate.")
|
||||
finalDedupScheduleInterval = flag.Duration("storage.finalDedupScheduleCheckInterval", time.Hour, "The interval for checking when final deduplication process should be started."+
|
||||
"Storage unconditionally adds 25% jitter to the interval value on each check evaluation."+
|
||||
" Changing the interval to the bigger values may delay downsampling, deduplication for historical data."+
|
||||
" See also https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#deduplication")
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -87,12 +76,6 @@ func main() {
|
||||
}
|
||||
logger.Infof("starting VictoriaMetrics at %q...", listenAddrs)
|
||||
startTime := time.Now()
|
||||
storage.SetDedupInterval(*minScrapeInterval)
|
||||
storage.SetDataFlushInterval(*inmemoryDataFlushInterval)
|
||||
if *finalDedupScheduleInterval < time.Hour {
|
||||
logger.Fatalf("-dedup.finalDedupScheduleCheckInterval cannot be smaller than 1 hour; got %s", *finalDedupScheduleInterval)
|
||||
}
|
||||
storage.SetFinalDedupScheduleInterval(*finalDedupScheduleInterval)
|
||||
vmstorage.Init(promql.ResetRollupResultCacheIfNeeded)
|
||||
vmselect.Init()
|
||||
vminsertcommon.StartIngestionRateLimiter(*maxIngestionRate)
|
||||
|
||||
@@ -79,7 +79,8 @@ var (
|
||||
"writing them to remote storage. "+
|
||||
"Examples: -remoteWrite.roundDigits=2 would round 1.236 to 1.24, while -remoteWrite.roundDigits=-1 would round 126.78 to 130. "+
|
||||
"By default, digits rounding is disabled. Set it to 100 for disabling it for a particular remote storage. "+
|
||||
"This option may be used for improving data compression for the stored metrics")
|
||||
"This option may be used for improving data compression for the stored metrics. "+
|
||||
"See also -remoteWrite.significantFigures")
|
||||
sortLabels = flag.Bool("sortLabels", false, `Whether to sort labels for incoming samples before writing them to all the configured remote storage systems. `+
|
||||
`This may be needed for reducing memory usage at remote storage when the order of labels in incoming samples is random. `+
|
||||
`For example, if m{k1="v1",k2="v2"} may be sent as m{k2="v2",k1="v1"}`+
|
||||
|
||||
@@ -95,6 +95,7 @@ type groupMetrics struct {
|
||||
iterationTotal *metrics.Counter
|
||||
iterationDuration *metrics.Summary
|
||||
iterationMissed *metrics.Counter
|
||||
iterationReset *metrics.Counter
|
||||
iterationInterval *metrics.Gauge
|
||||
}
|
||||
|
||||
@@ -330,6 +331,7 @@ func (g *Group) Init() {
|
||||
g.metrics.iterationTotal = g.metrics.set.NewCounter(fmt.Sprintf(`vmalert_iteration_total{%s}`, labels))
|
||||
g.metrics.iterationDuration = g.metrics.set.NewSummary(fmt.Sprintf(`vmalert_iteration_duration_seconds{%s}`, labels))
|
||||
g.metrics.iterationMissed = g.metrics.set.NewCounter(fmt.Sprintf(`vmalert_iteration_missed_total{%s}`, labels))
|
||||
g.metrics.iterationReset = g.metrics.set.NewCounter(fmt.Sprintf(`vmalert_iteration_reset_total{%s}`, labels))
|
||||
g.metrics.iterationInterval = g.metrics.set.NewGauge(fmt.Sprintf(`vmalert_iteration_interval_seconds{%s}`, labels), func() float64 {
|
||||
i := g.Interval.Seconds()
|
||||
return i
|
||||
@@ -474,14 +476,16 @@ func (g *Group) Start(ctx context.Context, rw remotewrite.RWClient, rr datasourc
|
||||
if missed < 0 {
|
||||
// missed can become < 0 due to irregular delays during evaluation
|
||||
// which can result in time.Since(evalTS) < g.Interval;
|
||||
// or the system wall clock was changed backward
|
||||
missed = 0
|
||||
// or the system wall clock was changed backward,
|
||||
// Reset the evalTS to the current time.
|
||||
evalTS = time.Now()
|
||||
g.metrics.iterationReset.Inc()
|
||||
} else {
|
||||
evalTS = evalTS.Add((missed + 1) * g.Interval)
|
||||
}
|
||||
if missed > 0 {
|
||||
g.metrics.iterationMissed.Inc()
|
||||
}
|
||||
evalTS = evalTS.Add((missed + 1) * g.Interval)
|
||||
|
||||
eval(evalCtx, evalTS)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/VictoriaMetrics/metricsql"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/rule"
|
||||
@@ -160,12 +162,12 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
|
||||
|
||||
case "/vmalert/api/v1/alerts", "/api/v1/alerts":
|
||||
// path used by Grafana for ng alerting
|
||||
gf, err := newGroupsFilter(r)
|
||||
af, err := newAlertsFilter(r)
|
||||
if err != nil {
|
||||
errJson(w, r, err)
|
||||
return true
|
||||
}
|
||||
data, err := rh.listAlerts(gf)
|
||||
data, err := rh.listAlerts(af)
|
||||
if err != nil {
|
||||
errJson(w, r, err)
|
||||
return true
|
||||
@@ -325,6 +327,48 @@ func (gf *groupsFilter) matches(group *rule.Group) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type alertsFilter struct {
|
||||
gf *groupsFilter
|
||||
match [][]metricsql.LabelFilter
|
||||
}
|
||||
|
||||
func getMatchFilters(matches []string) ([][]metricsql.LabelFilter, *httpserver.ErrorWithStatusCode) {
|
||||
if len(matches) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
tfss := make([][]metricsql.LabelFilter, 0, len(matches))
|
||||
for _, s := range matches {
|
||||
expr, err := metricsql.Parse(s)
|
||||
if err != nil {
|
||||
return nil, errResponse(fmt.Errorf(`invalid parameter "match[]": failed to parse %q: %w`, s, err), http.StatusBadRequest)
|
||||
}
|
||||
me, ok := expr.(*metricsql.MetricExpr)
|
||||
if !ok {
|
||||
return nil, errResponse(fmt.Errorf(`invalid parameter "match[]": expecting metricSelector; got %q`, expr.AppendString(nil)), http.StatusBadRequest)
|
||||
}
|
||||
if len(me.LabelFilterss) == 0 {
|
||||
return nil, errResponse(fmt.Errorf(`invalid parameter "match[]": labelFilterss cannot be empty`), http.StatusBadRequest)
|
||||
}
|
||||
tfss = append(tfss, me.LabelFilterss...)
|
||||
}
|
||||
return tfss, nil
|
||||
}
|
||||
|
||||
func newAlertsFilter(r *http.Request) (*alertsFilter, *httpserver.ErrorWithStatusCode) {
|
||||
gf, err := newGroupsFilter(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var af alertsFilter
|
||||
af.gf = gf
|
||||
af.match, err = getMatchFilters(r.Form["match[]"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &af, nil
|
||||
}
|
||||
|
||||
// see https://prometheus.io/docs/prometheus/latest/querying/api/#rules
|
||||
type rulesFilter struct {
|
||||
gf *groupsFilter
|
||||
@@ -335,6 +379,7 @@ type rulesFilter struct {
|
||||
maxGroups int
|
||||
pageNum int
|
||||
search string
|
||||
match [][]metricsql.LabelFilter
|
||||
extendedStates bool
|
||||
}
|
||||
|
||||
@@ -355,7 +400,10 @@ func newRulesFilter(r *http.Request) (*rulesFilter, *httpserver.ErrorWithStatusC
|
||||
return nil, errResponse(fmt.Errorf(`invalid parameter "type": not supported value %q`, ruleTypeParam), http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
rf.match, err = getMatchFilters(r.Form["match[]"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
states := vs["state"]
|
||||
if len(states) == 0 {
|
||||
states = vs["filter"]
|
||||
@@ -416,12 +464,47 @@ func (rf *rulesFilter) matchesRule(r *rule.ApiRule) bool {
|
||||
if len(rf.ruleNames) > 0 && !slices.Contains(rf.ruleNames, r.Name) {
|
||||
return false
|
||||
}
|
||||
if !areLabelsMatch(r.Labels, rf.match) {
|
||||
return false
|
||||
}
|
||||
if len(rf.states) == 0 {
|
||||
return true
|
||||
}
|
||||
return slices.Contains(rf.states, r.State)
|
||||
}
|
||||
|
||||
func areLabelsMatch(labels map[string]string, matches [][]metricsql.LabelFilter) bool {
|
||||
if len(matches) == 0 {
|
||||
return true
|
||||
}
|
||||
// labels need to match at least one of the provided match[] arg
|
||||
return slices.ContainsFunc(matches, func(filters []metricsql.LabelFilter) bool {
|
||||
for _, mf := range filters {
|
||||
if !isLabelFilterMatch(labels[mf.Label], mf) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func isLabelFilterMatch(s string, match metricsql.LabelFilter) bool {
|
||||
if !match.IsRegexp {
|
||||
if match.IsNegative {
|
||||
return s != match.Value
|
||||
}
|
||||
return s == match.Value
|
||||
}
|
||||
re, err := metricsql.CompileRegexpAnchored(match.Value)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if match.IsNegative {
|
||||
return !re.MatchString(s)
|
||||
}
|
||||
return re.MatchString(s)
|
||||
}
|
||||
|
||||
func (rh *requestHandler) groups(rf *rulesFilter) *listGroupsResponse {
|
||||
rh.m.groupsMu.RLock()
|
||||
defer rh.m.groupsMu.RUnlock()
|
||||
@@ -543,14 +626,14 @@ func (rh *requestHandler) groupAlerts() []rule.GroupAlerts {
|
||||
return gAlerts
|
||||
}
|
||||
|
||||
func (rh *requestHandler) listAlerts(gf *groupsFilter) ([]byte, *httpserver.ErrorWithStatusCode) {
|
||||
func (rh *requestHandler) listAlerts(af *alertsFilter) ([]byte, *httpserver.ErrorWithStatusCode) {
|
||||
rh.m.groupsMu.RLock()
|
||||
defer rh.m.groupsMu.RUnlock()
|
||||
|
||||
lr := listAlertsResponse{Status: "success"}
|
||||
lr.Data.Alerts = make([]*rule.ApiAlert, 0)
|
||||
for _, group := range rh.m.groups {
|
||||
if !gf.matches(group) {
|
||||
if !af.gf.matches(group) {
|
||||
continue
|
||||
}
|
||||
g := group.ToAPI()
|
||||
@@ -558,7 +641,11 @@ func (rh *requestHandler) listAlerts(gf *groupsFilter) ([]byte, *httpserver.Erro
|
||||
if r.Type != rule.TypeAlerting {
|
||||
continue
|
||||
}
|
||||
lr.Data.Alerts = append(lr.Data.Alerts, r.Alerts...)
|
||||
for _, alert := range r.Alerts {
|
||||
if areLabelsMatch(alert.Labels, af.match) {
|
||||
lr.Data.Alerts = append(lr.Data.Alerts, alert)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -348,7 +348,7 @@
|
||||
typeK, ns := keys[i], targets[notifier.TargetType(keys[i])]
|
||||
count := len(ns)
|
||||
%}
|
||||
<div class="w-100 flex-column vm-group">
|
||||
<div class="w-100 flex-column">
|
||||
<span class="d-flex justify-content-between" id="group-{%s typeK %}">
|
||||
<a href="#group-{%s typeK %}">{%s typeK %} ({%d count %})</a>
|
||||
<span
|
||||
@@ -361,7 +361,7 @@
|
||||
<div id="item-{%s typeK %}" class="collapse show">
|
||||
<table class="table table-striped table-hover table-sm">
|
||||
<thead>
|
||||
<tr class="vm-item">
|
||||
<tr>
|
||||
<th scope="col">Labels</th>
|
||||
<th scope="col">Address</th>
|
||||
</tr>
|
||||
|
||||
@@ -1115,7 +1115,7 @@ func StreamListTargets(qw422016 *qt422016.Writer, r *http.Request, targets map[n
|
||||
|
||||
//line app/vmalert/web.qtpl:350
|
||||
qw422016.N().S(`
|
||||
<div class="w-100 flex-column vm-group">
|
||||
<div class="w-100 flex-column">
|
||||
<span class="d-flex justify-content-between" id="group-`)
|
||||
//line app/vmalert/web.qtpl:352
|
||||
qw422016.E().S(typeK)
|
||||
@@ -1152,7 +1152,7 @@ func StreamListTargets(qw422016 *qt422016.Writer, r *http.Request, targets map[n
|
||||
qw422016.N().S(`" class="collapse show">
|
||||
<table class="table table-striped table-hover table-sm">
|
||||
<thead>
|
||||
<tr class="vm-item">
|
||||
<tr>
|
||||
<th scope="col">Labels</th>
|
||||
<th scope="col">Address</th>
|
||||
</tr>
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/metricsql"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
||||
@@ -37,12 +39,14 @@ func TestHandler(t *testing.T) {
|
||||
Concurrency: 1,
|
||||
Rules: []config.Rule{
|
||||
{
|
||||
ID: 0,
|
||||
Alert: "alert",
|
||||
ID: 0,
|
||||
Alert: "alert",
|
||||
Labels: map[string]string{"job": "foo"},
|
||||
},
|
||||
{
|
||||
ID: 1,
|
||||
Record: "record",
|
||||
Labels: map[string]string{"job": "bar"},
|
||||
},
|
||||
},
|
||||
}, fq, 1*time.Minute, nil)
|
||||
@@ -128,6 +132,18 @@ func TestHandler(t *testing.T) {
|
||||
if length := len(lr.Data.Alerts); length != 2 {
|
||||
t.Fatalf("expected 2 alert got %d", length)
|
||||
}
|
||||
|
||||
lr = listAlertsResponse{}
|
||||
getResp(t, ts.URL+`/api/v1/alerts?match[]={job="foo"}`, &lr, 200)
|
||||
if length := len(lr.Data.Alerts); length != 3 {
|
||||
t.Fatalf("expected 3 alerts got %d", length)
|
||||
}
|
||||
|
||||
lr = listAlertsResponse{}
|
||||
getResp(t, ts.URL+`/api/v1/alerts?match[]={job="bar"}`, &lr, 200)
|
||||
if length := len(lr.Data.Alerts); length != 0 {
|
||||
t.Fatalf("expected 0 alerts got %d", length)
|
||||
}
|
||||
})
|
||||
t.Run("/api/v1/alert?alertID&groupID", func(t *testing.T) {
|
||||
expAlert := rule.NewAlertAPI(ar, ar.GetAlerts()[0])
|
||||
@@ -242,6 +258,13 @@ func TestHandler(t *testing.T) {
|
||||
check("/vmalert/api/v1/rules?datasource_type=graphite", 200, 1, 2)
|
||||
check("/vmalert/api/v1/rules?datasource_type=graphiti", 400, 0, 0)
|
||||
|
||||
// invalid match[] params
|
||||
check(`/vmalert/api/v1/rules?match[]={job=!"foo"}`, 400, 0, 0)
|
||||
check(`/vmalert/api/v1/rules?match[]={job="foo"}`, 200, 3, 3)
|
||||
check(`/vmalert/api/v1/rules?match[]={job="bar"}`, 200, 3, 3)
|
||||
check(`/vmalert/api/v1/rules?match[]={job="bar"}&match[]={job="foo"}`, 200, 3, 6)
|
||||
check(`/vmalert/api/v1/rules?match[]={job="barzz"}`, 200, 0, 0)
|
||||
|
||||
// no filtering expected due to bad params
|
||||
check("/api/v1/rules?type=badParam", 400, 0, 0)
|
||||
check("/api/v1/rules?foo=bar", 200, 3, 6)
|
||||
@@ -367,3 +390,116 @@ func TestEmptyResponse(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMatchesRule(t *testing.T) {
|
||||
parseMatch := func(t *testing.T, selectors []string) [][]metricsql.LabelFilter {
|
||||
t.Helper()
|
||||
var match [][]metricsql.LabelFilter
|
||||
for _, s := range selectors {
|
||||
expr, err := metricsql.Parse(s)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse selector %q: %v", s, err)
|
||||
}
|
||||
me, ok := expr.(*metricsql.MetricExpr)
|
||||
if !ok {
|
||||
t.Fatalf("expected MetricExpr for %q, got %T", s, expr)
|
||||
}
|
||||
match = append(match, me.LabelFilterss...)
|
||||
}
|
||||
return match
|
||||
}
|
||||
|
||||
f := func(t *testing.T, selectors []string, labels map[string]string, wantMatch bool) {
|
||||
t.Helper()
|
||||
rf := &rulesFilter{
|
||||
gf: &groupsFilter{},
|
||||
match: parseMatch(t, selectors),
|
||||
}
|
||||
r := &rule.ApiRule{Labels: labels}
|
||||
got := rf.matchesRule(r)
|
||||
if got != wantMatch {
|
||||
t.Fatalf("matchesRule(%v) with selectors %v: got %v, want %v",
|
||||
labels, selectors, got, wantMatch)
|
||||
}
|
||||
}
|
||||
|
||||
f(t, nil, map[string]string{"foo": "bar"}, true)
|
||||
|
||||
f(t, []string{`{foo="bar"}`}, map[string]string{"foo": "bar"}, true)
|
||||
f(t, []string{`{foo="bar"}`}, map[string]string{"foo": "baz"}, false)
|
||||
|
||||
f(t, []string{`{foo="bar"}`}, map[string]string{"bar": "baz"}, false)
|
||||
f(t, []string{`{foo=""}`}, map[string]string{"bar": "baz"}, true)
|
||||
|
||||
f(t, []string{`{foo!="bar"}`}, map[string]string{"foo": "baz"}, true)
|
||||
f(t, []string{`{foo!="bar"}`}, map[string]string{"foo": "bar"}, false)
|
||||
|
||||
f(t, []string{`{foo=~"bar.*"}`}, map[string]string{"foo": "bar"}, true)
|
||||
f(t, []string{`{foo=~"bar.*"}`}, map[string]string{"foo": "baz"}, false)
|
||||
f(t, []string{`{bar=~"baz|bar"}`}, map[string]string{"bar": "baz"}, true)
|
||||
f(t, []string{`{bar=~"baz|bar"}`}, map[string]string{"bar": "bar"}, true)
|
||||
f(t, []string{`{bar=~"baz|bar"}`}, map[string]string{"bar": "foo"}, false)
|
||||
|
||||
f(t, []string{`{foo!~"bar.*"}`}, map[string]string{"foo": "baz"}, true)
|
||||
f(t, []string{`{foo!~"bar.*"}`}, map[string]string{"foo": "bar"}, false)
|
||||
|
||||
// single match[] with multiple filters
|
||||
f(t,
|
||||
[]string{`{job="foo",instance="bar"}`},
|
||||
map[string]string{"job": "foo", "instance": "bar"},
|
||||
true,
|
||||
)
|
||||
f(t,
|
||||
[]string{`{job="foo",instance="bar"}`},
|
||||
map[string]string{"job": "other", "instance": "bar"},
|
||||
false,
|
||||
)
|
||||
|
||||
f(t,
|
||||
[]string{`{foo="bar",baz=~"b.*"}`},
|
||||
map[string]string{"foo": "bar", "baz": "bazinga"},
|
||||
true,
|
||||
)
|
||||
f(t,
|
||||
[]string{`{foo="bar",baz=~"b.*"}`},
|
||||
map[string]string{"foo": "other", "baz": "bazinga"},
|
||||
false,
|
||||
)
|
||||
|
||||
// multiple matches[]
|
||||
f(t,
|
||||
[]string{`{foo="bar"}`, `{foo="baz"}`},
|
||||
map[string]string{"foo": "baz"},
|
||||
true,
|
||||
)
|
||||
f(t,
|
||||
[]string{`{foo="bar"}`, `{foo="baz"}`},
|
||||
map[string]string{"foo": "unknown"},
|
||||
false,
|
||||
)
|
||||
f(t,
|
||||
[]string{`{foo=~"bar.*"}`, `{bar=~"baz.*"}`},
|
||||
map[string]string{"bar": "bazinga"},
|
||||
true,
|
||||
)
|
||||
f(t,
|
||||
[]string{`{foo=~"bar.*"}`, `{bar=~"baz.*"}`},
|
||||
map[string]string{"foo": "bartender"},
|
||||
true,
|
||||
)
|
||||
f(t,
|
||||
[]string{`{foo=~"bar.*"}`, `{bar=~"baz.*"}`},
|
||||
map[string]string{"foo": "other", "bar": "other"},
|
||||
false,
|
||||
)
|
||||
f(t,
|
||||
[]string{`{job="foo",instance="bar"}`, `{foo="bar"}`},
|
||||
map[string]string{"foo": "bar"},
|
||||
true,
|
||||
)
|
||||
f(t,
|
||||
[]string{`{job="foo", instance="bar"}`, `{foo="bar"}`},
|
||||
map[string]string{"instance": "barr", "job": "foo"},
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -889,7 +889,8 @@ func reloadAuthConfig() (bool, error) {
|
||||
}
|
||||
|
||||
mp := authUsers.Load()
|
||||
logger.Infof("loaded information about %d users from -auth.config=%q", len(*mp), *authConfigPath)
|
||||
jwtc := jwtAuthCache.Load()
|
||||
logger.Infof("loaded information about %d users from -auth.config=%q", len(*mp)+len(jwtc.users), *authConfigPath)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -545,6 +545,31 @@ requested_url={BACKEND}/path2/foo/?de=fg`
|
||||
if n := retries.Load(); n != 2 {
|
||||
t.Fatalf("unexpected number of retries; got %d; want 2", n)
|
||||
}
|
||||
|
||||
// make sure that empty config value erases client extra filters and extra labels
|
||||
cfgStr = `
|
||||
unauthorized_user:
|
||||
url_prefix: {BACKEND}/foo?bar=baz&extra_filters[]=&extra_label=&extra_filters=`
|
||||
requestURL = "http://some-host.com/abc/def?some_arg=some_value&extra_filters[]=baz&extra_label=tenant=admin&extra_filters=bar"
|
||||
backendHandler = func(w http.ResponseWriter, r *http.Request) {
|
||||
h := w.Header()
|
||||
h.Set("Connection", "close")
|
||||
h.Set("Foo", "bar")
|
||||
|
||||
var bb bytes.Buffer
|
||||
if err := r.Header.Write(&bb); err != nil {
|
||||
panic(fmt.Errorf("unexpected error when marshaling headers: %w", err))
|
||||
}
|
||||
fmt.Fprintf(w, "requested_url=http://%s%s\n%s", r.Host, r.URL, bb.String())
|
||||
}
|
||||
responseExpected = `
|
||||
statusCode=200
|
||||
Foo: bar
|
||||
requested_url={BACKEND}/foo/abc/def?bar=baz&extra_filters=&extra_filters%5B%5D=&extra_label=&some_arg=some_value
|
||||
Pass-Header: abc
|
||||
User-Agent: vmauth
|
||||
X-Forwarded-For: 12.34.56.78, 42.2.3.84`
|
||||
f(cfgStr, requestURL, backendHandler, responseExpected)
|
||||
}
|
||||
|
||||
func TestJWTRequestHandler(t *testing.T) {
|
||||
|
||||
@@ -146,7 +146,8 @@ var (
|
||||
Name: vmRoundDigits,
|
||||
Value: 100,
|
||||
Usage: "Round metric values to the given number of decimal digits after the point. " +
|
||||
"This option may be used for increasing on-disk compression level for the stored metrics",
|
||||
"This option may be used for increasing on-disk compression level for the stored metrics. " +
|
||||
"See also --vm-significant-figures option",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: vmExtraLabel,
|
||||
@@ -500,6 +501,96 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
mimirPath = "mimir-path"
|
||||
mimirTenantID = "mimir-tenant-id"
|
||||
mimirConcurrency = "mimir-concurrency"
|
||||
mimirFilterTimeStart = "mimir-filter-time-start"
|
||||
mimirFilterTimeEnd = "mimir-filter-time-end"
|
||||
mimirFilterLabel = "mimir-filter-label"
|
||||
mimirFilterLabelValue = "mimir-filter-label-value"
|
||||
|
||||
mimirCredsFilePath = "mimir-creds-file-path"
|
||||
mimirConfigFilePath = "mimir-config-file-path"
|
||||
mimirConfigProfile = "mimir-config-profile"
|
||||
mimirCustomS3Endpoint = "mimir-custom-s3-endpoint"
|
||||
mimirS3ForcePathStyle = "mimir-s3-force-path-style"
|
||||
mimirS3TLSInsecureSkipVerify = "mimir-s3-tls-insecure-skip-verify"
|
||||
mimirSSEKMSKeyID = "mimir-s3-sse-kms-key-id"
|
||||
mimirSSEAlgorithm = "mimir-s3-sse-algorithm"
|
||||
)
|
||||
|
||||
var (
|
||||
mimirFlags = []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: mimirPath,
|
||||
Usage: "Path to Mimir storage bucket or local folder.",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: mimirTenantID,
|
||||
Usage: "Tenant ID for Mimir storage",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: mimirConcurrency,
|
||||
Usage: "Number of concurrently running block readers",
|
||||
Value: 1,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: mimirFilterTimeStart,
|
||||
Usage: "The time filter in RFC3339 format to select timeseries with timestamp equal or higher than provided value. E.g. '2020-01-01T20:07:00Z'",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: mimirFilterTimeEnd,
|
||||
Usage: "The time filter in RFC3339 format to select timeseries with timestamp equal or lower than provided value. E.g. '2020-01-01T20:07:00Z'",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: mimirFilterLabel,
|
||||
Usage: "Mimir label name to filter timeseries by. E.g. '__name__' will filter timeseries by name.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: mimirFilterLabelValue,
|
||||
Usage: fmt.Sprintf("Regular expression to filter label from %q flag.", mimirFilterLabel),
|
||||
Value: ".*",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: mimirCredsFilePath,
|
||||
Usage: "Path to file with GCS or S3 credentials. Credentials are loaded from default locations if not set. See https://cloud.google.com/iam/docs/creating-managing-service-account-keys and https://docs.aws.amazon.com/general/latest/gr/aws-security-credentials.html",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: mimirConfigFilePath,
|
||||
Usage: "Path to file with S3 configs. Configs are loaded from default location if not set. See https://docs.aws.amazon.com/general/latest/gr/aws-security-credentials.html",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: mimirConfigProfile,
|
||||
Usage: "Profile name for S3 configs. If no set, the value of the environment variable will be loaded (AWS_PROFILE or AWS_DEFAULT_PROFILE), or if both not set, DefaultSharedConfigProfile is used",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: mimirCustomS3Endpoint,
|
||||
Usage: "Custom S3 endpoint for use with S3-compatible storages (e.g. MinIO). S3 is used if not set",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: mimirS3ForcePathStyle,
|
||||
Usage: "Prefixing endpoint with bucket name when set false, true by default.",
|
||||
Value: true,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: mimirS3TLSInsecureSkipVerify,
|
||||
Usage: "Whether to skip TLS verification when connecting to the S3 endpoint.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: mimirSSEKMSKeyID,
|
||||
Usage: "SSE KMS Key ID for use with S3-compatible storages.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: mimirSSEAlgorithm,
|
||||
Usage: "SSE algorithm for use with S3-compatible storages.",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
vmNativeFilterMatch = "vm-native-filter-match"
|
||||
vmNativeFilterTimeStart = "vm-native-filter-time-start"
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/auth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/backoff"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/barpool"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/mimir"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/native"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/remoteread"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
@@ -297,12 +298,12 @@ func main() {
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "thanos",
|
||||
Usage: "Migrate time series from Thanos blocks (supports raw and downsampled data)",
|
||||
Flags: mergeFlags(globalFlags, thanosFlags, vmFlags),
|
||||
Name: "mimir",
|
||||
Usage: "Migrate time series from Mimir object storage or local filesystem",
|
||||
Flags: mergeFlags(globalFlags, mimirFlags, vmFlags),
|
||||
Before: beforeFn,
|
||||
Action: func(c *cli.Context) error {
|
||||
fmt.Println("Thanos import mode")
|
||||
fmt.Println("Mimir import mode")
|
||||
|
||||
vmCfg, err := initConfigVM(c)
|
||||
if err != nil {
|
||||
@@ -314,6 +315,54 @@ func main() {
|
||||
return fmt.Errorf("failed to create VM importer: %s", err)
|
||||
}
|
||||
|
||||
mCfg := mimir.Config{
|
||||
Filter: mimir.Filter{
|
||||
TimeMin: c.String(mimirFilterTimeStart),
|
||||
TimeMax: c.String(mimirFilterTimeEnd),
|
||||
Label: c.String(mimirFilterLabel),
|
||||
LabelValue: c.String(mimirFilterLabelValue),
|
||||
},
|
||||
Path: c.String(mimirPath),
|
||||
TenantID: c.String(mimirTenantID),
|
||||
CredsFilePath: c.String(mimirCredsFilePath),
|
||||
ConfigFilePath: c.String(mimirConfigFilePath),
|
||||
ConfigProfile: c.String(mimirConfigProfile),
|
||||
CustomS3Endpoint: c.String(mimirCustomS3Endpoint),
|
||||
S3ForcePathStyle: c.Bool(mimirS3ForcePathStyle),
|
||||
S3TLSInsecureSkipVerify: c.Bool(mimirS3TLSInsecureSkipVerify),
|
||||
SSEKMSKeyID: c.String(mimirSSEKMSKeyID),
|
||||
SSEAlgorithm: c.String(mimirSSEAlgorithm),
|
||||
}
|
||||
cl, err := mimir.NewClient(ctx, mCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create mimir client: %s", err)
|
||||
}
|
||||
|
||||
pp := prometheusProcessor{
|
||||
cl: cl,
|
||||
im: importer,
|
||||
cc: c.Int(mimirConcurrency),
|
||||
isVerbose: c.Bool(globalVerbose),
|
||||
}
|
||||
return pp.run(ctx)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "thanos",
|
||||
Usage: "Migrate time series from Thanos blocks (supports raw and downsampled data)",
|
||||
Flags: mergeFlags(globalFlags, thanosFlags, vmFlags),
|
||||
Before: beforeFn,
|
||||
Action: func(c *cli.Context) error {
|
||||
fmt.Println("Thanos import mode")
|
||||
vmCfg, err := initConfigVM(c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to init VM configuration: %s", err)
|
||||
}
|
||||
|
||||
importer, err = vm.NewImporter(ctx, vmCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create VM importer: %s", err)
|
||||
}
|
||||
thanosCfg := thanos.Config{
|
||||
Snapshot: c.String(thanosSnapshot),
|
||||
Filter: thanos.Filter{
|
||||
|
||||
195
app/vmctl/mimir/lazyreader.go
Normal file
195
app/vmctl/mimir/lazyreader.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package mimir
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/prometheus/prometheus/tsdb"
|
||||
"github.com/prometheus/prometheus/tsdb/tombstones"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/backup/common"
|
||||
)
|
||||
|
||||
var _ tsdb.BlockReader = (*lazyBlockReader)(nil)
|
||||
|
||||
// lazyBlockReader is stores block id and segment num information.
|
||||
// It is used to lazily fetch and parse block data.
|
||||
// It implements tsdb.BlockReader interface.
|
||||
type lazyBlockReader struct {
|
||||
// Block ID.
|
||||
ID ulid.ULID
|
||||
// SegmentsNum stores the number of chunks segments in the block.
|
||||
SegmentsNum int
|
||||
|
||||
mu sync.Mutex
|
||||
reader *tsdb.Block
|
||||
tempDirPath string
|
||||
fs common.RemoteFS
|
||||
err error
|
||||
}
|
||||
|
||||
// newLazyBlockReader returns a new LazyBlockReader for the given block.
|
||||
func newLazyBlockReader(block *Block, fs common.RemoteFS) (*lazyBlockReader, error) {
|
||||
if block.SegmentsFormat != "1b6d" {
|
||||
return nil, fmt.Errorf("unsupported segments format: %s", block.SegmentsFormat)
|
||||
}
|
||||
|
||||
return &lazyBlockReader{
|
||||
ID: block.ID,
|
||||
SegmentsNum: block.SegmentsNum,
|
||||
fs: fs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (lbr *lazyBlockReader) initialize() error {
|
||||
lbr.mu.Lock()
|
||||
defer lbr.mu.Unlock()
|
||||
if lbr.reader != nil {
|
||||
return nil
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
|
||||
lbr.tempDirPath = temp
|
||||
|
||||
// TODO: replace fetchFile and writeFile with buffered IO if needed
|
||||
meta, err := lbr.fetchFile(metaFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := lbr.writeFile(temp, metaFilename, meta); err != nil {
|
||||
return fmt.Errorf("failed to write meta file: %w", err)
|
||||
}
|
||||
idx, err := lbr.fetchFile(indexFilename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch index file %q: %w", indexFilename, err)
|
||||
}
|
||||
if err := lbr.writeFile(temp, indexFilename, idx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := 1; i <= lbr.SegmentsNum; i++ {
|
||||
// segments formats has format 1b06d
|
||||
// https://github.com/grafana/mimir/blob/main/pkg/storage/tsdb/bucketindex/index.go#L32
|
||||
chunkName := fmt.Sprintf("%06d", i)
|
||||
blockChunkPath := filepath.Join("chunks", chunkName)
|
||||
chunk, err := lbr.fetchFile(blockChunkPath)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Set postingDecoder to nil because
|
||||
// If it is nil then a default decoder is used, compatible with Prometheus v2.
|
||||
pb, err := tsdb.OpenBlock(nil, temp, nil, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open block %q: %w", lbr.ID, err)
|
||||
}
|
||||
lbr.reader = pb
|
||||
return nil
|
||||
}
|
||||
|
||||
// Index returns an IndexReader over the block's data.
|
||||
func (lbr *lazyBlockReader) Index() (tsdb.IndexReader, error) {
|
||||
if err := lbr.initialize(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return lbr.reader.Index()
|
||||
}
|
||||
|
||||
// Chunks returns a ChunkReader over the block's data.
|
||||
func (lbr *lazyBlockReader) Chunks() (tsdb.ChunkReader, error) {
|
||||
if err := lbr.initialize(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return lbr.reader.Chunks()
|
||||
}
|
||||
|
||||
// Tombstones returns a tombstones.Reader over the block's deleted data.
|
||||
func (lbr *lazyBlockReader) Tombstones() (tombstones.Reader, error) {
|
||||
if err := lbr.initialize(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return lbr.reader.Tombstones()
|
||||
}
|
||||
|
||||
// Meta provides meta information about the block reader.
|
||||
func (lbr *lazyBlockReader) Meta() tsdb.BlockMeta {
|
||||
if err := lbr.initialize(); err != nil {
|
||||
lbr.err = fmt.Errorf("cannot get BlockMeta: %w", err)
|
||||
return tsdb.BlockMeta{}
|
||||
}
|
||||
return lbr.reader.Meta()
|
||||
}
|
||||
|
||||
// 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)
|
||||
return 0
|
||||
}
|
||||
return lbr.reader.Size()
|
||||
}
|
||||
|
||||
// Err returns the last error that occurred on the block reader.
|
||||
func (lbr *lazyBlockReader) Err() error {
|
||||
return lbr.err
|
||||
}
|
||||
|
||||
// Close closes block and releases all resources
|
||||
func (lbr *lazyBlockReader) Close() error {
|
||||
lbr.mu.Lock()
|
||||
defer lbr.mu.Unlock()
|
||||
if lbr.reader == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := lbr.reader.Close()
|
||||
if err := os.RemoveAll(lbr.tempDirPath); err != nil {
|
||||
log.Printf("failed to remove temp dir: %s", err)
|
||||
}
|
||||
lbr.reader = nil
|
||||
lbr.tempDirPath = ""
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
err = os.Mkdir(filepath.Join(temp, "chunks"), os.ModePerm)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create temp dir: %s", err)
|
||||
}
|
||||
return temp, nil
|
||||
}
|
||||
|
||||
func (lbr *lazyBlockReader) fetchFile(filePath string) ([]byte, error) {
|
||||
blockID := lbr.ID.String()
|
||||
blockPath := filepath.Join(blockID, filePath)
|
||||
has, err := lbr.fs.HasFile(blockPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, fmt.Errorf("block meta %s not found", blockID)
|
||||
}
|
||||
return lbr.fs.ReadFile(blockPath)
|
||||
}
|
||||
|
||||
func (lbr *lazyBlockReader) writeFile(folder string, filename string, file []byte) error {
|
||||
fileName := filepath.Join(folder, filename)
|
||||
return os.WriteFile(fileName, file, os.ModePerm)
|
||||
}
|
||||
238
app/vmctl/mimir/mimir.go
Normal file
238
app/vmctl/mimir/mimir.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package mimir
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
"github.com/prometheus/prometheus/tsdb"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/prometheus"
|
||||
utils "github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/vmctlutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/backup/common"
|
||||
)
|
||||
|
||||
const (
|
||||
bucketIndex = "bucket-index.json"
|
||||
bucketIndexCompressedFilename = bucketIndex + ".gz"
|
||||
metaFilename = "meta.json"
|
||||
indexFilename = "index"
|
||||
)
|
||||
|
||||
// BlockDeletionMark holds the information about a block's deletion mark in the index.
|
||||
// This type was copied from the mimir repository https://github.com/grafana/mimir/blob/main/pkg/storage/tsdb/bucketindex/index.go#L234.
|
||||
type BlockDeletionMark struct {
|
||||
// Block ID.
|
||||
ID ulid.ULID `json:"block_id"`
|
||||
|
||||
// DeletionTime is a unix timestamp (seconds precision) of when the block was marked to be deleted.
|
||||
DeletionTime int64 `json:"deletion_time"`
|
||||
}
|
||||
|
||||
// Block holds the information about a block in the index.
|
||||
// This is a partial implementation of the https://github.com/grafana/mimir/blob/main/pkg/storage/tsdb/bucketindex/index.go#L73
|
||||
type Block struct {
|
||||
// Block ID.
|
||||
ID ulid.ULID `json:"block_id"`
|
||||
|
||||
// MinTime and MaxTime specify the time range all samples in the block are in (millis precision).
|
||||
MinTime int64 `json:"min_time"`
|
||||
MaxTime int64 `json:"max_time"`
|
||||
|
||||
// SegmentsFormat and SegmentsNum stores the format and number of chunks segments
|
||||
// in the block.
|
||||
SegmentsFormat string `json:"segments_format,omitempty"`
|
||||
SegmentsNum int `json:"segments_num,omitempty"`
|
||||
}
|
||||
|
||||
// Index contains all known blocks and markers of a tenant.
|
||||
// This is a partial implementation pof the https://github.com/grafana/mimir/blob/main/pkg/storage/tsdb/bucketindex/index.go#L36
|
||||
type Index struct {
|
||||
// Version of the index format.
|
||||
Version int `json:"version"`
|
||||
|
||||
// List of complete blocks (partial blocks are excluded from the index).
|
||||
Blocks []*Block `json:"blocks"`
|
||||
}
|
||||
|
||||
// Config contains a list of params needed
|
||||
// for reading mimir snapshots
|
||||
type Config struct {
|
||||
// Path to remote storage bucket
|
||||
Path string
|
||||
// TenantID is the tenant id for the storage
|
||||
TenantID string
|
||||
|
||||
Filter Filter
|
||||
|
||||
CredsFilePath string
|
||||
ConfigFilePath string
|
||||
ConfigProfile string
|
||||
CustomS3Endpoint string
|
||||
S3ForcePathStyle bool
|
||||
S3TLSInsecureSkipVerify bool
|
||||
|
||||
SSEKMSKeyID string
|
||||
SSEAlgorithm string
|
||||
}
|
||||
|
||||
// Filter contains configuration for filtering
|
||||
// the timeseries
|
||||
type Filter struct {
|
||||
TimeMin string
|
||||
TimeMax string
|
||||
Label string
|
||||
LabelValue string
|
||||
}
|
||||
|
||||
// Client is a wrapper over Prometheus tsdb.DBReader
|
||||
type Client struct {
|
||||
common.RemoteFS
|
||||
filter filter
|
||||
}
|
||||
|
||||
type filter struct {
|
||||
min, max int64
|
||||
label string
|
||||
labelValue string
|
||||
}
|
||||
|
||||
func (f filter) inRange(minTime, maxTime int64) bool {
|
||||
fmin, fmax := f.min, f.max
|
||||
if minTime == 0 {
|
||||
fmin = minTime
|
||||
}
|
||||
if fmax == 0 {
|
||||
fmax = maxTime
|
||||
}
|
||||
return minTime <= fmax && fmin <= maxTime
|
||||
}
|
||||
|
||||
// NewClient creates and validates new Client
|
||||
// with given Config
|
||||
func NewClient(ctx context.Context, cfg Config) (*Client, error) {
|
||||
if cfg.Path == "" {
|
||||
return nil, fmt.Errorf("path cannot be empty")
|
||||
}
|
||||
|
||||
if cfg.TenantID != "" {
|
||||
cfg.Path = fmt.Sprintf("%s/%s", cfg.Path, cfg.TenantID)
|
||||
}
|
||||
|
||||
var c Client
|
||||
rfs, err := newRemoteFS(ctx, cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse `-src`=%q: %w", cfg.Path, err)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
timeMax, err := utils.ParseTime(cfg.Filter.TimeMax)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse max time in filter: %s", err)
|
||||
}
|
||||
c.filter = filter{
|
||||
min: timeMin.UnixMilli(),
|
||||
max: timeMax.UnixMilli(),
|
||||
label: cfg.Filter.Label,
|
||||
labelValue: cfg.Filter.LabelValue,
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
// Explore a fetches bucket-index.json file from a remote storage or local filesystem
|
||||
// and filter blocks via the defined time range, but does not take into account label filters.
|
||||
func (c *Client) Explore() ([]tsdb.BlockReader, error) {
|
||||
|
||||
log.Printf("Fetching blocks from remote storage")
|
||||
|
||||
indexFile, err := c.fetchIndexFile()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch index file: %s", err)
|
||||
}
|
||||
|
||||
var blocksToImport []tsdb.BlockReader
|
||||
for _, block := range indexFile.Blocks {
|
||||
if !c.filter.inRange(block.MinTime, block.MaxTime) {
|
||||
// Skipping block outside of time range
|
||||
continue
|
||||
}
|
||||
|
||||
if block.ID.String() == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
lazyBlockReader, err := newLazyBlockReader(block, c.RemoteFS)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create lazy block reader: %s", err)
|
||||
}
|
||||
blocksToImport = append(blocksToImport, lazyBlockReader)
|
||||
}
|
||||
|
||||
return blocksToImport, nil
|
||||
}
|
||||
|
||||
// Read reads the given BlockReader according to configured
|
||||
// time and label filters.
|
||||
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())
|
||||
}
|
||||
|
||||
if meta.ULID.String() == "" {
|
||||
return nil, fmt.Errorf("unexpected block without id")
|
||||
}
|
||||
|
||||
minTime, maxTime := meta.MinTime, meta.MaxTime
|
||||
if c.filter.min != 0 {
|
||||
minTime = c.filter.min
|
||||
}
|
||||
if c.filter.max != 0 {
|
||||
maxTime = c.filter.max
|
||||
}
|
||||
q, err := tsdb.NewBlockQuerier(block, minTime, maxTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ss := q.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchRegexp, c.filter.label, c.filter.labelValue))
|
||||
return &prometheus.CloseableSeriesSet{SeriesSet: ss, Close: q.Close}, nil
|
||||
}
|
||||
|
||||
func (c *Client) fetchIndexFile() (*Index, error) {
|
||||
has, err := c.HasFile(bucketIndexCompressedFilename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, fmt.Errorf("bucket-index.json.gz not found")
|
||||
}
|
||||
|
||||
file, err := c.ReadFile(bucketIndexCompressedFilename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read bucket index: %s", 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)
|
||||
}
|
||||
|
||||
var indexFile Index
|
||||
err = json.NewDecoder(gzipReader).Decode(&indexFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode bucket index: %s", err)
|
||||
}
|
||||
|
||||
return &indexFile, nil
|
||||
}
|
||||
93
app/vmctl/mimir/remotefs.go
Normal file
93
app/vmctl/mimir/remotefs.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package mimir
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/backup/azremote"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/backup/common"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/backup/fsremote"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/backup/gcsremote"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/backup/s3remote"
|
||||
)
|
||||
|
||||
// newRemoteFS returns new remote fs from the given Config.
|
||||
func newRemoteFS(ctx context.Context, cfg Config) (common.RemoteFS, error) {
|
||||
if len(cfg.Path) == 0 {
|
||||
return nil, fmt.Errorf("path cannot be empty")
|
||||
}
|
||||
n := strings.Index(cfg.Path, "://")
|
||||
if n < 0 {
|
||||
return nil, fmt.Errorf("missing scheme in path %q. Supported schemes: `gs://`, `s3://`, `azblob://`, `fs://`", cfg.Path)
|
||||
}
|
||||
scheme := cfg.Path[:n]
|
||||
dir := cfg.Path[n+len("://"):]
|
||||
switch scheme {
|
||||
case "fs":
|
||||
if !filepath.IsAbs(dir) {
|
||||
return nil, fmt.Errorf("dir must be absolute; got %q", dir)
|
||||
}
|
||||
fsr := &fsremote.FS{
|
||||
Dir: filepath.Clean(dir),
|
||||
}
|
||||
return fsr, nil
|
||||
case "gcs", "gs":
|
||||
n := strings.Index(dir, "/")
|
||||
if n < 0 {
|
||||
return nil, fmt.Errorf("missing directory on the gcs bucket %q", dir)
|
||||
}
|
||||
bucket := dir[:n]
|
||||
dir = dir[n:]
|
||||
fsr := &gcsremote.FS{
|
||||
CredsFilePath: cfg.CredsFilePath,
|
||||
Bucket: bucket,
|
||||
Dir: dir,
|
||||
}
|
||||
if err := fsr.Init(ctx); err != nil {
|
||||
return nil, fmt.Errorf("cannot initialize connection to gcs: %w", err)
|
||||
}
|
||||
return fsr, nil
|
||||
case "azblob":
|
||||
n := strings.Index(dir, "/")
|
||||
if n < 0 {
|
||||
return nil, fmt.Errorf("missing directory on the AZBlob container %q", dir)
|
||||
}
|
||||
bucket := dir[:n]
|
||||
dir = dir[n:]
|
||||
fsr := &azremote.FS{
|
||||
Container: bucket,
|
||||
Dir: dir,
|
||||
}
|
||||
if err := fsr.Init(ctx); err != nil {
|
||||
return nil, fmt.Errorf("cannot initialize connection to AZBlob: %w", err)
|
||||
}
|
||||
return fsr, nil
|
||||
case "s3":
|
||||
n := strings.Index(dir, "/")
|
||||
if n < 0 {
|
||||
return nil, fmt.Errorf("missing directory on the s3 bucket %q", dir)
|
||||
}
|
||||
bucket := dir[:n]
|
||||
dir = dir[n:]
|
||||
fsr := &s3remote.FS{
|
||||
CredsFilePath: cfg.CredsFilePath,
|
||||
ConfigFilePath: cfg.ConfigFilePath,
|
||||
CustomEndpoint: cfg.CustomS3Endpoint,
|
||||
TLSInsecureSkipVerify: cfg.S3TLSInsecureSkipVerify,
|
||||
S3ForcePathStyle: cfg.S3ForcePathStyle,
|
||||
ProfileName: cfg.ConfigProfile,
|
||||
Bucket: bucket,
|
||||
Dir: dir,
|
||||
SSEKMSKeyId: cfg.SSEKMSKeyID,
|
||||
SSEAlgorithm: s3remote.StringToEncryptionAlgorithm(cfg.SSEAlgorithm),
|
||||
}
|
||||
if err := fsr.Init(ctx); err != nil {
|
||||
return nil, fmt.Errorf("cannot initialize connection to s3: %w", err)
|
||||
}
|
||||
return fsr, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported scheme %q", scheme)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -18,10 +19,17 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/vm"
|
||||
)
|
||||
|
||||
// Runner is an interface for fetching and reading
|
||||
// snapshot blocks
|
||||
type Runner interface {
|
||||
Explore() ([]tsdb.BlockReader, error)
|
||||
Read(context.Context, tsdb.BlockReader) (*prometheus.CloseableSeriesSet, error)
|
||||
}
|
||||
|
||||
type prometheusProcessor struct {
|
||||
// prometheus client fetches and reads
|
||||
// Runner fetches and reads
|
||||
// snapshot blocks
|
||||
cl *prometheus.Client
|
||||
cl Runner
|
||||
// importer performs import requests
|
||||
// for timeseries data returned from
|
||||
// snapshot blocks
|
||||
@@ -48,7 +56,7 @@ func (pp *prometheusProcessor) run(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := pp.processBlocks(blocks); err != nil {
|
||||
if err := pp.processBlocks(ctx, blocks); err != nil {
|
||||
return fmt.Errorf("migration failed: %s", err)
|
||||
}
|
||||
|
||||
@@ -57,11 +65,17 @@ func (pp *prometheusProcessor) run(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pp *prometheusProcessor) do(b tsdb.BlockReader) error {
|
||||
ss, err := pp.cl.Read(b)
|
||||
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)
|
||||
}
|
||||
defer func() {
|
||||
if err := css.Close(); err != nil {
|
||||
log.Printf("cannot close SeriesSet for block: %q : %s\n", b.Meta().ULID, err)
|
||||
}
|
||||
}()
|
||||
ss := css.SeriesSet
|
||||
var it chunkenc.Iterator
|
||||
for ss.Next() {
|
||||
var name string
|
||||
@@ -114,7 +128,7 @@ func (pp *prometheusProcessor) do(b tsdb.BlockReader) error {
|
||||
return ss.Err()
|
||||
}
|
||||
|
||||
func (pp *prometheusProcessor) processBlocks(blocks []tsdb.BlockReader) error {
|
||||
func (pp *prometheusProcessor) processBlocks(ctx context.Context, blocks []tsdb.BlockReader) error {
|
||||
promBlocksTotal.Add(len(blocks))
|
||||
bar := barpool.AddWithTemplate(fmt.Sprintf(barTpl, "Processing blocks"), len(blocks))
|
||||
if err := barpool.Start(); err != nil {
|
||||
@@ -130,11 +144,16 @@ func (pp *prometheusProcessor) processBlocks(blocks []tsdb.BlockReader) error {
|
||||
for range pp.cc {
|
||||
wg.Go(func() {
|
||||
for br := range blockReadersCh {
|
||||
if err := pp.do(br); err != nil {
|
||||
if err := pp.do(ctx, br); err != nil {
|
||||
promErrorsTotal.Inc()
|
||||
errCh <- fmt.Errorf("read failed for block %q: %s", br.Meta().ULID, err)
|
||||
errCh <- fmt.Errorf("cannot read block %q: %s", br.Meta().ULID, err)
|
||||
return
|
||||
}
|
||||
if cb, ok := br.(io.Closer); ok {
|
||||
if err := cb.Close(); err != nil {
|
||||
errCh <- fmt.Errorf("cannot close block: %q: %w", br.Meta().ULID, err)
|
||||
}
|
||||
}
|
||||
promBlocksProcessed.Inc()
|
||||
bar.Increment()
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
"github.com/prometheus/prometheus/storage"
|
||||
"github.com/prometheus/prometheus/tsdb"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/vmctlutil"
|
||||
)
|
||||
|
||||
// Config contains a list of params needed
|
||||
@@ -60,13 +62,13 @@ func NewClient(cfg Config) (*Client, error) {
|
||||
return nil, fmt.Errorf("failed to open snapshot %q: %s", cfg.Snapshot, err)
|
||||
}
|
||||
c := &Client{DBReadOnly: db}
|
||||
minTime, maxTime, err := parseTime(cfg.Filter.TimeMin, cfg.Filter.TimeMax)
|
||||
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)
|
||||
}
|
||||
c.filter = filter{
|
||||
min: minTime,
|
||||
max: maxTime,
|
||||
min: timeMin,
|
||||
max: timeMax,
|
||||
label: cfg.Filter.Label,
|
||||
labelValue: cfg.Filter.LabelValue,
|
||||
}
|
||||
@@ -83,7 +85,7 @@ func (c *Client) Explore() ([]tsdb.BlockReader, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch blocks: %s", err)
|
||||
}
|
||||
s := &Stats{
|
||||
s := &vmctlutil.Stats{
|
||||
Filtered: c.filter.min != 0 || c.filter.max != 0 || c.filter.label != "",
|
||||
Blocks: len(blocks),
|
||||
}
|
||||
@@ -108,9 +110,15 @@ func (c *Client) Explore() ([]tsdb.BlockReader, error) {
|
||||
return blocksToImport, nil
|
||||
}
|
||||
|
||||
// CloseableSeriesSet defines a SeriesSet with Close method
|
||||
type CloseableSeriesSet struct {
|
||||
SeriesSet storage.SeriesSet
|
||||
Close func() error
|
||||
}
|
||||
|
||||
// Read reads the given BlockReader according to configured
|
||||
// time and label filters.
|
||||
func (c *Client) Read(block tsdb.BlockReader) (storage.SeriesSet, error) {
|
||||
func (c *Client) Read(ctx context.Context, block tsdb.BlockReader) (*CloseableSeriesSet, error) {
|
||||
minTime, maxTime := block.Meta().MinTime, block.Meta().MaxTime
|
||||
if c.filter.min != 0 {
|
||||
minTime = c.filter.min
|
||||
@@ -122,8 +130,8 @@ func (c *Client) Read(block tsdb.BlockReader) (storage.SeriesSet, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ss := q.Select(context.Background(), false, nil, labels.MustNewMatcher(labels.MatchRegexp, c.filter.label, c.filter.labelValue))
|
||||
return ss, nil
|
||||
ss := q.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchRegexp, c.filter.label, c.filter.labelValue))
|
||||
return &CloseableSeriesSet{ss, q.Close}, nil
|
||||
}
|
||||
|
||||
func parseTime(start, end string) (int64, int64, error) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package prometheus
|
||||
package vmctlutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -18,7 +18,7 @@ type Stats struct {
|
||||
|
||||
// String returns string representation for s.
|
||||
func (s Stats) String() string {
|
||||
str := fmt.Sprintf("Prometheus snapshot stats:\n"+
|
||||
str := fmt.Sprintf("Snapshot stats:\n"+
|
||||
" blocks found: %d;\n"+
|
||||
" blocks skipped by time filter: %d;\n"+
|
||||
" min time: %d (%v);\n"+
|
||||
@@ -2,13 +2,16 @@
|
||||
"math"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||
) %}
|
||||
|
||||
{% stripspace %}
|
||||
|
||||
// Federate writes rs in /federate format.
|
||||
// See https://prometheus.io/docs/prometheus/latest/federation/
|
||||
{% func Federate(rs *netstorage.Result) %}
|
||||
{% func Federate(rs *netstorage.Result, escapeScheme string) %}
|
||||
{% code
|
||||
values := rs.Values
|
||||
timestamps := rs.Timestamps
|
||||
@@ -24,10 +27,54 @@
|
||||
See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3185
|
||||
{% endcomment %}
|
||||
{% return %}
|
||||
{% endif %}
|
||||
{%= prometheusMetricName(&rs.MetricName) %}{% space %}
|
||||
{% endif %}
|
||||
|
||||
{% switch escapeScheme %}
|
||||
{% case federateEscapeSchemeUTF8 %}
|
||||
{%= prometheusFederateMetricNameUTF8(&rs.MetricName) %}{% space %}
|
||||
|
||||
{% case federateEscapeSchemeUnderscore %}
|
||||
{%= prometheusFederateMetricNameEscapeUnderscore(&rs.MetricName) %}{% space %}
|
||||
|
||||
{% case "" %}
|
||||
{%= prometheusMetricName(&rs.MetricName) %}{% space %}
|
||||
{% endswitch %}
|
||||
|
||||
{%f= lastValue %}{% space %}
|
||||
{%dl= timestamps[len(timestamps)-1] %}{% newline %}
|
||||
{% endfunc %}
|
||||
|
||||
{% func prometheusFederateMetricNameEscapeUnderscore(mn *storage.MetricName) %}
|
||||
{%s= promrelabel.SanitizeMetricName(bytesutil.ToUnsafeString(mn.MetricGroup)) %}
|
||||
{% if len(mn.Tags) > 0 %}
|
||||
{
|
||||
{% code tags := mn.Tags %}
|
||||
{%s= promrelabel.SanitizeLabelName(bytesutil.ToUnsafeString(tags[0].Key)) %}={%= escapePrometheusLabel(tags[0].Value) %}
|
||||
{% code tags = tags[1:] %}
|
||||
{% for i := range tags %}
|
||||
{% code tag := &tags[i] %}
|
||||
,{%s= promrelabel.SanitizeLabelName(bytesutil.ToUnsafeString(tag.Key)) %}={%= escapePrometheusLabel(tag.Value) %}
|
||||
{% endfor %}
|
||||
}
|
||||
{% endif %}
|
||||
{% endfunc %}
|
||||
|
||||
{% func prometheusFederateMetricNameUTF8(mn *storage.MetricName) %}
|
||||
{
|
||||
{%= escapePrometheusLabel(mn.MetricGroup) %}
|
||||
{% if len(mn.Tags) > 0 %}
|
||||
,
|
||||
{% code tags := mn.Tags %}
|
||||
{%= escapePrometheusLabel(tags[0].Key) %}={%= escapePrometheusLabel(tags[0].Value) %}
|
||||
{% code tags = tags[1:] %}
|
||||
{% for i := range tags %}
|
||||
{% code tag := &tags[i] %}
|
||||
,{%= escapePrometheusLabel(tag.Key) %}={%= escapePrometheusLabel(tag.Value) %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
}
|
||||
{% endfunc %}
|
||||
|
||||
|
||||
|
||||
{% endstripspace %}
|
||||
|
||||
@@ -9,82 +9,241 @@ import (
|
||||
"math"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||
)
|
||||
|
||||
// Federate writes rs in /federate format.// See https://prometheus.io/docs/prometheus/latest/federation/
|
||||
|
||||
//line app/vmselect/prometheus/federate.qtpl:11
|
||||
//line app/vmselect/prometheus/federate.qtpl:14
|
||||
import (
|
||||
qtio422016 "io"
|
||||
|
||||
qt422016 "github.com/valyala/quicktemplate"
|
||||
)
|
||||
|
||||
//line app/vmselect/prometheus/federate.qtpl:11
|
||||
//line app/vmselect/prometheus/federate.qtpl:14
|
||||
var (
|
||||
_ = qtio422016.Copy
|
||||
_ = qt422016.AcquireByteBuffer
|
||||
)
|
||||
|
||||
//line app/vmselect/prometheus/federate.qtpl:11
|
||||
func StreamFederate(qw422016 *qt422016.Writer, rs *netstorage.Result) {
|
||||
//line app/vmselect/prometheus/federate.qtpl:13
|
||||
//line app/vmselect/prometheus/federate.qtpl:14
|
||||
func StreamFederate(qw422016 *qt422016.Writer, rs *netstorage.Result, escapeScheme string) {
|
||||
//line app/vmselect/prometheus/federate.qtpl:16
|
||||
values := rs.Values
|
||||
timestamps := rs.Timestamps
|
||||
|
||||
//line app/vmselect/prometheus/federate.qtpl:16
|
||||
//line app/vmselect/prometheus/federate.qtpl:19
|
||||
if len(timestamps) == 0 || len(values) == 0 {
|
||||
//line app/vmselect/prometheus/federate.qtpl:16
|
||||
//line app/vmselect/prometheus/federate.qtpl:19
|
||||
return
|
||||
//line app/vmselect/prometheus/federate.qtpl:16
|
||||
//line app/vmselect/prometheus/federate.qtpl:19
|
||||
}
|
||||
//line app/vmselect/prometheus/federate.qtpl:18
|
||||
//line app/vmselect/prometheus/federate.qtpl:21
|
||||
lastValue := values[len(values)-1]
|
||||
|
||||
//line app/vmselect/prometheus/federate.qtpl:20
|
||||
//line app/vmselect/prometheus/federate.qtpl:23
|
||||
if math.IsNaN(lastValue) {
|
||||
//line app/vmselect/prometheus/federate.qtpl:26
|
||||
//line app/vmselect/prometheus/federate.qtpl:29
|
||||
return
|
||||
//line app/vmselect/prometheus/federate.qtpl:27
|
||||
//line app/vmselect/prometheus/federate.qtpl:30
|
||||
}
|
||||
//line app/vmselect/prometheus/federate.qtpl:28
|
||||
streamprometheusMetricName(qw422016, &rs.MetricName)
|
||||
//line app/vmselect/prometheus/federate.qtpl:28
|
||||
qw422016.N().S(` `)
|
||||
//line app/vmselect/prometheus/federate.qtpl:29
|
||||
//line app/vmselect/prometheus/federate.qtpl:32
|
||||
switch escapeScheme {
|
||||
//line app/vmselect/prometheus/federate.qtpl:33
|
||||
case federateEscapeSchemeUTF8:
|
||||
//line app/vmselect/prometheus/federate.qtpl:34
|
||||
streamprometheusFederateMetricNameUTF8(qw422016, &rs.MetricName)
|
||||
//line app/vmselect/prometheus/federate.qtpl:34
|
||||
qw422016.N().S(` `)
|
||||
//line app/vmselect/prometheus/federate.qtpl:36
|
||||
case federateEscapeSchemeUnderscore:
|
||||
//line app/vmselect/prometheus/federate.qtpl:37
|
||||
streamprometheusFederateMetricNameEscapeUnderscore(qw422016, &rs.MetricName)
|
||||
//line app/vmselect/prometheus/federate.qtpl:37
|
||||
qw422016.N().S(` `)
|
||||
//line app/vmselect/prometheus/federate.qtpl:39
|
||||
case "":
|
||||
//line app/vmselect/prometheus/federate.qtpl:40
|
||||
streamprometheusMetricName(qw422016, &rs.MetricName)
|
||||
//line app/vmselect/prometheus/federate.qtpl:40
|
||||
qw422016.N().S(` `)
|
||||
//line app/vmselect/prometheus/federate.qtpl:41
|
||||
}
|
||||
//line app/vmselect/prometheus/federate.qtpl:43
|
||||
qw422016.N().F(lastValue)
|
||||
//line app/vmselect/prometheus/federate.qtpl:29
|
||||
//line app/vmselect/prometheus/federate.qtpl:43
|
||||
qw422016.N().S(` `)
|
||||
//line app/vmselect/prometheus/federate.qtpl:30
|
||||
//line app/vmselect/prometheus/federate.qtpl:44
|
||||
qw422016.N().DL(timestamps[len(timestamps)-1])
|
||||
//line app/vmselect/prometheus/federate.qtpl:30
|
||||
//line app/vmselect/prometheus/federate.qtpl:44
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line app/vmselect/prometheus/federate.qtpl:31
|
||||
//line app/vmselect/prometheus/federate.qtpl:45
|
||||
}
|
||||
|
||||
//line app/vmselect/prometheus/federate.qtpl:31
|
||||
func WriteFederate(qq422016 qtio422016.Writer, rs *netstorage.Result) {
|
||||
//line app/vmselect/prometheus/federate.qtpl:31
|
||||
//line app/vmselect/prometheus/federate.qtpl:45
|
||||
func WriteFederate(qq422016 qtio422016.Writer, rs *netstorage.Result, escapeScheme string) {
|
||||
//line app/vmselect/prometheus/federate.qtpl:45
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vmselect/prometheus/federate.qtpl:31
|
||||
StreamFederate(qw422016, rs)
|
||||
//line app/vmselect/prometheus/federate.qtpl:31
|
||||
//line app/vmselect/prometheus/federate.qtpl:45
|
||||
StreamFederate(qw422016, rs, escapeScheme)
|
||||
//line app/vmselect/prometheus/federate.qtpl:45
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vmselect/prometheus/federate.qtpl:31
|
||||
//line app/vmselect/prometheus/federate.qtpl:45
|
||||
}
|
||||
|
||||
//line app/vmselect/prometheus/federate.qtpl:31
|
||||
func Federate(rs *netstorage.Result) string {
|
||||
//line app/vmselect/prometheus/federate.qtpl:31
|
||||
//line app/vmselect/prometheus/federate.qtpl:45
|
||||
func Federate(rs *netstorage.Result, escapeScheme string) string {
|
||||
//line app/vmselect/prometheus/federate.qtpl:45
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vmselect/prometheus/federate.qtpl:31
|
||||
WriteFederate(qb422016, rs)
|
||||
//line app/vmselect/prometheus/federate.qtpl:31
|
||||
//line app/vmselect/prometheus/federate.qtpl:45
|
||||
WriteFederate(qb422016, rs, escapeScheme)
|
||||
//line app/vmselect/prometheus/federate.qtpl:45
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vmselect/prometheus/federate.qtpl:31
|
||||
//line app/vmselect/prometheus/federate.qtpl:45
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vmselect/prometheus/federate.qtpl:31
|
||||
//line app/vmselect/prometheus/federate.qtpl:45
|
||||
return qs422016
|
||||
//line app/vmselect/prometheus/federate.qtpl:31
|
||||
//line app/vmselect/prometheus/federate.qtpl:45
|
||||
}
|
||||
|
||||
//line app/vmselect/prometheus/federate.qtpl:47
|
||||
func streamprometheusFederateMetricNameEscapeUnderscore(qw422016 *qt422016.Writer, mn *storage.MetricName) {
|
||||
//line app/vmselect/prometheus/federate.qtpl:48
|
||||
qw422016.N().S(promrelabel.SanitizeMetricName(bytesutil.ToUnsafeString(mn.MetricGroup)))
|
||||
//line app/vmselect/prometheus/federate.qtpl:49
|
||||
if len(mn.Tags) > 0 {
|
||||
//line app/vmselect/prometheus/federate.qtpl:49
|
||||
qw422016.N().S(`{`)
|
||||
//line app/vmselect/prometheus/federate.qtpl:51
|
||||
tags := mn.Tags
|
||||
|
||||
//line app/vmselect/prometheus/federate.qtpl:52
|
||||
qw422016.N().S(promrelabel.SanitizeLabelName(bytesutil.ToUnsafeString(tags[0].Key)))
|
||||
//line app/vmselect/prometheus/federate.qtpl:52
|
||||
qw422016.N().S(`=`)
|
||||
//line app/vmselect/prometheus/federate.qtpl:52
|
||||
streamescapePrometheusLabel(qw422016, tags[0].Value)
|
||||
//line app/vmselect/prometheus/federate.qtpl:53
|
||||
tags = tags[1:]
|
||||
|
||||
//line app/vmselect/prometheus/federate.qtpl:54
|
||||
for i := range tags {
|
||||
//line app/vmselect/prometheus/federate.qtpl:55
|
||||
tag := &tags[i]
|
||||
|
||||
//line app/vmselect/prometheus/federate.qtpl:55
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vmselect/prometheus/federate.qtpl:56
|
||||
qw422016.N().S(promrelabel.SanitizeLabelName(bytesutil.ToUnsafeString(tag.Key)))
|
||||
//line app/vmselect/prometheus/federate.qtpl:56
|
||||
qw422016.N().S(`=`)
|
||||
//line app/vmselect/prometheus/federate.qtpl:56
|
||||
streamescapePrometheusLabel(qw422016, tag.Value)
|
||||
//line app/vmselect/prometheus/federate.qtpl:57
|
||||
}
|
||||
//line app/vmselect/prometheus/federate.qtpl:57
|
||||
qw422016.N().S(`}`)
|
||||
//line app/vmselect/prometheus/federate.qtpl:59
|
||||
}
|
||||
//line app/vmselect/prometheus/federate.qtpl:60
|
||||
}
|
||||
|
||||
//line app/vmselect/prometheus/federate.qtpl:60
|
||||
func writeprometheusFederateMetricNameEscapeUnderscore(qq422016 qtio422016.Writer, mn *storage.MetricName) {
|
||||
//line app/vmselect/prometheus/federate.qtpl:60
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vmselect/prometheus/federate.qtpl:60
|
||||
streamprometheusFederateMetricNameEscapeUnderscore(qw422016, mn)
|
||||
//line app/vmselect/prometheus/federate.qtpl:60
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vmselect/prometheus/federate.qtpl:60
|
||||
}
|
||||
|
||||
//line app/vmselect/prometheus/federate.qtpl:60
|
||||
func prometheusFederateMetricNameEscapeUnderscore(mn *storage.MetricName) string {
|
||||
//line app/vmselect/prometheus/federate.qtpl:60
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vmselect/prometheus/federate.qtpl:60
|
||||
writeprometheusFederateMetricNameEscapeUnderscore(qb422016, mn)
|
||||
//line app/vmselect/prometheus/federate.qtpl:60
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vmselect/prometheus/federate.qtpl:60
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vmselect/prometheus/federate.qtpl:60
|
||||
return qs422016
|
||||
//line app/vmselect/prometheus/federate.qtpl:60
|
||||
}
|
||||
|
||||
//line app/vmselect/prometheus/federate.qtpl:62
|
||||
func streamprometheusFederateMetricNameUTF8(qw422016 *qt422016.Writer, mn *storage.MetricName) {
|
||||
//line app/vmselect/prometheus/federate.qtpl:62
|
||||
qw422016.N().S(`{`)
|
||||
//line app/vmselect/prometheus/federate.qtpl:64
|
||||
streamescapePrometheusLabel(qw422016, mn.MetricGroup)
|
||||
//line app/vmselect/prometheus/federate.qtpl:65
|
||||
if len(mn.Tags) > 0 {
|
||||
//line app/vmselect/prometheus/federate.qtpl:65
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vmselect/prometheus/federate.qtpl:67
|
||||
tags := mn.Tags
|
||||
|
||||
//line app/vmselect/prometheus/federate.qtpl:68
|
||||
streamescapePrometheusLabel(qw422016, tags[0].Key)
|
||||
//line app/vmselect/prometheus/federate.qtpl:68
|
||||
qw422016.N().S(`=`)
|
||||
//line app/vmselect/prometheus/federate.qtpl:68
|
||||
streamescapePrometheusLabel(qw422016, tags[0].Value)
|
||||
//line app/vmselect/prometheus/federate.qtpl:69
|
||||
tags = tags[1:]
|
||||
|
||||
//line app/vmselect/prometheus/federate.qtpl:70
|
||||
for i := range tags {
|
||||
//line app/vmselect/prometheus/federate.qtpl:71
|
||||
tag := &tags[i]
|
||||
|
||||
//line app/vmselect/prometheus/federate.qtpl:71
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vmselect/prometheus/federate.qtpl:72
|
||||
streamescapePrometheusLabel(qw422016, tag.Key)
|
||||
//line app/vmselect/prometheus/federate.qtpl:72
|
||||
qw422016.N().S(`=`)
|
||||
//line app/vmselect/prometheus/federate.qtpl:72
|
||||
streamescapePrometheusLabel(qw422016, tag.Value)
|
||||
//line app/vmselect/prometheus/federate.qtpl:73
|
||||
}
|
||||
//line app/vmselect/prometheus/federate.qtpl:74
|
||||
}
|
||||
//line app/vmselect/prometheus/federate.qtpl:74
|
||||
qw422016.N().S(`}`)
|
||||
//line app/vmselect/prometheus/federate.qtpl:76
|
||||
}
|
||||
|
||||
//line app/vmselect/prometheus/federate.qtpl:76
|
||||
func writeprometheusFederateMetricNameUTF8(qq422016 qtio422016.Writer, mn *storage.MetricName) {
|
||||
//line app/vmselect/prometheus/federate.qtpl:76
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vmselect/prometheus/federate.qtpl:76
|
||||
streamprometheusFederateMetricNameUTF8(qw422016, mn)
|
||||
//line app/vmselect/prometheus/federate.qtpl:76
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vmselect/prometheus/federate.qtpl:76
|
||||
}
|
||||
|
||||
//line app/vmselect/prometheus/federate.qtpl:76
|
||||
func prometheusFederateMetricNameUTF8(mn *storage.MetricName) string {
|
||||
//line app/vmselect/prometheus/federate.qtpl:76
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vmselect/prometheus/federate.qtpl:76
|
||||
writeprometheusFederateMetricNameUTF8(qb422016, mn)
|
||||
//line app/vmselect/prometheus/federate.qtpl:76
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vmselect/prometheus/federate.qtpl:76
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vmselect/prometheus/federate.qtpl:76
|
||||
return qs422016
|
||||
//line app/vmselect/prometheus/federate.qtpl:76
|
||||
}
|
||||
|
||||
@@ -8,15 +8,15 @@ import (
|
||||
)
|
||||
|
||||
func TestFederate(t *testing.T) {
|
||||
f := func(rs *netstorage.Result, expectedResult string) {
|
||||
f := func(rs *netstorage.Result, escapeScheme string, expectedResult string) {
|
||||
t.Helper()
|
||||
result := Federate(rs)
|
||||
result := Federate(rs, escapeScheme)
|
||||
if result != expectedResult {
|
||||
t.Fatalf("unexpected result; got\n%s\nwant\n%s", result, expectedResult)
|
||||
}
|
||||
}
|
||||
|
||||
f(&netstorage.Result{}, ``)
|
||||
f(&netstorage.Result{}, ``, ``)
|
||||
|
||||
f(&netstorage.Result{
|
||||
MetricName: storage.MetricName{
|
||||
@@ -39,5 +39,60 @@ func TestFederate(t *testing.T) {
|
||||
},
|
||||
Values: []float64{1.23},
|
||||
Timestamps: []int64{123},
|
||||
}, `foo{a="b",qqq="\\",abc="a<b\"\\c"} 1.23 123`+"\n")
|
||||
}, ``, `foo{a="b",qqq="\\",abc="a<b\"\\c"} 1.23 123`+"\n")
|
||||
|
||||
f(&netstorage.Result{
|
||||
MetricName: storage.MetricName{
|
||||
MetricGroup: []byte("foo.bar"),
|
||||
Tags: []storage.Tag{
|
||||
{
|
||||
Key: []byte("some.!other"),
|
||||
Value: []byte("value.unchanged!."),
|
||||
},
|
||||
{
|
||||
Key: []byte("qqq"),
|
||||
Value: []byte("\\"),
|
||||
},
|
||||
{
|
||||
Key: []byte("!key"),
|
||||
Value: []byte("value"),
|
||||
},
|
||||
{
|
||||
Key: []byte("abc"),
|
||||
// Verify that < isn't encoded. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5431
|
||||
Value: []byte("a<b\"\\c"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Values: []float64{1.23},
|
||||
Timestamps: []int64{123},
|
||||
}, federateEscapeSchemeUnderscore, `foo_bar{some__other="value.unchanged!.",qqq="\\",_key="value",abc="a<b\"\\c"} 1.23 123`+"\n")
|
||||
|
||||
f(&netstorage.Result{
|
||||
MetricName: storage.MetricName{
|
||||
MetricGroup: []byte("foo.bar"),
|
||||
Tags: []storage.Tag{
|
||||
{
|
||||
Key: []byte("some.!other"),
|
||||
Value: []byte("value.unchanged!."),
|
||||
},
|
||||
{
|
||||
Key: []byte("qqq"),
|
||||
Value: []byte("\\"),
|
||||
},
|
||||
{
|
||||
Key: []byte("!key"),
|
||||
Value: []byte("value"),
|
||||
},
|
||||
{
|
||||
Key: []byte(`ab"c`),
|
||||
// Verify that < isn't encoded. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5431
|
||||
Value: []byte("a<b\"\\c"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Values: []float64{1.23},
|
||||
Timestamps: []int64{123},
|
||||
}, federateEscapeSchemeUTF8, `{"foo.bar","some.!other"="value.unchanged!.","qqq"="\\","!key"="value","ab\"c"="a<b\"\\c"} 1.23 123`+"\n")
|
||||
|
||||
}
|
||||
|
||||
@@ -9,16 +9,17 @@ import (
|
||||
)
|
||||
|
||||
func BenchmarkFederate(b *testing.B) {
|
||||
|
||||
rs := &netstorage.Result{
|
||||
MetricName: storage.MetricName{
|
||||
MetricGroup: []byte("foo_bar_bazaaaa_total"),
|
||||
MetricGroup: []byte("foo_bar_?_._bazaaaa_total"),
|
||||
Tags: []storage.Tag{
|
||||
{
|
||||
Key: []byte("instance"),
|
||||
Key: []byte("instance:job"),
|
||||
Value: []byte("foobarbaz:2344"),
|
||||
},
|
||||
{
|
||||
Key: []byte("job"),
|
||||
Key: []byte("job.name"),
|
||||
Value: []byte("aaabbbccc"),
|
||||
},
|
||||
},
|
||||
@@ -27,12 +28,22 @@ func BenchmarkFederate(b *testing.B) {
|
||||
Timestamps: []int64{1234567890},
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
var bb bytes.Buffer
|
||||
for pb.Next() {
|
||||
bb.Reset()
|
||||
WriteFederate(&bb, rs)
|
||||
}
|
||||
})
|
||||
f := func(name, escapeScheme string) {
|
||||
b.Helper()
|
||||
|
||||
b.Run(name, func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
var bb bytes.Buffer
|
||||
for pb.Next() {
|
||||
bb.Reset()
|
||||
WriteFederate(&bb, rs, escapeScheme)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
f("without escape", "")
|
||||
f("allow-utf-8", federateEscapeSchemeUTF8)
|
||||
f("legacy-underscore", federateEscapeSchemeUnderscore)
|
||||
}
|
||||
|
||||
@@ -108,6 +108,11 @@ func PrettifyQuery(w http.ResponseWriter, r *http.Request) {
|
||||
_ = bw.Flush()
|
||||
}
|
||||
|
||||
const (
|
||||
federateEscapeSchemeUnderscore = "underscore"
|
||||
federateEscapeSchemeUTF8 = "utf-8"
|
||||
)
|
||||
|
||||
// FederateHandler implements /federate . See https://prometheus.io/docs/prometheus/latest/federation/
|
||||
func FederateHandler(startTime time.Time, w http.ResponseWriter, r *http.Request) error {
|
||||
defer federateDuration.UpdateDuration(startTime)
|
||||
@@ -132,6 +137,21 @@ func FederateHandler(startTime time.Time, w http.ResponseWriter, r *http.Request
|
||||
return fmt.Errorf("cannot fetch data for %q: %w", sq, err)
|
||||
}
|
||||
|
||||
// add best-effort format negotiation
|
||||
// modern version of Prometheus always set allow-utf-8 in order to properly parse utf-8 names and labels
|
||||
// prometheus below v3 uses underscore escaping by default and it's the most common standard
|
||||
var escapeScheme string
|
||||
accept := r.Header.Get("Accept")
|
||||
if len(accept) > 0 && strings.Contains(accept, "allow-utf-8") {
|
||||
escapeScheme = federateEscapeSchemeUTF8
|
||||
}
|
||||
// try fallback to legacy underscore escaping if needed for Prometheus only,
|
||||
// it's not widely used after Prometheus v3.0 release
|
||||
// most of the Prometheus scrapers already use allow-utf-8 header
|
||||
isPrometheus := strings.HasPrefix(r.UserAgent(), "Prometheus")
|
||||
if len(escapeScheme) == 0 && isPrometheus {
|
||||
escapeScheme = federateEscapeSchemeUnderscore
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
bw := bufferedwriter.Get(w)
|
||||
defer bufferedwriter.Put(bw)
|
||||
@@ -141,7 +161,7 @@ func FederateHandler(startTime time.Time, w http.ResponseWriter, r *http.Request
|
||||
return err
|
||||
}
|
||||
bb := sw.getBuffer(workerID)
|
||||
WriteFederate(bb, rs)
|
||||
WriteFederate(bb, rs, escapeScheme)
|
||||
return sw.maybeFlushBuffer(bb)
|
||||
})
|
||||
if err == nil {
|
||||
|
||||
@@ -8801,6 +8801,17 @@ func TestExecSuccess(t *testing.T) {
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range()`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `range()`
|
||||
r := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{1000, 1000, 1000, 1000, 1000, 1000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`step()`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `time() / step()`
|
||||
|
||||
@@ -2439,8 +2439,15 @@ func rollupIntegrate(rfa *rollupFuncArg) float64 {
|
||||
prevTimestamp = timestamp
|
||||
prevValue = v
|
||||
}
|
||||
dt := float64(rfa.currTimestamp-prevTimestamp) / 1e3
|
||||
sum += prevValue * dt
|
||||
// Only extrapolate the last value through to currTimestamp when the time
|
||||
// series has any sample after the lookbehind window. When realNextValue is
|
||||
// NaN the series has effectively ended at prevTimestamp, so accruing area
|
||||
// past it would overcount the integral.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9474
|
||||
if !math.IsNaN(rfa.realNextValue) {
|
||||
dt := float64(rfa.currTimestamp-prevTimestamp) / 1e3
|
||||
sum += prevValue * dt
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
|
||||
@@ -1385,10 +1385,65 @@ func TestRollupFuncsNoWindow(t *testing.T) {
|
||||
if samplesScanned != 24 {
|
||||
t.Fatalf("expecting 24 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
|
||||
}
|
||||
valuesExpected := []float64{nan, 2.148, 1.593, 1.156, 1.36}
|
||||
// At tEnd=160 the series has no samples past the window (last sample is at
|
||||
// ts=130), so integrate() must not extrapolate prevValue through tEnd.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9474
|
||||
valuesExpected := []float64{nan, 2.148, 1.593, 1.156, 0.34}
|
||||
timestampsExpected := []int64{0, 40, 80, 120, 160}
|
||||
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
|
||||
})
|
||||
t.Run("integrate_past_series_end", func(t *testing.T) {
|
||||
// Constant series of value 1.0 from t=0..3600s (1h) at 60s step.
|
||||
// Query integrate(metric[1h]) across t=0..10800s with 600s step.
|
||||
// For t=0..3600s the window overlap with the data is [0,t], so the integral grows from 0 to 3600 (seconds).
|
||||
// After the series ends, integrate must NOT keep accruing 3600 — it
|
||||
// should taper to 0 once the lookbehind window is entirely past the
|
||||
// last sample.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9474
|
||||
var testValues []int64
|
||||
var testTimestamps []float64
|
||||
for t := int64(0); t <= 3600_000; t += 60_000 {
|
||||
testValues = append(testValues, t)
|
||||
testTimestamps = append(testTimestamps, 1.0)
|
||||
}
|
||||
rc := rollupConfig{
|
||||
Func: rollupIntegrate,
|
||||
Start: 0,
|
||||
End: 10800_000,
|
||||
Step: 600_000,
|
||||
Window: 3600_000,
|
||||
MaxPointsPerSeries: 1e4,
|
||||
}
|
||||
rc.Timestamps = rc.getTimestamps()
|
||||
values, _ := rc.Do(nil, testTimestamps, testValues)
|
||||
for i, ti := range rc.Timestamps {
|
||||
v := values[i]
|
||||
|
||||
// For t<=3600s: window overlap is [0,ti], integral equals ti in seconds.
|
||||
if ti <= 3600_000 {
|
||||
expV := float64(ti / 1e3)
|
||||
if v != expV {
|
||||
t.Fatalf("unexpected integrate result at t=%ds, want=%.3f got=%.3f", ti/1e3, expV, v)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// For 3600s<t<7200s: data is partially outside the window, so the
|
||||
// integral shrinks linearly from 3600 to 0 as t approaches 7200s.
|
||||
if ti > 3600_000 && ti < 7200_000 {
|
||||
expV := float64((7200_000 - ti) / 1e3)
|
||||
if v != expV {
|
||||
t.Fatalf("unexpected integrate result at t=%ds, want=%.3f got=%.3f", ti/1e3, expV, v)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if ti >= 7200_000 {
|
||||
// Window entirely past data end: must be NaN.
|
||||
if !math.IsNaN(v) {
|
||||
t.Fatalf("unexpected integrate result at t=%ds, want=NaN got=%.3f", ti/1e3, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
t.Run("distinct_over_time_1", func(t *testing.T) {
|
||||
rc := rollupConfig{
|
||||
Func: rollupDistinct,
|
||||
|
||||
@@ -90,6 +90,7 @@ var transformFuncs = map[string]transformFunc{
|
||||
"rand": newTransformRand(newRandFloat64),
|
||||
"rand_exponential": newTransformRand(newRandExpFloat64),
|
||||
"rand_normal": newTransformRand(newRandNormFloat64),
|
||||
"range": newTransformFuncZeroArgs(transformRange),
|
||||
"range_avg": newTransformFuncRange(runningAvg),
|
||||
"range_first": transformRangeFirst,
|
||||
"range_last": transformRangeLast,
|
||||
@@ -2808,6 +2809,10 @@ func transformEnd(tfa *transformFuncArg) float64 {
|
||||
return float64(tfa.ec.End) / 1e3
|
||||
}
|
||||
|
||||
func transformRange(tfa *transformFuncArg) float64 {
|
||||
return float64(tfa.ec.End-tfa.ec.Start) / 1e3
|
||||
}
|
||||
|
||||
// copyTimeseries returns a copy of tss.
|
||||
func copyTimeseries(tss []*timeseries) []*timeseries {
|
||||
rvs := make([]*timeseries, len(tss))
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -37,7 +37,7 @@
|
||||
<meta property="og:title" content="UI for VictoriaMetrics">
|
||||
<meta property="og:url" content="https://victoriametrics.com/">
|
||||
<meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data">
|
||||
<script type="module" crossorigin src="./assets/index-BjJ7fDL7.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index-U3iNn2Tx.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/rolldown-runtime-COnpUsM8.js">
|
||||
<link rel="modulepreload" crossorigin href="./assets/vendor-C8Kwp93_.js">
|
||||
<link rel="stylesheet" crossorigin href="./assets/vendor-CnsZ1jie.css">
|
||||
|
||||
@@ -51,6 +51,12 @@ var (
|
||||
retentionTimezoneOffset = flag.Duration("retentionTimezoneOffset", 0, "The offset for performing indexdb rotation. "+
|
||||
"If set to 0, then the indexdb rotation is performed at 4am UTC time per each -retentionPeriod. "+
|
||||
"If set to 2h, then the indexdb rotation is performed at 4am EET time (the timezone with +2h offset)")
|
||||
minScrapeInterval = flag.Duration("dedup.minScrapeInterval", 0, "Leave only the last sample in every time series per each discrete interval "+
|
||||
"equal to -dedup.minScrapeInterval > 0. See also -streamAggr.dedupInterval and https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#deduplication")
|
||||
inmemoryDataFlushInterval = flag.Duration("inmemoryDataFlushInterval", 5*time.Second, "The interval for guaranteed saving of in-memory data to disk. "+
|
||||
"The saved data survives unclean shutdowns such as OOM crash, hardware reset, SIGKILL, etc. "+
|
||||
"Bigger intervals may help increase the lifetime of flash storage with limited write cycles (e.g. Raspberry PI). "+
|
||||
"Smaller intervals increase disk IO load. Minimum supported value is 1s")
|
||||
|
||||
logNewSeries = flag.Bool("logNewSeries", false, "Whether to log new series. This option is for debug purposes only. It can lead to performance issues "+
|
||||
"when big number of new series are ingested into VictoriaMetrics")
|
||||
@@ -68,6 +74,11 @@ var (
|
||||
|
||||
minFreeDiskSpaceBytes = flagutil.NewBytes("storage.minFreeDiskSpaceBytes", 100e6, "The minimum free disk space at -storageDataPath after which the storage stops accepting new data")
|
||||
|
||||
finalDedupScheduleInterval = flag.Duration("storage.finalDedupScheduleCheckInterval", time.Hour, "The interval for checking when final deduplication process should be started."+
|
||||
"Storage unconditionally adds 25% jitter to the interval value on each check evaluation."+
|
||||
" Changing the interval to the bigger values may delay downsampling, deduplication for historical data."+
|
||||
" See also https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#deduplication")
|
||||
|
||||
cacheSizeStorageTSID = flagutil.NewBytes("storage.cacheSizeStorageTSID", 0, "Overrides max size for storage/tsid cache. "+
|
||||
"See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#cache-tuning")
|
||||
cacheSizeStorageMetricName = flagutil.NewBytes("storage.cacheSizeStorageMetricName", 0, "Overrides max size for storage/metricName cache. "+
|
||||
@@ -111,11 +122,16 @@ func Init(resetCacheIfNeeded func(mrs []storage.MetricRow)) {
|
||||
logger.Fatalf("invalid `-precisionBits`: %s", err)
|
||||
}
|
||||
|
||||
resetResponseCacheIfNeeded = resetCacheIfNeeded
|
||||
storage.SetDedupInterval(*minScrapeInterval)
|
||||
storage.SetDataFlushInterval(*inmemoryDataFlushInterval)
|
||||
storage.LegacySetRetentionTimezoneOffset(*retentionTimezoneOffset)
|
||||
storage.SetFreeDiskSpaceLimit(minFreeDiskSpaceBytes.N)
|
||||
storage.SetTSIDCacheSize(cacheSizeStorageTSID.IntN())
|
||||
storage.SetTagFiltersCacheSize(cacheSizeIndexDBTagFilters.IntN())
|
||||
if *finalDedupScheduleInterval < time.Hour {
|
||||
logger.Fatalf("-storage.finalDedupScheduleCheckInterval cannot be smaller than 1 hour; got %s", *finalDedupScheduleInterval)
|
||||
}
|
||||
storage.SetFinalDedupScheduleInterval(*finalDedupScheduleInterval)
|
||||
storage.SetMetricNamesStatsCacheSize(cacheSizeMetricNamesStats.IntN())
|
||||
storage.SetMetricNameCacheSize(cacheSizeStorageMetricName.IntN())
|
||||
storage.SetMetadataStorageSize(metadataStorageSize.IntN())
|
||||
@@ -134,9 +150,9 @@ func Init(resetCacheIfNeeded func(mrs []storage.MetricRow)) {
|
||||
if *idbPrefillStart > 23*time.Hour {
|
||||
logger.Panicf("-storage.idbPrefillStart cannot exceed 23 hours; got %s", idbPrefillStart)
|
||||
}
|
||||
fs.RegisterPathFsMetrics(*storageDataPath)
|
||||
logger.Infof("opening storage at %q with -retentionPeriod=%s", *storageDataPath, retentionPeriod)
|
||||
startTime := time.Now()
|
||||
WG = syncwg.WaitGroup{}
|
||||
opts := storage.OpenOptions{
|
||||
Retention: retentionPeriod.Duration(),
|
||||
FutureRetention: futureRetention.Duration(),
|
||||
@@ -149,7 +165,6 @@ func Init(resetCacheIfNeeded func(mrs []storage.MetricRow)) {
|
||||
LogNewSeries: *logNewSeries,
|
||||
}
|
||||
strg := storage.MustOpenStorage(*storageDataPath, opts)
|
||||
Storage = strg
|
||||
initStaleSnapshotsRemover(strg)
|
||||
|
||||
var m storage.Metrics
|
||||
@@ -168,7 +183,10 @@ func Init(resetCacheIfNeeded func(mrs []storage.MetricRow)) {
|
||||
writeStorageMetrics(w, strg)
|
||||
})
|
||||
metrics.RegisterSet(storageMetrics)
|
||||
fs.RegisterPathFsMetrics(*storageDataPath)
|
||||
|
||||
WG = syncwg.WaitGroup{}
|
||||
resetResponseCacheIfNeeded = resetCacheIfNeeded
|
||||
Storage = strg
|
||||
}
|
||||
|
||||
var storageMetrics *metrics.Set
|
||||
@@ -340,6 +358,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
startTime := time.Now()
|
||||
if err := Storage.ForceMergePartitions(partitionNamePrefix); err != nil {
|
||||
logger.Errorf("error in forced merge for partition_prefix=%q: %s", partitionNamePrefix, err)
|
||||
return
|
||||
}
|
||||
logger.Infof("forced merge for partition_prefix=%q has been successfully finished in %.3f seconds", partitionNamePrefix, time.Since(startTime).Seconds())
|
||||
}()
|
||||
@@ -353,6 +372,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
Storage.DebugFlush()
|
||||
return true
|
||||
}
|
||||
|
||||
if path == "/internal/log_new_series" {
|
||||
if !httpserver.CheckAuthFlag(w, r, logNewSeriesAuthKey) {
|
||||
return true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export const seriesFetchedWarning = `No match!
|
||||
export const seriesFetchedWarning = `No match!
|
||||
This query hasn't selected any time series from database.
|
||||
Either the requested metrics are missing in the database,
|
||||
or there is a typo in series selector.`;
|
||||
|
||||
export const partialWarning = `The shown results are marked as PARTIAL.
|
||||
The result is marked as partial if one or more vmstorage nodes failed to respond to the query.`;
|
||||
The result is marked as partial if one or more storage nodes failed to respond to the query.`;
|
||||
|
||||
@@ -71,7 +71,7 @@ const RulesHeader = ({
|
||||
<TextField
|
||||
label="Search"
|
||||
value={search}
|
||||
placeholder="Filter by rule, name or labels"
|
||||
placeholder="Filter by group or rule name"
|
||||
startIcon={<SearchIcon />}
|
||||
onChange={onChangeSearch}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Component, FC, Ref } from "preact/compat";
|
||||
import classNames from "classnames";
|
||||
import { getCssVariable } from "../../../utils/theme";
|
||||
import { TabItemType } from "./Tabs";
|
||||
import TabItemWrapper from "./TabItemWrapper";
|
||||
import "./style.scss";
|
||||
@@ -8,7 +7,6 @@ import "./style.scss";
|
||||
interface TabItemProps {
|
||||
activeItem: string
|
||||
item: TabItemType
|
||||
color?: string
|
||||
onChange?: (value: string) => void
|
||||
activeNavRef: Ref<Component>
|
||||
isNavLink?: boolean
|
||||
@@ -17,7 +15,6 @@ interface TabItemProps {
|
||||
const TabItem: FC<TabItemProps> = ({
|
||||
activeItem,
|
||||
item,
|
||||
color = getCssVariable("color-primary"),
|
||||
activeNavRef,
|
||||
onChange,
|
||||
isNavLink
|
||||
@@ -35,7 +32,6 @@ const TabItem: FC<TabItemProps> = ({
|
||||
})}
|
||||
isNavLink={isNavLink}
|
||||
to={item.value}
|
||||
style={{ color: color }}
|
||||
onClick={createHandlerClickTab(item.value)}
|
||||
ref={activeItem === item.value ? activeNavRef : undefined}
|
||||
>
|
||||
|
||||
@@ -6,7 +6,6 @@ interface TabItemWrapperProps {
|
||||
to: string
|
||||
isNavLink?: boolean
|
||||
className: string
|
||||
style: { color: string }
|
||||
children: ReactNode
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Component, FC, useRef, useState } from "preact/compat";
|
||||
import { ReactNode, useEffect } from "react";
|
||||
import { getCssVariable } from "../../../utils/theme";
|
||||
import TabItem from "./TabItem";
|
||||
import "./style.scss";
|
||||
import useWindowSize from "../../../hooks/useWindowSize";
|
||||
@@ -15,7 +14,6 @@ export interface TabItemType {
|
||||
interface TabsProps {
|
||||
activeItem: string
|
||||
items: TabItemType[]
|
||||
color?: string
|
||||
onChange?: (value: string) => void
|
||||
indicatorPlacement?: "bottom" | "top"
|
||||
isNavLink?: boolean
|
||||
@@ -24,7 +22,6 @@ interface TabsProps {
|
||||
const Tabs: FC<TabsProps> = ({
|
||||
activeItem,
|
||||
items,
|
||||
color = getCssVariable("color-primary"),
|
||||
onChange,
|
||||
indicatorPlacement = "bottom",
|
||||
isNavLink,
|
||||
@@ -48,14 +45,13 @@ const Tabs: FC<TabsProps> = ({
|
||||
activeItem={activeItem}
|
||||
item={item}
|
||||
onChange={onChange}
|
||||
color={color}
|
||||
activeNavRef={activeNavRef}
|
||||
isNavLink={isNavLink}
|
||||
/>
|
||||
))}
|
||||
<div
|
||||
className="vm-tabs__indicator"
|
||||
style={{ ...indicatorPosition, borderColor: color }}
|
||||
style={{ ...indicatorPosition }}
|
||||
/>
|
||||
</div>;
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $padding-global $padding-small;
|
||||
color: inherit;
|
||||
color: $color-primary;
|
||||
text-decoration: none;
|
||||
text-transform: capitalize;
|
||||
font-size: inherit;
|
||||
@@ -46,5 +46,6 @@
|
||||
position: absolute;
|
||||
border-bottom: 2px solid;
|
||||
transition: width 200ms ease, left 300ms cubic-bezier(0.280, 0.840, 0.420, 1);
|
||||
border-color: $color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
35
app/vmui/packages/vmui/src/state/customPanel/reducer.test.ts
Normal file
35
app/vmui/packages/vmui/src/state/customPanel/reducer.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { afterEach, describe, expect, it, vi, type Mock } from "vitest";
|
||||
import { getFromStorage, saveToStorage } from "../../utils/storage";
|
||||
|
||||
vi.mock("../../utils/storage", () => ({
|
||||
getFromStorage: vi.fn(),
|
||||
saveToStorage: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("customPanel reducer", () => {
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("persists reduceMemUsage under its own storage key", async () => {
|
||||
const { reducer, initialCustomPanelState } = await import("./reducer");
|
||||
|
||||
reducer(initialCustomPanelState, { type: "TOGGLE_REDUCE_MEM_USAGE" });
|
||||
|
||||
expect(saveToStorage).toHaveBeenCalledWith("REDUCE_MEM_USAGE", true);
|
||||
expect(saveToStorage).not.toHaveBeenCalledWith("TABLE_COMPACT", true);
|
||||
});
|
||||
|
||||
it("hydrates reduceMemUsage from storage", async () => {
|
||||
const getFromStorageMock = getFromStorage as Mock;
|
||||
getFromStorageMock.mockImplementation((key: string) => {
|
||||
if (key === "REDUCE_MEM_USAGE") return true;
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const { initialCustomPanelState } = await import("./reducer");
|
||||
|
||||
expect(initialCustomPanelState.reduceMemUsage).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -35,7 +35,7 @@ export const initialCustomPanelState: CustomPanelState = {
|
||||
isTracingEnabled: false,
|
||||
seriesLimits: limitsStorage ? JSON.parse(limitsStorage) : DEFAULT_MAX_SERIES,
|
||||
tableCompact: getFromStorage("TABLE_COMPACT") as boolean || false,
|
||||
reduceMemUsage: false
|
||||
reduceMemUsage: getFromStorage("REDUCE_MEM_USAGE") as boolean || false
|
||||
};
|
||||
|
||||
export function reducer(state: CustomPanelState, action: CustomPanelAction): CustomPanelState {
|
||||
@@ -69,7 +69,7 @@ export function reducer(state: CustomPanelState, action: CustomPanelAction): Cus
|
||||
tableCompact: !state.tableCompact
|
||||
};
|
||||
case "TOGGLE_REDUCE_MEM_USAGE":
|
||||
saveToStorage("TABLE_COMPACT", !state.reduceMemUsage);
|
||||
saveToStorage("REDUCE_MEM_USAGE", !state.reduceMemUsage);
|
||||
return {
|
||||
...state,
|
||||
reduceMemUsage: !state.reduceMemUsage
|
||||
|
||||
@@ -7,6 +7,7 @@ export const ALL_STORAGE_KEYS = [
|
||||
"SERIES_LIMITS",
|
||||
"LEGEND_AUTO_COLLAPSE",
|
||||
"TABLE_COMPACT",
|
||||
"REDUCE_MEM_USAGE",
|
||||
"TIMEZONE",
|
||||
"DISABLED_DEFAULT_TIMEZONE",
|
||||
"THEME",
|
||||
|
||||
@@ -79,24 +79,25 @@ type PrometheusWriteQuerier interface {
|
||||
|
||||
// QueryOpts contains various params used for querying or ingesting data
|
||||
type QueryOpts struct {
|
||||
Tenant string
|
||||
Timeout string
|
||||
Start string
|
||||
End string
|
||||
Time string
|
||||
Step string
|
||||
ExtraFilters []string
|
||||
ExtraLabels []string
|
||||
Trace string
|
||||
ReduceMemUsage string
|
||||
MaxLookback string
|
||||
LatencyOffset string
|
||||
Format string
|
||||
NoCache string
|
||||
Headers http.Header
|
||||
From string
|
||||
Until string
|
||||
StorageStep string
|
||||
Tenant string
|
||||
Timeout string
|
||||
Start string
|
||||
End string
|
||||
Time string
|
||||
Step string
|
||||
ExtraFilters []string
|
||||
ExtraLabels []string
|
||||
Trace string
|
||||
ReduceMemUsage string
|
||||
MaxLookback string
|
||||
LatencyOffset string
|
||||
Format string
|
||||
NoCache string
|
||||
Headers http.Header
|
||||
From string
|
||||
Until string
|
||||
StorageStep string
|
||||
DenyPartialResponse string
|
||||
}
|
||||
|
||||
func (qos *QueryOpts) getHeaders() http.Header {
|
||||
@@ -132,6 +133,7 @@ func (qos *QueryOpts) asURLValues() url.Values {
|
||||
addNonEmpty("from", qos.From)
|
||||
addNonEmpty("until", qos.Until)
|
||||
addNonEmpty("storage_step", qos.StorageStep)
|
||||
addNonEmpty("deny_partial_response", qos.DenyPartialResponse)
|
||||
|
||||
return uv
|
||||
}
|
||||
|
||||
@@ -1015,35 +1015,42 @@ func testGroupSkipSlowReplicas(tc *apptest.TestCase, opts *testGroupReplicationO
|
||||
func testGroupPartialResponse(tc *apptest.TestCase, opts *testGroupReplicationOpts) {
|
||||
t := tc.T()
|
||||
|
||||
assertSeries := func(app *apptest.Vmselect, wantPartial bool) {
|
||||
assertSeries := func(app *apptest.Vmselect, denyPartialResponse string, want *apptest.PrometheusAPIV1SeriesResponse) {
|
||||
t.Helper()
|
||||
tc.Assert(&apptest.AssertOptions{
|
||||
Msg: "unexpected /api/v1/series response",
|
||||
Got: func() any {
|
||||
return app.PrometheusAPIV1Series(t, `{__name__=~".*"}`, apptest.QueryOpts{
|
||||
Start: "2024-01-01T00:00:00Z",
|
||||
End: "2024-01-31T00:00:00Z",
|
||||
Start: "2024-01-01T00:00:00Z",
|
||||
End: "2024-01-31T00:00:00Z",
|
||||
DenyPartialResponse: denyPartialResponse,
|
||||
}).Sort()
|
||||
},
|
||||
Want: &apptest.PrometheusAPIV1SeriesResponse{
|
||||
Status: "success",
|
||||
IsPartial: wantPartial,
|
||||
},
|
||||
Want: want,
|
||||
CmpOpts: []cmp.Option{
|
||||
cmpopts.IgnoreFields(apptest.PrometheusAPIV1SeriesResponse{}, "Data"),
|
||||
cmpopts.IgnoreFields(apptest.PrometheusAPIV1SeriesResponse{}, "Data", "Error"),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
mustReturnPartialResponse := true
|
||||
mustReturnFullResponse := false
|
||||
allowPartialResponse := ""
|
||||
denyPartialResponse := "1"
|
||||
|
||||
mustReturnPartialResponse := &apptest.PrometheusAPIV1SeriesResponse{
|
||||
Status: "success",
|
||||
IsPartial: true,
|
||||
}
|
||||
mustReturnFullResponse := &apptest.PrometheusAPIV1SeriesResponse{
|
||||
Status: "success",
|
||||
IsPartial: false,
|
||||
}
|
||||
|
||||
// All vmstorage replicas are available so both vmselects must return full
|
||||
// response.
|
||||
assertSeries(opts.c.vmselect, mustReturnFullResponse)
|
||||
assertSeries(opts.c.vmselectGroupRF, mustReturnFullResponse)
|
||||
assertSeries(opts.c.vmselectGlobalRF, mustReturnFullResponse)
|
||||
assertSeries(opts.c.vmselectGroupGlobalRF, mustReturnFullResponse)
|
||||
assertSeries(opts.c.vmselect, allowPartialResponse, mustReturnFullResponse)
|
||||
assertSeries(opts.c.vmselectGroupRF, allowPartialResponse, mustReturnFullResponse)
|
||||
assertSeries(opts.c.vmselectGlobalRF, allowPartialResponse, mustReturnFullResponse)
|
||||
assertSeries(opts.c.vmselectGroupGlobalRF, allowPartialResponse, mustReturnFullResponse)
|
||||
|
||||
// Stop groupRF-1 vmstorage nodes in first group.
|
||||
//
|
||||
@@ -1053,10 +1060,10 @@ func testGroupPartialResponse(tc *apptest.TestCase, opts *testGroupReplicationOp
|
||||
// about the replication factor and therefore they must still be able to
|
||||
// return full dataset.
|
||||
opts.c.storageGroups[0].stopNodes(tc, opts.groupRF-1)
|
||||
assertSeries(opts.c.vmselect, mustReturnPartialResponse)
|
||||
assertSeries(opts.c.vmselectGroupRF, mustReturnFullResponse)
|
||||
assertSeries(opts.c.vmselectGlobalRF, mustReturnFullResponse)
|
||||
assertSeries(opts.c.vmselectGroupGlobalRF, mustReturnFullResponse)
|
||||
assertSeries(opts.c.vmselect, allowPartialResponse, mustReturnPartialResponse)
|
||||
assertSeries(opts.c.vmselectGroupRF, allowPartialResponse, mustReturnFullResponse)
|
||||
assertSeries(opts.c.vmselectGlobalRF, allowPartialResponse, mustReturnFullResponse)
|
||||
assertSeries(opts.c.vmselectGroupGlobalRF, allowPartialResponse, mustReturnFullResponse)
|
||||
|
||||
// Stop groupRF-1 vmstorages in the remaining groups.
|
||||
//
|
||||
@@ -1066,10 +1073,10 @@ func testGroupPartialResponse(tc *apptest.TestCase, opts *testGroupReplicationOp
|
||||
for g := 1; g < len(opts.c.storageGroups); g++ {
|
||||
opts.c.storageGroups[g].stopNodes(tc, opts.groupRF-1)
|
||||
}
|
||||
assertSeries(opts.c.vmselect, mustReturnPartialResponse)
|
||||
assertSeries(opts.c.vmselectGroupRF, mustReturnFullResponse)
|
||||
assertSeries(opts.c.vmselectGlobalRF, mustReturnPartialResponse)
|
||||
assertSeries(opts.c.vmselectGroupGlobalRF, mustReturnFullResponse)
|
||||
assertSeries(opts.c.vmselect, allowPartialResponse, mustReturnPartialResponse)
|
||||
assertSeries(opts.c.vmselectGroupRF, allowPartialResponse, mustReturnFullResponse)
|
||||
assertSeries(opts.c.vmselectGlobalRF, allowPartialResponse, mustReturnPartialResponse)
|
||||
assertSeries(opts.c.vmselectGroupGlobalRF, allowPartialResponse, mustReturnFullResponse)
|
||||
|
||||
// Stop one more vmstorage in the first group.
|
||||
//
|
||||
@@ -1077,10 +1084,10 @@ func testGroupPartialResponse(tc *apptest.TestCase, opts *testGroupReplicationOp
|
||||
// because it is unaware of replication across groups. vmselectGroupGlobalRF
|
||||
// will continue retuning full dataset.
|
||||
opts.c.storageGroups[0].stopNodes(tc, 1)
|
||||
assertSeries(opts.c.vmselect, mustReturnPartialResponse)
|
||||
assertSeries(opts.c.vmselectGroupRF, mustReturnPartialResponse)
|
||||
assertSeries(opts.c.vmselectGlobalRF, mustReturnPartialResponse)
|
||||
assertSeries(opts.c.vmselectGroupGlobalRF, mustReturnFullResponse)
|
||||
assertSeries(opts.c.vmselect, allowPartialResponse, mustReturnPartialResponse)
|
||||
assertSeries(opts.c.vmselectGroupRF, allowPartialResponse, mustReturnPartialResponse)
|
||||
assertSeries(opts.c.vmselectGlobalRF, allowPartialResponse, mustReturnPartialResponse)
|
||||
assertSeries(opts.c.vmselectGroupGlobalRF, allowPartialResponse, mustReturnFullResponse)
|
||||
|
||||
// Stop one more vmstoarge in remaining globarRF-1 groups.
|
||||
//
|
||||
@@ -1089,19 +1096,56 @@ func testGroupPartialResponse(tc *apptest.TestCase, opts *testGroupReplicationOp
|
||||
for g := 1; g < opts.globalRF-1; g++ {
|
||||
opts.c.storageGroups[g].stopNodes(tc, 1)
|
||||
}
|
||||
assertSeries(opts.c.vmselect, mustReturnPartialResponse)
|
||||
assertSeries(opts.c.vmselectGroupRF, mustReturnPartialResponse)
|
||||
assertSeries(opts.c.vmselectGlobalRF, mustReturnPartialResponse)
|
||||
assertSeries(opts.c.vmselectGroupGlobalRF, mustReturnFullResponse)
|
||||
assertSeries(opts.c.vmselect, allowPartialResponse, mustReturnPartialResponse)
|
||||
assertSeries(opts.c.vmselectGroupRF, allowPartialResponse, mustReturnPartialResponse)
|
||||
assertSeries(opts.c.vmselectGlobalRF, allowPartialResponse, mustReturnPartialResponse)
|
||||
assertSeries(opts.c.vmselectGroupGlobalRF, allowPartialResponse, mustReturnFullResponse)
|
||||
|
||||
// Stop one more vmstoarge in one more group.
|
||||
//
|
||||
// vmselectGroupGlobalRF must now return partial dataset.
|
||||
opts.c.storageGroups[opts.globalRF].stopNodes(tc, 1)
|
||||
assertSeries(opts.c.vmselect, mustReturnPartialResponse)
|
||||
assertSeries(opts.c.vmselectGroupRF, mustReturnPartialResponse)
|
||||
assertSeries(opts.c.vmselectGlobalRF, mustReturnPartialResponse)
|
||||
assertSeries(opts.c.vmselectGroupGlobalRF, mustReturnPartialResponse)
|
||||
assertSeries(opts.c.vmselect, allowPartialResponse, mustReturnPartialResponse)
|
||||
assertSeries(opts.c.vmselectGroupRF, allowPartialResponse, mustReturnPartialResponse)
|
||||
assertSeries(opts.c.vmselectGlobalRF, allowPartialResponse, mustReturnPartialResponse)
|
||||
assertSeries(opts.c.vmselectGroupGlobalRF, allowPartialResponse, mustReturnPartialResponse)
|
||||
|
||||
// Stop all the remaining vmstorage nodes except a single node.
|
||||
//
|
||||
// At this point vmselects still must be able to return partial response
|
||||
// because at least one vmstorage node has successfully returned results.
|
||||
n := len(opts.c.storageGroups[0].vmstorages)
|
||||
opts.c.storageGroups[0].stopNodes(tc, n-1)
|
||||
for g := 1; g < len(opts.c.storageGroups); g++ {
|
||||
n := len(opts.c.storageGroups[g].vmstorages)
|
||||
opts.c.storageGroups[g].stopNodes(tc, n)
|
||||
}
|
||||
assertSeries(opts.c.vmselect, allowPartialResponse, mustReturnPartialResponse)
|
||||
assertSeries(opts.c.vmselectGroupRF, allowPartialResponse, mustReturnPartialResponse)
|
||||
assertSeries(opts.c.vmselectGlobalRF, allowPartialResponse, mustReturnPartialResponse)
|
||||
assertSeries(opts.c.vmselectGroupGlobalRF, allowPartialResponse, mustReturnPartialResponse)
|
||||
|
||||
mustReturnUnavailableError := &apptest.PrometheusAPIV1SeriesResponse{
|
||||
Status: "error",
|
||||
ErrorType: "503",
|
||||
}
|
||||
|
||||
// vmselects must return an error for the same request when partial
|
||||
// responses are denied explicitly.
|
||||
assertSeries(opts.c.vmselect, denyPartialResponse, mustReturnUnavailableError)
|
||||
assertSeries(opts.c.vmselectGroupRF, denyPartialResponse, mustReturnUnavailableError)
|
||||
assertSeries(opts.c.vmselectGlobalRF, denyPartialResponse, mustReturnUnavailableError)
|
||||
assertSeries(opts.c.vmselectGroupGlobalRF, denyPartialResponse, mustReturnUnavailableError)
|
||||
|
||||
// Stop the last remaining vmstorage node.
|
||||
//
|
||||
// vmselects must return an error when there are no successful vmstorage
|
||||
// responses.
|
||||
opts.c.storageGroups[0].stopNodes(tc, 1)
|
||||
assertSeries(opts.c.vmselect, allowPartialResponse, mustReturnUnavailableError)
|
||||
assertSeries(opts.c.vmselectGroupRF, allowPartialResponse, mustReturnUnavailableError)
|
||||
assertSeries(opts.c.vmselectGlobalRF, allowPartialResponse, mustReturnUnavailableError)
|
||||
assertSeries(opts.c.vmselectGroupGlobalRF, allowPartialResponse, mustReturnUnavailableError)
|
||||
}
|
||||
|
||||
// TestClusterReplication_PartialResponseMultitenant checks how vmselect handles some
|
||||
|
||||
BIN
apptest/tests/testdata/mimir-tsdb/anonymous/01JFJBS3YP1SHZ3PJQ6HK76EC3/chunks/000001
vendored
Normal file
BIN
apptest/tests/testdata/mimir-tsdb/anonymous/01JFJBS3YP1SHZ3PJQ6HK76EC3/chunks/000001
vendored
Normal file
Binary file not shown.
BIN
apptest/tests/testdata/mimir-tsdb/anonymous/01JFJBS3YP1SHZ3PJQ6HK76EC3/index
vendored
Normal file
BIN
apptest/tests/testdata/mimir-tsdb/anonymous/01JFJBS3YP1SHZ3PJQ6HK76EC3/index
vendored
Normal file
Binary file not shown.
51
apptest/tests/testdata/mimir-tsdb/anonymous/01JFJBS3YP1SHZ3PJQ6HK76EC3/meta.json
vendored
Normal file
51
apptest/tests/testdata/mimir-tsdb/anonymous/01JFJBS3YP1SHZ3PJQ6HK76EC3/meta.json
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"ulid": "01JFJBS3YP1SHZ3PJQ6HK76EC3",
|
||||
"minTime": 1734709200000,
|
||||
"maxTime": 1734709320000,
|
||||
"stats": {
|
||||
"numSamples": 400,
|
||||
"numSeries": 100,
|
||||
"numChunks": 100
|
||||
},
|
||||
"compaction": {
|
||||
"level": 1,
|
||||
"sources": [
|
||||
"01JFJBS3YP1SHZ3PJQ6HK76EC3"
|
||||
],
|
||||
"parents": [
|
||||
{
|
||||
"ulid": "00000000000000000000000000",
|
||||
"minTime": 0,
|
||||
"maxTime": 0
|
||||
}
|
||||
],
|
||||
"hints": [
|
||||
"from-out-of-order"
|
||||
]
|
||||
},
|
||||
"version": 1,
|
||||
"out_of_order": false,
|
||||
"thanos": {
|
||||
"labels": {},
|
||||
"downsample": {
|
||||
"resolution": 0
|
||||
},
|
||||
"source": "receive",
|
||||
"segment_files": [
|
||||
"000001"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"rel_path": "chunks/000001",
|
||||
"size_bytes": 4808
|
||||
},
|
||||
{
|
||||
"rel_path": "index",
|
||||
"size_bytes": 55021
|
||||
},
|
||||
{
|
||||
"rel_path": "meta.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
apptest/tests/testdata/mimir-tsdb/anonymous/bucket-index.json.gz
vendored
Normal file
BIN
apptest/tests/testdata/mimir-tsdb/anonymous/bucket-index.json.gz
vendored
Normal file
Binary file not shown.
1
apptest/tests/testdata/mimir-tsdb/expected_response.json
vendored
Normal file
1
apptest/tests/testdata/mimir-tsdb/expected_response.json
vendored
Normal file
File diff suppressed because one or more lines are too long
139
apptest/tests/vmctl_mimir_migration_test.go
Normal file
139
apptest/tests/vmctl_mimir_migration_test.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/apptest"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||
)
|
||||
|
||||
const (
|
||||
testMimirPath = "testdata/mimir-tsdb"
|
||||
expectedMimirResponseFile = "./testdata/mimir-tsdb/expected_response.json"
|
||||
)
|
||||
|
||||
func TestSingleVmctlMimirProtocol(t *testing.T) {
|
||||
fs.MustRemoveDir(t.Name())
|
||||
|
||||
tc := apptest.NewTestCase(t)
|
||||
defer tc.Stop()
|
||||
|
||||
vmsingleDst := tc.MustStartDefaultVmsingle()
|
||||
vmAddr := fmt.Sprintf("http://%s/", vmsingleDst.HTTPAddr())
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("cannot get current working directory: %s", err)
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("fs://%s/%s", dir, testMimirPath)
|
||||
vmctlFlags := []string{
|
||||
`mimir`,
|
||||
`--mimir-tenant-id=anonymous`,
|
||||
`--mimir-filter-time-start=2024-12-01T00:00:00Z`,
|
||||
`--mimir-filter-time-end=2024-12-31T23:59:59Z`,
|
||||
`--mimir-custom-s3-endpoint=http://localhost:9000`,
|
||||
`--mimir-path=` + path,
|
||||
`--vm-addr=` + vmAddr,
|
||||
`--disable-progress-bar=true`,
|
||||
`--vm-concurrency=6`,
|
||||
`--mimir-concurrency=6`,
|
||||
}
|
||||
|
||||
testMimirProtocol(tc, vmsingleDst, vmctlFlags)
|
||||
}
|
||||
|
||||
func TestClusterVmctlMimirProtocol(t *testing.T) {
|
||||
fs.MustRemoveDir(t.Name())
|
||||
|
||||
tc := apptest.NewTestCase(t)
|
||||
defer tc.Stop()
|
||||
|
||||
cluster := tc.MustStartDefaultCluster()
|
||||
vmAddr := fmt.Sprintf("http://%s/", cluster.Vminsert.HTTPAddr())
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("cannot get current working directory: %s", err)
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("fs://%s/%s", dir, testMimirPath)
|
||||
|
||||
vmctlFlags := []string{
|
||||
`mimir`,
|
||||
`--mimir-tenant-id=anonymous`,
|
||||
`--mimir-filter-time-start=2024-12-01T00:00:00Z`,
|
||||
`--mimir-filter-time-end=2024-12-31T23:59:59Z`,
|
||||
`--mimir-custom-s3-endpoint=http://localhost:9000`,
|
||||
`--mimir-path=` + path,
|
||||
`--vm-addr=` + vmAddr,
|
||||
`--disable-progress-bar=true`,
|
||||
`--vm-concurrency=6`,
|
||||
`--mimir-concurrency=6`,
|
||||
}
|
||||
|
||||
testMimirProtocol(tc, cluster, vmctlFlags)
|
||||
}
|
||||
|
||||
func testMimirProtocol(tc *apptest.TestCase, sut apptest.PrometheusWriteQuerier, vmctlFlags []string) {
|
||||
t := tc.T()
|
||||
t.Helper()
|
||||
|
||||
cmpOpt := cmpopts.IgnoreFields(apptest.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType")
|
||||
|
||||
// test for empty data request
|
||||
got := sut.PrometheusAPIV1Query(t, `{__name__=~".*"}`, apptest.QueryOpts{
|
||||
Step: "5m",
|
||||
Time: "2025-06-02T17:14:00Z",
|
||||
})
|
||||
|
||||
want := apptest.NewPrometheusAPIV1QueryResponse(t, `{"data":{"result":[]}}`)
|
||||
if diff := cmp.Diff(want, got, cmpOpt); diff != "" {
|
||||
t.Errorf("unexpected response (-want, +got):\n%s", diff)
|
||||
}
|
||||
|
||||
tc.MustStartVmctl("vmctl", vmctlFlags)
|
||||
|
||||
sut.ForceFlush(t)
|
||||
|
||||
// open the expected series response file
|
||||
file, err := os.Open(expectedMimirResponseFile)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot open expected series response file: %s", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
bytes, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot read expected series response file: %s", err)
|
||||
}
|
||||
|
||||
var wantResponse apptest.PrometheusAPIV1QueryResponse
|
||||
if err := json.Unmarshal(bytes, &wantResponse); err != nil {
|
||||
t.Fatalf("cannot unmarshal expected series response file: %s", err)
|
||||
}
|
||||
wantResponse.Sort()
|
||||
|
||||
tc.Assert(&apptest.AssertOptions{
|
||||
// For cluster version, we need to wait longer for the metrics to be stored
|
||||
Retries: 300,
|
||||
Msg: `unexpected metrics stored on vmsingle via the prometheus protocol`,
|
||||
Got: func() any {
|
||||
expected := sut.PrometheusAPIV1Export(t, `{__name__=~".*"}`, apptest.QueryOpts{
|
||||
Start: "2024-12-01T15:31:10Z",
|
||||
End: "2024-12-31T15:32:20Z",
|
||||
})
|
||||
expected.Sort()
|
||||
return expected.Data.Result
|
||||
},
|
||||
Want: wantResponse.Data.Result,
|
||||
CmpOpts: []cmp.Option{
|
||||
cmpopts.IgnoreFields(apptest.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType"),
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -2804,10 +2804,10 @@
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"h": 7,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 352
|
||||
"y": 11
|
||||
},
|
||||
"id": 63,
|
||||
"options": {
|
||||
@@ -2843,7 +2843,113 @@
|
||||
],
|
||||
"title": "Restarts ($job)",
|
||||
"type": "timeseries"
|
||||
}
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "victoriametrics-metrics-datasource",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Group iteration reset can be caused by irregular delays during evaluation or by the system wall clock being moved backward.\nIf it is caused by host clock changes, vmalert could generate duplicate results for the group rules, since some evaluations could be repeated.\nCheck the host clock time synchronization configuration if this happens frequently.\n",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "bars",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"showValues": false,
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 11
|
||||
},
|
||||
"id": 70,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"mean",
|
||||
"lastNotNull",
|
||||
"max"
|
||||
],
|
||||
"displayMode": "table",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "multi",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.2.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "victoriametrics-metrics-datasource",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"exemplar": false,
|
||||
"expr": "sum(increase(vmalert_iteration_reset_total{job=~\"$job\", instance=~\"$instance\"}[$__rate_interval])) by(job, group, file) > 0",
|
||||
"interval": "1m",
|
||||
"legendFormat": "({{job}}) {{group}}({{file}})",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Group Iteration Reset ($instance)",
|
||||
"type": "timeseries"
|
||||
}
|
||||
],
|
||||
"title": "Troubleshooting",
|
||||
"type": "row"
|
||||
|
||||
@@ -2803,10 +2803,10 @@
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"h": 7,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 352
|
||||
"y": 11
|
||||
},
|
||||
"id": 63,
|
||||
"options": {
|
||||
@@ -2842,7 +2842,113 @@
|
||||
],
|
||||
"title": "Restarts ($job)",
|
||||
"type": "timeseries"
|
||||
}
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Group iteration reset can be caused by irregular delays during evaluation or by the system wall clock being moved backward.\nIf it is caused by host clock changes, vmalert could generate duplicate results for the group rules, since some evaluations could be repeated.\nCheck the host clock time synchronization configuration if this happens frequently.\n",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "bars",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"showValues": false,
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 11
|
||||
},
|
||||
"id": 70,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"mean",
|
||||
"lastNotNull",
|
||||
"max"
|
||||
],
|
||||
"displayMode": "table",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "multi",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.2.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"exemplar": false,
|
||||
"expr": "sum(increase(vmalert_iteration_reset_total{job=~\"$job\", instance=~\"$instance\"}[$__rate_interval])) by(job, group, file) > 0",
|
||||
"interval": "1m",
|
||||
"legendFormat": "({{job}}) {{group}}({{file}})",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Group Iteration Reset ($instance)",
|
||||
"type": "timeseries"
|
||||
}
|
||||
],
|
||||
"title": "Troubleshooting",
|
||||
"type": "row"
|
||||
|
||||
@@ -64,6 +64,18 @@ groups:
|
||||
group \"{{ $labels.group }}\". See https://docs.victoriametrics.com/victoriametrics/vmalert/#groups.
|
||||
If rule expressions are taking longer than expected, please see https://docs.victoriametrics.com/victoriametrics/troubleshooting/#slow-queries."
|
||||
|
||||
- alert: GroupIterationReset
|
||||
expr: increase(vmalert_iteration_reset_total[5m]) > 0
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Evaluation iteration for group {{ $labels.group }} in file {{ $labels.file }} is reset"
|
||||
description: "Evaluation iteration for group \"{{ $labels.group }}\" in file \"{{ $labels.file }}\" is reset on vmalert instance {{ $labels.instance }}.
|
||||
This can be caused by irregular delays during evaluation or by the system wall clock being moved backward. If it is caused by host clock changes, vmalert could
|
||||
generate duplicate results for the group rules since some evaluations could be repeated. Check host clock time synchronization configurations if this happens frequently."
|
||||
|
||||
|
||||
- alert: RemoteWriteErrors
|
||||
expr: increase(vmalert_remotewrite_errors_total[5m]) > 0
|
||||
for: 15m
|
||||
@@ -108,4 +120,3 @@ groups:
|
||||
summary: "vmalert instance {{ $labels.instance }} is failing to send notifications to Alertmanager"
|
||||
description: "vmalert instance {{ $labels.instance }} is failing to send alert notifications to \"{{ $labels.addr }}\".
|
||||
Check vmalert's logs for detailed error message."
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ groups:
|
||||
annotations:
|
||||
summary: "Metrics have not been seen from \"{{ $labels.job }}\"(\"{{ $labels.instance }}\") for {{ $value }} seconds"
|
||||
description: >
|
||||
The missing metric may indicate that vmanomaly is not running or is inaccessible from vmagent or the remotewrite endpoint.
|
||||
The missing metric may indicate that vmanomaly is not running or is inaccessible from vmagent or the remotewrite endpoint.
|
||||
|
||||
- alert: ProcessNearFDLimits
|
||||
expr: (process_max_fds{job=~".*vmanomaly.*"} - process_open_fds{job=~".*vmanomaly.*"}) < 100
|
||||
|
||||
@@ -150,12 +150,12 @@ You can experiment with your own data during the month‑long trial without depl
|
||||
are fast-booting Linux microVMs that run on a fleet of large bare-metal servers. You can start a playground right from your browser.
|
||||
Once up and running, accessing a playground is no different from SSH-ing into a remote server rented from your favorite VPS or Cloud provider.
|
||||
|
||||
Iximiuz Labs provides various [learning-by-doing resources for VictoriaMetrics](https://labs.iximiuz.com/v/victoriametrics-bb1fdaa1):
|
||||
Iximiuz Labs provides various [learning-by-doing resources for VictoriaMetrics](https://labs.iximiuz.com/v/victoriametrics):
|
||||
- Tutorial:
|
||||
- [Getting Started with VictoriaMetrics on Kubernetes](https://labs.iximiuz.com/tutorials/victoriametrics-getting-started-kubernetes-0e9c0993)
|
||||
- [Getting Started with VictoriaMetrics on Kubernetes](https://labs.iximiuz.com/tutorials/victoriametrics-getting-started-kubernetes)
|
||||
- Playgrounds:
|
||||
- [VictoriaMetrics single node](https://labs.iximiuz.com/playgrounds/victoriametrics-e2f9b613)
|
||||
- [VictoriaMetrics cluster](https://labs.iximiuz.com/playgrounds/victoriametrics-cluster-8eacb19d)
|
||||
- [VictoriaMetrics on Kubernetes](https://labs.iximiuz.com/playgrounds/victoriametrics-kubernetes-9eebc258)
|
||||
- [VictoriaMetrics single node](https://labs.iximiuz.com/playgrounds/victoriametrics)
|
||||
- [VictoriaMetrics cluster](https://labs.iximiuz.com/playgrounds/victoriametrics-cluster)
|
||||
- [VictoriaMetrics on Kubernetes](https://labs.iximiuz.com/playgrounds/victoriametrics-kubernetes)
|
||||
|
||||
Iximiuz Labs requires a [free account](https://labs.iximiuz.com/signup) to access the materials.
|
||||
@@ -205,13 +205,15 @@ curl 'http://vmselect:8481/select/multitenant/prometheus/api/v1/query' \
|
||||
|
||||
The precedence for applying filters for tenants follows this order:
|
||||
|
||||
1. Filter tenants by `extra_label` and `extra_filters` filters.
|
||||
1. Filter tenants by `extra_label`, `extra_filters` and `extra_filters[]` filters.
|
||||
These filters have the highest priority and are applied first when provided through the query arguments.
|
||||
Filters use `OR` logic - a tenant is selected if it matches any of the filters.
|
||||
2. Filter tenants from labels selectors defined at metricsQL query expression.
|
||||
|
||||
> **Security considerations**
|
||||
It is recommended restricting access to `multitenant` endpoints only to trusted sources,
|
||||
since untrusted source may break per-tenant data by writing unwanted samples or get access to data of arbitrary tenants.
|
||||
See also [vmauth security doc](https://docs.victoriametrics.com/victoriametrics/vmauth/#security).
|
||||
|
||||
## Binaries
|
||||
|
||||
|
||||
@@ -1357,7 +1357,7 @@ by replacing all the values bigger or equal to 30 with 40.
|
||||
`end()` is a [transform function](#transform-functions), which returns the unix timestamp in seconds for the last point.
|
||||
It is known as `end` query arg passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query).
|
||||
|
||||
See also [start](#start), [time](#time) and [now](#now).
|
||||
See also [start](#start), [time](#time), [now](#now) and [range](#range).
|
||||
|
||||
#### exp
|
||||
|
||||
@@ -1575,6 +1575,12 @@ with [normal distribution](https://en.wikipedia.org/wiki/Normal_distribution). O
|
||||
|
||||
See also [rand](#rand) and [rand_exponential](#rand_exponential).
|
||||
|
||||
#### range
|
||||
|
||||
`range()` is a [transform function](#transform-functions), which returns the range duration of the current query range evaluation in seconds and is equivalent to `end() - start()`.
|
||||
|
||||
See also [start](#start) and [end](#end).
|
||||
|
||||
#### range_avg
|
||||
|
||||
`range_avg(q)` is a [transform function](#transform-functions), which calculates the avg value across points per each time series returned by `q`.
|
||||
@@ -1789,7 +1795,7 @@ This function is supported by PromQL.
|
||||
|
||||
It is known as `start` query arg passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query).
|
||||
|
||||
See also [end](#end), [time](#time) and [now](#now).
|
||||
See also [end](#end), [time](#time), [now](#now) and [range](#range).
|
||||
|
||||
#### step
|
||||
|
||||
|
||||
@@ -1136,6 +1136,8 @@ By default, the last point on the interval `[now - max_lookback ... now]` is scr
|
||||
For instance, `/federate?match[]=up&max_lookback=1h` would return last points on the `[now - 1h ... now]` interval. This may be useful for time series federation
|
||||
with scrape intervals exceeding `5m`.
|
||||
|
||||
VictoriaMetrics supports Prometheus v3.0 utf-8 content encoding with `Accept` header. If `Accept: allow-utf-8` HTTP header provided, `/federate` API response changes according to [Prometheus utf-8](https://prometheus.io/docs/guides/utf8/#querying) specification - `metric_name{tag="value"}` transforms into `{"metric_name","tag"="value"}`.
|
||||
|
||||
## Capacity planning
|
||||
|
||||
VictoriaMetrics uses lower amounts of CPU, RAM and storage space on production workloads compared to competing solutions (Prometheus, Thanos, Cortex, TimescaleDB, InfluxDB, QuestDB, M3DB) according to [our case studies](https://docs.victoriametrics.com/victoriametrics/casestudies/).
|
||||
|
||||
@@ -26,7 +26,19 @@ See also [LTS releases](https://docs.victoriametrics.com/victoriametrics/lts-rel
|
||||
|
||||
## tip
|
||||
|
||||
* FEATURE: [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/): support `match[]=<label_selector>` query parameters in `/api/v1/rules` and `/api/v1/alerts` APIs to return only the rules that have configured labels satisfying the provided label selectors. See [11020](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11020).
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/), [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) and `vminsert` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): add `-opentelemetry.promoteAllResourceAttributes` and `-opentelemetry.promoteScopeMetadata` command-line flags to allow managing label promotion for resource attributes and OTel scope metadata. See [OpenTelemetry](https://docs.victoriametrics.com/victoriametrics/integrations/opentelemetry/) docs and [#10931](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10931).
|
||||
* FEATURE: [vmctl](https://docs.victoriametrics.com/vmctl/): add the ability to migrate data from Mimir object storage to VictoriaMetrics. See [#7717](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7717).
|
||||
* FEATURE: [MetricsQL](https://docs.victoriametrics.com/victoriametrics/metricsql/): support `range()` function, which returns the range duration of the current query range evaluation in seconds. See [#11028](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11028).
|
||||
|
||||
* BUGFIX: [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/): fix the `Notifiers` page in web UI appearing blank despite the API returning notifier data correctly. See [#11035](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11035).
|
||||
* BUGFIX: [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/): reset the group evaluation timestamp if it exceeds the current host time. Previously, vmalert could use future timestamps for evaluations if the system clock was shifted backward. See [#10985](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10985).
|
||||
* BUGFIX: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/) and [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/): properly parse [Prometheus Native Histograms](https://prometheus.io/docs/specs/native_histograms/), previously Protobuf parser could produce unexpected `vmrange` labels. See [#11041](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11041).
|
||||
* BUGFIX: [vmauth](https://docs.victoriametrics.com/victoriametrics/vmauth/): properly calculate number of loaded users to be printed in startup log. Previously, it was only accounting for static users and skipped JWT configuration entries.
|
||||
* BUGFIX: [MetricsQL](https://docs.victoriametrics.com/victoriametrics/metricsql/): `integrate()` no longer extrapolates the last sample's value past the end of the time series. Previously, querying `integrate(metric[1h])` at a timestamp where the series had already ended would keep accruing area as if the last value continued indefinitely, producing values much larger than the true integral. See [#9474](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9474). Thanks to @wtfashwin for contribution.
|
||||
* BUGFIX: `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): avoid returning HTTP 503 for queries with partial results when a storage group is unavailable and `-search.denyPartialResponse` is disabled.
|
||||
* BUGFIX: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) and `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): properly escape `utf-8` label names for [/federate](https://docs.victoriametrics.com/victoriametrics/#federation) API requests. See [#10968](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10968).
|
||||
* BUGFIX: [vmui](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui): persist the `Disable deduplication` toggle under its own local storage key. Before this fix, the toggle state was lost after reload and could overwrite the `Compact view` table setting. See [#11004](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11004). Thanks to @immanuwell for the contribution.
|
||||
|
||||
## [v1.144.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.144.0)
|
||||
|
||||
|
||||
@@ -173,7 +173,7 @@ Released at 2024-10-02
|
||||
|
||||
It is recommended upgrading to [v1.107.0](https://docs.victoriametrics.com/victoriametrics/changelog/#v11070) because [v1.104.0](https://docs.victoriametrics.com/victoriametrics/changelog/#v11040) contains a bug, which can lead to runtime panic at `vmselect` component. See this [issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7549) for details.
|
||||
|
||||
**Update note 1: `*.passwordFile` and similar flags are trimming trailing whitespaces at the end of content. If authorization check performed with `*.passwordFile` content, make sure to update authorization settings to not include trailing whitespaces before the upgrade. In case of [operator](https://docs.victoriametrics.com/operator/) managed installations, make sure to update operator version to [v0.48.*](https://docs.victoriametrics.com/operator/changelog/#v0480---25-sep-2024). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6986) for the details. This change reverts behavior introduced at [v1.102.0-rc2](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.102.0-rc2) release**
|
||||
**Update note 1: `*.passwordFile` and similar flags are trimming trailing whitespaces at the end of content. If authorization check performed with `*.passwordFile` content, make sure to update authorization settings to not include trailing whitespaces before the upgrade. In case of [operator](https://docs.victoriametrics.com/operator/) managed installations, make sure to update operator version to [v0.48.*](https://docs.victoriametrics.com/operator/changelog/#v0480). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6986) for the details. This change reverts behavior introduced at [v1.102.0-rc2](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.102.0-rc2) release**
|
||||
|
||||
* SECURITY: upgrade Go builder from Go1.23.0 to Go1.23.1. See the list of issues addressed in [Go1.23.1](https://github.com/golang/go/issues?q=milestone%3AGo1.23.1+label%3ACherryPickApproved).
|
||||
* SECURITY: upgrade base docker image (Alpine) from 3.20.2 to 3.20.3. See [alpine 3.20.3 release notes](https://alpinelinux.org/posts/Alpine-3.17.10-3.18.9-3.19.4-3.20.3-released.html).
|
||||
|
||||
@@ -502,7 +502,7 @@ See the docs at https://docs.victoriametrics.com/victoriametrics/vmagent/ .
|
||||
Supports array of values separated by comma or specified via multiple flags.
|
||||
Empty values are set to default value.
|
||||
-remoteWrite.roundDigits array
|
||||
Round metric values to this number of decimal digits after the point before writing them to remote storage. Examples: -remoteWrite.roundDigits=2 would round 1.236 to 1.24, while -remoteWrite.roundDigits=-1 would round 126.78 to 130. By default, digits rounding is disabled. Set it to 100 for disabling it for a particular remote storage. This option may be used for improving data compression for the stored metrics (default 100)
|
||||
Round metric values to this number of decimal digits after the point before writing them to remote storage. Examples: -remoteWrite.roundDigits=2 would round 1.236 to 1.24, while -remoteWrite.roundDigits=-1 would round 126.78 to 130. By default, digits rounding is disabled. Set it to 100 for disabling it for a particular remote storage. This option may be used for improving data compression for the stored metrics. See also -remoteWrite.significantFigures (default 100)
|
||||
Supports array of values separated by comma or specified via multiple flags.
|
||||
Empty values are set to default value.
|
||||
-remoteWrite.sendTimeout array
|
||||
|
||||
@@ -801,17 +801,15 @@ Please refer to the [VictoriaMetrics Cloud documentation](https://docs.victoriam
|
||||
`vmalert` runs a web-server (`-httpListenAddr`) for serving metrics and alerts endpoints:
|
||||
|
||||
* `http://<vmalert-addr>` - UI;
|
||||
* `http://<vmalert-addr>/api/v1/rules` - list of all loaded groups and rules. Supports `search`, `group_limit`, and `page_num` parameters, as well as additional [filtering](https://prometheus.io/docs/prometheus/latest/querying/api/#rules);
|
||||
* `http://<vmalert-addr>/api/v1/alerts` - list of all active alerts;
|
||||
* `http://<vmalert-addr>/api/v1/notifiers` - list all available notifiers;
|
||||
* `http://<vmalert-addr>/vmalert/api/v1/alert?group_id=<group_id>&alert_id=<alert_id>` - get alert status in JSON format.
|
||||
* `http://<vmalert-addr>/vmalert/api/v1/rule?group_id=<group_id>&rule_id=<rule_id>` - get rule status in JSON format.
|
||||
* `http://<vmalert-addr>/vmalert/api/v1/group?group_id=<group_id>` - get group status in JSON format.
|
||||
Used as alert source in AlertManager.
|
||||
* `http://<vmalert-addr>/vmalert/alert?group_id=<group_id>&alert_id=<alert_id>` - get alert status in web UI.
|
||||
* `http://<vmalert-addr>/vmalert/rule?group_id=<group_id>&rule_id=<rule_id>` - get rule status in web UI.
|
||||
* `http://<vmalert-addr>/vmalert/api/v1/rule?group_id=<group_id>&alert_id=<alert_id>` - get rule status in JSON format.
|
||||
* `http://<vmalert-addr>/metrics` - application metrics.
|
||||
* `http://<vmalert-addr>/api/v1/rules` - returns a list of all loaded groups and rules. Supports the `datasource_type`, `search`, `group_limit`, and `page_num` parameters, as well as additional [filtering](https://prometheus.io/docs/prometheus/latest/querying/api/#rules);
|
||||
* `http://<vmalert-addr>/api/v1/alerts` - returns a list of all active alerts. Supports the `datasource_type`, `rule_group[]`, `file[]` and `match[]`(applied on templated alert labels) query parameters;
|
||||
* `http://<vmalert-addr>/api/v1/notifiers` - returns a list of all available notifiers;
|
||||
* `http://<vmalert-addr>/vmalert/api/v1/alert?group_id=<group_id>&alert_id=<alert_id>` - returns the alert status in JSON format;
|
||||
* `http://<vmalert-addr>/vmalert/api/v1/rule?group_id=<group_id>&rule_id=<rule_id>` - returns the rule status in JSON format;
|
||||
* `http://<vmalert-addr>/vmalert/api/v1/group?group_id=<group_id>` - returns the group status in JSON format. Used as the alert source in AlertManager;
|
||||
* `http://<vmalert-addr>/vmalert/alert?group_id=<group_id>&alert_id=<alert_id>` - displays the alert status in the web UI;
|
||||
* `http://<vmalert-addr>/vmalert/rule?group_id=<group_id>&rule_id=<rule_id>` - displays the rule status in the web UI;
|
||||
* `http://<vmalert-addr>/metrics` - application metrics endpoint;
|
||||
* `http://<vmalert-addr>/-/reload` - hot configuration reload.
|
||||
|
||||
`vmalert` web UI can be accessed from [single-node version of VictoriaMetrics](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/)
|
||||
|
||||
@@ -1536,6 +1536,16 @@ To enable TLS on the public listener while keeping the internal listener non-TLS
|
||||
|
||||
`vmauth` also supports restricting access by IP - see [these docs](#ip-filters). See also [concurrency limiting docs](#concurrency-limiting).
|
||||
|
||||
|
||||
When `vmauth` performs tenant routing for [multitenant](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#multitenant-reads) requests, it is crucial to explicitly set `extra_label`, `extra_filters` and `extra_filters[]` in the url_prefix configuration:
|
||||
|
||||
```yaml
|
||||
unauthorized_user:
|
||||
url_prefix: http://vmselect/select/multitenant?extra_filters[]=&extra_filters=&extra_label=vm_account_id=10&extra_label=vm_project_id=100
|
||||
```
|
||||
|
||||
This is required because `vmselect` uses `OR` logic for tenant filtering. If a client sets `extra_filters[]` or `extra_filters`, it could bypass the tenant restriction configured via `extra_label`.
|
||||
|
||||
## Automatic issuing of TLS certificates
|
||||
|
||||
`vmauth` [Enterprise](https://docs.victoriametrics.com/victoriametrics/enterprise/) supports automatic issuing of TLS certificates via [Let's Encrypt service](https://letsencrypt.org/).
|
||||
|
||||
@@ -73,4 +73,66 @@ ou can define it via the flag `--remote-read-headers=X-Scope-OrgID:demo`.
|
||||
|
||||
See [remote-read mode](https://docs.victoriametrics.com/victoriametrics/vmctl/remoteread/) for more details.
|
||||
|
||||
See also general [vmctl migration tips](https://docs.victoriametrics.com/victoriametrics/vmctl/#migration-tips).
|
||||
See also general [vmctl migration tips](https://docs.victoriametrics.com/victoriametrics/vmctl/#migration-tips).
|
||||
|
||||
### Read data from the remote storage like S3, GCS, Azure etc.
|
||||
|
||||
If you have data stored in remote storage like S3, GCS, Azure etc. you can use `vmctl` in `mimir` mode to read data from
|
||||
the remote storage and import it into VictoriaMetrics. In this mode `vmctl` reads data from the remote storage or file system
|
||||
and checks index file, define needed blocks to be processed. After it downloads blocks by defined filters and
|
||||
use Prometheus converter to read and sent data to VictoriaMetrics.
|
||||
|
||||
The following example shows how to read data from the file system and import it into VictoriaMetrics:
|
||||
|
||||
```sh
|
||||
./vmctl mimir --mimir-path="fs:///mimir/test_data/mimir-tsdb" \ ? ? orbstack
|
||||
--mimir-tenant-id=anonymous \
|
||||
--mimir-filter-time-start=2024-12-01T00:00:00 \
|
||||
--mimir-filter-time-end=2024-12-18T23:59:59 \
|
||||
--mimir-creds-file-path=creads \
|
||||
--vm-concurrency=6 \
|
||||
--mimir-concurrency=6 \
|
||||
--vm-addr=http://localhost:8428/
|
||||
```
|
||||
This approach is useful when you have data stored on the local file system or you have a mounted volume,
|
||||
download the data from the remote storage etc.
|
||||
|
||||
The following example shows how to read data from the remote storage and import it into VictoriaMetrics:
|
||||
|
||||
```sh
|
||||
./vmctl mimir --mimir-path="s3:///mimir-tsdb/anonymous" \
|
||||
--mimir-filter-time-start=2024-12-01T00:00:00 \
|
||||
--mimir-filter-time-end=2024-12-17T23:59:59 \
|
||||
--mimir-creds-file-path=creads \
|
||||
--mimir-custom-s3-endpoint='http://localhost:9000' \
|
||||
--vm-concurrency=6 \
|
||||
--mimir-concurrency=6 \
|
||||
--vm-addr=http://localhost:8428/
|
||||
```
|
||||
|
||||
In the example above we are used `--mimir-custom-s3-endpoint` flag to specify the custom S3 endpoint if it is needed.
|
||||
|
||||
When the process finishes, you will see the following:
|
||||
|
||||
```sh
|
||||
2025/01/18 13:01:59 Fetching blocks from remote storage
|
||||
Found 204 blocks to import. Continue? [Y/n] y
|
||||
VM worker 0:? 1589405 samples/s
|
||||
VM worker 1:? 1911834 samples/s
|
||||
VM worker 2:? 1849187 samples/s
|
||||
VM worker 3:? 1648820 samples/s
|
||||
VM worker 4:? 1539212 samples/s
|
||||
VM worker 5:? 1411485 samples/s
|
||||
Processing blocks: 204 / 204 [?????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????] 100.00%
|
||||
2025/01/18 13:02:18 Import finished!
|
||||
2025/01/18 13:02:18 VictoriaMetrics importer stats:
|
||||
idle duration: 18.485875611s;
|
||||
time spent while importing: 16.40543875s;
|
||||
total samples: 177961995;
|
||||
samples/s: 10847743.71;
|
||||
total bytes: 4.1 GB;
|
||||
bytes/s: 248.2 MB;
|
||||
import requests: 893;
|
||||
import requests retries: 0;
|
||||
2025/01/18 13:02:18 Total time: 18.867547083s
|
||||
```
|
||||
|
||||
@@ -56,7 +56,7 @@ OPTIONS:
|
||||
--vm-compress Whether to apply gzip compression to import requests (default: true)
|
||||
--vm-batch-size value How many samples importer collects before sending the import request to VM (default: 200000)
|
||||
--vm-significant-figures value The number of significant figures to leave in metric values before importing. See https://en.wikipedia.org/wiki/Significant_figures. Zero value saves all the significant figures. This option may be used for increasing on-disk compression level for the stored metrics. See also --vm-round-digits option (default: 0)
|
||||
--vm-round-digits value Round metric values to the given number of decimal digits after the point. This option may be used for increasing on-disk compression level for the stored metrics (default: 100)
|
||||
--vm-round-digits value Round metric values to the given number of decimal digits after the point. This option may be used for increasing on-disk compression level for the stored metrics. See also --vm-significant-figures option (default: 100)
|
||||
--vm-extra-label value [ --vm-extra-label value ] Extra labels, that will be added to imported timeseries. In case of collision, label value defined by flag will have priority. Flag can be set multiple times, to add few additional labels.
|
||||
--vm-rate-limit value Optional data transfer rate limit in bytes per second.
|
||||
By default, the rate limit is disabled. It can be useful for limiting load on configured via '--vm-addr' destination. (default: 0)
|
||||
|
||||
@@ -51,7 +51,7 @@ OPTIONS:
|
||||
--vm-compress Whether to apply gzip compression to import requests (default: true)
|
||||
--vm-batch-size value How many samples importer collects before sending the import request to VM (default: 200000)
|
||||
--vm-significant-figures value The number of significant figures to leave in metric values before importing. See https://en.wikipedia.org/wiki/Significant_figures. Zero value saves all the significant figures. This option may be used for increasing on-disk compression level for the stored metrics. See also --vm-round-digits option (default: 0)
|
||||
--vm-round-digits value Round metric values to the given number of decimal digits after the point. This option may be used for increasing on-disk compression level for the stored metrics (default: 100)
|
||||
--vm-round-digits value Round metric values to the given number of decimal digits after the point. This option may be used for increasing on-disk compression level for the stored metrics. See also --vm-significant-figures option (default: 100)
|
||||
--vm-extra-label value [ --vm-extra-label value ] Extra labels, that will be added to imported timeseries. In case of collision, label value defined by flag will have priority. Flag can be set multiple times, to add few additional labels.
|
||||
--vm-rate-limit value Optional data transfer rate limit in bytes per second.
|
||||
By default, the rate limit is disabled. It can be useful for limiting load on configured via '--vm-addr' destination. (default: 0)
|
||||
|
||||
@@ -44,7 +44,7 @@ OPTIONS:
|
||||
--vm-compress Whether to apply gzip compression to import requests (default: true)
|
||||
--vm-batch-size value How many samples importer collects before sending the import request to VM (default: 200000)
|
||||
--vm-significant-figures value The number of significant figures to leave in metric values before importing. See https://en.wikipedia.org/wiki/Significant_figures. Zero value saves all the significant figures. This option may be used for increasing on-disk compression level for the stored metrics. See also --vm-round-digits option (default: 0)
|
||||
--vm-round-digits value Round metric values to the given number of decimal digits after the point. This option may be used for increasing on-disk compression level for the stored metrics (default: 100)
|
||||
--vm-round-digits value Round metric values to the given number of decimal digits after the point. This option may be used for increasing on-disk compression level for the stored metrics. See also --vm-significant-figures option (default: 100)
|
||||
--vm-extra-label value [ --vm-extra-label value ] Extra labels, that will be added to imported timeseries. In case of collision, label value defined by flag will have priority. Flag can be set multiple times, to add few additional labels.
|
||||
--vm-rate-limit value Optional data transfer rate limit in bytes per second.
|
||||
By default, the rate limit is disabled. It can be useful for limiting load on configured via '--vm-addr' destination. (default: 0)
|
||||
|
||||
@@ -59,7 +59,7 @@ OPTIONS:
|
||||
--vm-compress Whether to apply gzip compression to import requests (default: true)
|
||||
--vm-batch-size value How many samples importer collects before sending the import request to VM (default: 200000)
|
||||
--vm-significant-figures value The number of significant figures to leave in metric values before importing. See https://en.wikipedia.org/wiki/Significant_figures. Zero value saves all the significant figures. This option may be used for increasing on-disk compression level for the stored metrics. See also --vm-round-digits option (default: 0)
|
||||
--vm-round-digits value Round metric values to the given number of decimal digits after the point. This option may be used for increasing on-disk compression level for the stored metrics (default: 100)
|
||||
--vm-round-digits value Round metric values to the given number of decimal digits after the point. This option may be used for increasing on-disk compression level for the stored metrics. See also --vm-significant-figures option (default: 100)
|
||||
--vm-extra-label value [ --vm-extra-label value ] Extra labels, that will be added to imported timeseries. In case of collision, label value defined by flag will have priority. Flag can be set multiple times, to add few additional labels.
|
||||
--vm-rate-limit value Optional data transfer rate limit in bytes per second.
|
||||
By default, the rate limit is disabled. It can be useful for limiting load on configured via '--vm-addr' destination. (default: 0)
|
||||
|
||||
@@ -398,6 +398,10 @@ func (nhctx *nativeHistogramContext) appendTimeSeries(tss []TimeSeries, baseLabe
|
||||
if baseName == "" {
|
||||
return tss, labelsPool, samplesPool
|
||||
}
|
||||
originName := *nameValueP
|
||||
defer func() {
|
||||
*nameValueP = originName
|
||||
}()
|
||||
|
||||
*nameValueP = fb.formatName(baseName, "_count")
|
||||
tss, labelsPool, samplesPool = appendHistogramSeries(tss, labelsPool, samplesPool, baseLabels, "", tsMillis, count)
|
||||
@@ -550,9 +554,11 @@ func (nhctx *nativeHistogramContext) reset() {
|
||||
nhctx.zeroCountInt = 0
|
||||
nhctx.zeroCountFloat = 0
|
||||
nhctx.timestamp = 0
|
||||
clear(nhctx.negativeSpans)
|
||||
nhctx.negativeSpans = nhctx.negativeSpans[:0]
|
||||
nhctx.negativeDeltas = nhctx.negativeDeltas[:0]
|
||||
nhctx.negativeCounts = nhctx.negativeCounts[:0]
|
||||
clear(nhctx.positiveSpans)
|
||||
nhctx.positiveSpans = nhctx.positiveSpans[:0]
|
||||
nhctx.positiveDeltas = nhctx.positiveDeltas[:0]
|
||||
nhctx.positiveCounts = nhctx.positiveCounts[:0]
|
||||
|
||||
@@ -3,8 +3,9 @@ package prompb
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestUnmarshalTimeSeries(t *testing.T) {
|
||||
@@ -18,8 +19,8 @@ func TestUnmarshalTimeSeries(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
if !reflect.DeepEqual(tss, wantTSS) {
|
||||
t.Fatalf("unexpected result\ngot:\n%v\nwant:\n%v", tss, wantTSS)
|
||||
if diff := cmp.Diff(wantTSS, tss); len(diff) > 0 {
|
||||
t.Fatalf("unexpected timeseries (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,6 +198,93 @@ func TestUnmarshalTimeSeries(t *testing.T) {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
// verify histogram fields are correctly reused
|
||||
nativeHistogramC := nativeHistogramContext{
|
||||
countInt: 0,
|
||||
isCountFloat: true,
|
||||
countFloat: 2.5,
|
||||
sum: 1.0,
|
||||
schema: 1,
|
||||
zeroThreshold: 0.00001,
|
||||
isZeroCountFloat: true,
|
||||
zeroCountFloat: 0.5,
|
||||
timestamp: 3000,
|
||||
positiveSpans: []bucketSpan{{offset: 1, length: 2}},
|
||||
positiveCounts: []float64{1.5, 1.0},
|
||||
negativeSpans: []bucketSpan{{offset: 0, length: 1}},
|
||||
}
|
||||
nativeHistogramC2 := nativeHistogramContext{
|
||||
countInt: 0,
|
||||
isCountFloat: true,
|
||||
countFloat: 0,
|
||||
sum: 1.0,
|
||||
schema: 1,
|
||||
zeroThreshold: 0.00001,
|
||||
isZeroCountFloat: true,
|
||||
zeroCountFloat: 0.5,
|
||||
timestamp: 4000,
|
||||
positiveSpans: []bucketSpan{{offset: 0, length: 2}},
|
||||
positiveCounts: []float64{1.5, 1.0},
|
||||
negativeSpans: []bucketSpan{{offset: 0, length: 1}},
|
||||
negativeCounts: []float64{1.5, 0},
|
||||
}
|
||||
hd1 := encodeHistogram(nativeHistogramC)
|
||||
hd2 := encodeHistogram(nativeHistogramC2)
|
||||
|
||||
src := encodeTimeSeries(
|
||||
[]Label{{Name: "__name__", Value: "rpc_latency_seconds"}},
|
||||
nil,
|
||||
[][]byte{hd1, hd2},
|
||||
)
|
||||
f(src, []TimeSeries{
|
||||
{
|
||||
Labels: []Label{{Name: "__name__", Value: "rpc_latency_seconds_count"}},
|
||||
Samples: []Sample{{Value: 2.5, Timestamp: 3000}},
|
||||
},
|
||||
{
|
||||
Labels: []Label{{Name: "__name__", Value: "rpc_latency_seconds_sum"}},
|
||||
Samples: []Sample{{Value: 1.0, Timestamp: 3000}},
|
||||
},
|
||||
{
|
||||
Labels: []Label{{Name: "__name__", Value: "rpc_latency_seconds_bucket"}, {Name: "vmrange", Value: appendVmrangeHelper(-0.00001, 0.00001)}},
|
||||
Samples: []Sample{{Value: 0.5, Timestamp: 3000}},
|
||||
},
|
||||
{
|
||||
Labels: []Label{{Name: "__name__", Value: "rpc_latency_seconds_bucket"}, {Name: "vmrange", Value: appendVmrangeHelper(1, 1.414)}},
|
||||
Samples: []Sample{{Value: 1.5, Timestamp: 3000}},
|
||||
},
|
||||
{
|
||||
Labels: []Label{{Name: "__name__", Value: "rpc_latency_seconds_bucket"}, {Name: "vmrange", Value: appendVmrangeHelper(1.414, 2.0)}},
|
||||
Samples: []Sample{{Value: 1.0, Timestamp: 3000}},
|
||||
},
|
||||
{
|
||||
Labels: []Label{{Name: "__name__", Value: "rpc_latency_seconds_count"}},
|
||||
Samples: []Sample{{Value: 0, Timestamp: 4000}},
|
||||
},
|
||||
{
|
||||
Labels: []Label{{Name: "__name__", Value: "rpc_latency_seconds_sum"}},
|
||||
Samples: []Sample{{Value: 1.0, Timestamp: 4000}},
|
||||
},
|
||||
{
|
||||
Labels: []Label{{Name: "__name__", Value: "rpc_latency_seconds_bucket"}, {Name: "vmrange", Value: appendVmrangeHelper(-0.00001, 0.00001)}},
|
||||
Samples: []Sample{{Value: 0.5, Timestamp: 4000}},
|
||||
},
|
||||
{
|
||||
Labels: []Label{{Name: "__name__", Value: "rpc_latency_seconds_bucket"}, {Name: "vmrange", Value: appendVmrangeHelper(0.7071, 1)}},
|
||||
Samples: []Sample{{Value: 1.5, Timestamp: 4000}},
|
||||
},
|
||||
{
|
||||
Labels: []Label{{Name: "__name__", Value: "rpc_latency_seconds_bucket"}, {Name: "vmrange", Value: appendVmrangeHelper(1, 1.414)}},
|
||||
Samples: []Sample{{Value: 1.0, Timestamp: 4000}},
|
||||
},
|
||||
{
|
||||
Labels: []Label{{Name: "__name__", Value: "rpc_latency_seconds_bucket"}, {Name: "vmrange", Value: appendVmrangeHelper(-1, -0.7071)}},
|
||||
Samples: []Sample{{Value: 1.5, Timestamp: 4000}},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func encodeTimeSeries(labels []Label, samples []Sample, histograms [][]byte) []byte {
|
||||
|
||||
Reference in New Issue
Block a user