mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-06-17 07:43:16 +03:00
Compare commits
90 Commits
v1-143-0-d
...
cluster
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb8a6c9902 | ||
|
|
d1e2e533f7 | ||
|
|
9e903759cc | ||
|
|
c5ec4f81c7 | ||
|
|
5358d250bb | ||
|
|
ee642d3541 | ||
|
|
cd9c98d404 | ||
|
|
f06d0c251b | ||
|
|
d49d844a31 | ||
|
|
3c64a86f29 | ||
|
|
cf91d3b7c6 | ||
|
|
f781c7328e | ||
|
|
bfc69e967c | ||
|
|
de4aa35a71 | ||
|
|
68ea8e4d02 | ||
|
|
0b0e1f5602 | ||
|
|
450312befd | ||
|
|
70fb362ec7 | ||
|
|
bd78e06656 | ||
|
|
8614247708 | ||
|
|
4f1420271b | ||
|
|
a8d1323819 | ||
|
|
302ac3cf39 | ||
|
|
c497c8c2e9 | ||
|
|
959d5fe7e2 | ||
|
|
8e764af33b | ||
|
|
9ba930da83 | ||
|
|
f4c85aea5e | ||
|
|
e23420b854 | ||
|
|
b420379838 | ||
|
|
285328eaff | ||
|
|
1468b31fc9 | ||
|
|
702172ce66 | ||
|
|
c7aa1c3a8c | ||
|
|
ec3e329fb0 | ||
|
|
cbf8beee00 | ||
|
|
0d7cad418c | ||
|
|
b628b573af | ||
|
|
f52b752461 | ||
|
|
f56dfef272 | ||
|
|
0bceb58c76 | ||
|
|
4614afb4dc | ||
|
|
d5c142d7db | ||
|
|
7dd8892af1 | ||
|
|
82196af26c | ||
|
|
d7ea8a3467 | ||
|
|
0d46c2af3a | ||
|
|
5fc3d40d10 | ||
|
|
d2baab0f54 | ||
|
|
2050b000fa | ||
|
|
f761290fea | ||
|
|
b4a664df4a | ||
|
|
831bf919e5 | ||
|
|
09f7459669 | ||
|
|
e628be33e2 | ||
|
|
7509f0d5de | ||
|
|
cbb4a0a8bf | ||
|
|
dfab7cc62b | ||
|
|
11f6fd113f | ||
|
|
594b676717 | ||
|
|
49b879eaa4 | ||
|
|
4119a92b01 | ||
|
|
a1ecf9d3f0 | ||
|
|
f36ddf43bd | ||
|
|
ec45497ef1 | ||
|
|
e184a3f457 | ||
|
|
3766e0f956 | ||
|
|
1eb4429b8a | ||
|
|
e5887ddaf2 | ||
|
|
ba47790ddf | ||
|
|
080424fb02 | ||
|
|
c9c0b1a07c | ||
|
|
94868693ef | ||
|
|
97559b536f | ||
|
|
317a09a05b | ||
|
|
c8923dc1c0 | ||
|
|
a805601d39 | ||
|
|
fd39878e41 | ||
|
|
52cec28d06 | ||
|
|
32b394670d | ||
|
|
09105e2c85 | ||
|
|
318f52f079 | ||
|
|
2de3835945 | ||
|
|
58eedc1524 | ||
|
|
a49f3555b0 | ||
|
|
0ac6e56590 | ||
|
|
9934a2e8c9 | ||
|
|
6045d53385 | ||
|
|
5a819c2f9a | ||
|
|
f321872f47 |
10
.github/workflows/build.yml
vendored
10
.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,7 +31,10 @@ concurrency:
|
||||
jobs:
|
||||
build:
|
||||
name: ${{ matrix.os }}-${{ matrix.arch }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
# Runs on dedicated runner with extra resources to increase build speed.
|
||||
runs-on: 'vm-runner'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -60,6 +62,8 @@ jobs:
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Go
|
||||
id: go
|
||||
|
||||
5
.github/workflows/changelog-linter.yml
vendored
5
.github/workflows/changelog-linter.yml
vendored
@@ -5,14 +5,19 @@ 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
|
||||
with:
|
||||
# needed for proper diff
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Validate that changelog changes are under ## tip'
|
||||
run: |
|
||||
|
||||
5
.github/workflows/check-commit-signed.yml
vendored
5
.github/workflows/check-commit-signed.yml
vendored
@@ -3,14 +3,19 @@ name: check-commit-signed
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
check-commit-signed:
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0 # we need full history for commit verification
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check commit signatures
|
||||
run: |
|
||||
|
||||
8
.github/workflows/check-licenses.yml
vendored
8
.github/workflows/check-licenses.yml
vendored
@@ -6,16 +6,20 @@ on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'vendor'
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Go
|
||||
id: go
|
||||
|
||||
4
.github/workflows/codeql-analysis-go.yml
vendored
4
.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
|
||||
@@ -30,6 +32,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
id: go
|
||||
|
||||
10
.github/workflows/docs.yaml
vendored
10
.github/workflows/docs.yaml
vendored
@@ -7,18 +7,21 @@ 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
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
path: __vm
|
||||
persist-credentials: false
|
||||
|
||||
- name: Checkout private code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -26,6 +29,7 @@ jobs:
|
||||
repository: VictoriaMetrics/vmdocs
|
||||
token: ${{ secrets.VM_BOT_GH_TOKEN }}
|
||||
path: __vm-docs
|
||||
persist-credentials: true
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
|
||||
|
||||
22
.github/workflows/test.yml
vendored
22
.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,10 +28,15 @@ concurrency:
|
||||
jobs:
|
||||
lint:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
# Runs on dedicated runner with extra resources since golangci-lint requires extra memory
|
||||
runs-on: 'vm-runner'
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Go
|
||||
id: go
|
||||
@@ -61,7 +65,10 @@ jobs:
|
||||
|
||||
unit:
|
||||
name: unit
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
# Runs on dedicated runner with extra resources to increase tests speed.
|
||||
runs-on: 'vm-runner'
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -73,6 +80,8 @@ jobs:
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Go
|
||||
id: go
|
||||
@@ -90,11 +99,16 @@ jobs:
|
||||
|
||||
apptest:
|
||||
name: apptest
|
||||
permissions:
|
||||
contents: read
|
||||
# Runs on dedicated runner to isolate app tests from other tests.
|
||||
runs-on: apptest
|
||||
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Go
|
||||
id: go
|
||||
|
||||
12
.github/workflows/vmui.yml
vendored
12
.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,10 +25,16 @@ 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
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Cache node_modules
|
||||
id: cache
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
version: "2"
|
||||
linters:
|
||||
enable:
|
||||
- errorlint
|
||||
settings:
|
||||
errcheck:
|
||||
exclude-functions:
|
||||
- fmt.Fprintf
|
||||
- fmt.Fprint
|
||||
- (net/http.ResponseWriter).Write
|
||||
errorlint:
|
||||
errorf: true
|
||||
# Do not enable `comparison` and `asserts`: they produce false positives,
|
||||
# since many call sites intentionally compare sentinel errors directly (e.g. err == io.EOF)
|
||||
# when the producer is documented to return them unwrapped. See https://github.com/VictoriaMetrics/VictoriaLogs/pull/1490
|
||||
comparison: false
|
||||
asserts: false
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
- linters:
|
||||
- staticcheck
|
||||
text: 'SA(4003|1019|5011):'
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
- ^app/vmui/
|
||||
|
||||
8
Makefile
8
Makefile
@@ -17,7 +17,7 @@ EXTRA_GO_BUILD_TAGS ?=
|
||||
GO_BUILDINFO = -X '$(PKG_PREFIX)/lib/buildinfo.Version=$(APP_NAME)-$(DATEINFO_TAG)-$(BUILDINFO_TAG)'
|
||||
TAR_OWNERSHIP ?= --owner=1000 --group=1000
|
||||
|
||||
GOLANGCI_LINT_VERSION := 2.9.0
|
||||
GOLANGCI_LINT_VERSION := 2.12.2
|
||||
|
||||
.PHONY: $(MAKECMDGOALS)
|
||||
|
||||
@@ -293,8 +293,8 @@ apptest-legacy: vminsert-race vmselect-race vmstorage-race vmbackup-race vmresto
|
||||
curl --output-dir /tmp -LO $${URL}/$${VMSINGLE} && tar xzf /tmp/$${VMSINGLE} -C $${DIR} && \
|
||||
curl --output-dir /tmp -LO $${URL}/$${VMCLUSTER} && tar xzf /tmp/$${VMCLUSTER} -C $${DIR} \
|
||||
); \
|
||||
VM_LEGACY_VMSINGLE_PATH=$${DIR}/victoria-metrics-prod \
|
||||
VM_LEGACY_VMSTORAGE_PATH=$${DIR}/vmstorage-prod \
|
||||
VMSINGLE_V1_132_0_PATH=$${DIR}/victoria-metrics-prod \
|
||||
VMSTORAGE_V1_132_0_PATH=$${DIR}/vmstorage-prod \
|
||||
go test ./apptest/tests -run="^TestLegacyCluster.*"
|
||||
|
||||
benchmark:
|
||||
@@ -335,7 +335,7 @@ golangci-lint: install-golangci-lint
|
||||
golangci-lint run --build-tags 'synctest'
|
||||
|
||||
install-golangci-lint:
|
||||
which golangci-lint && (golangci-lint --version | grep -q $(GOLANGCI_LINT_VERSION)) || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v$(GOLANGCI_LINT_VERSION)
|
||||
which golangci-lint && (golangci-lint --version | grep -q $(GOLANGCI_LINT_VERSION)) || curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v$(GOLANGCI_LINT_VERSION)
|
||||
|
||||
remove-golangci-lint:
|
||||
rm -rf `which golangci-lint`
|
||||
|
||||
@@ -118,6 +118,7 @@ func main() {
|
||||
remotewrite.InitSecretFlags()
|
||||
buildinfo.Init()
|
||||
logger.Init()
|
||||
opentelemetry.Init()
|
||||
timeserieslimits.Init(*maxLabelsPerTimeseries, *maxLabelNameLen, *maxLabelValueLen)
|
||||
|
||||
if promscrape.IsDryRun() {
|
||||
|
||||
@@ -25,6 +25,11 @@ var (
|
||||
rowsPerInsert = metrics.NewHistogram(`vmagent_rows_per_insert{type="opentelemetry"}`)
|
||||
)
|
||||
|
||||
// Init must be called after flag.Parse and before using the opentelemetry package.
|
||||
func Init() {
|
||||
stream.InitDecodeOptions()
|
||||
}
|
||||
|
||||
// InsertHandlerForReader processes metrics from given reader.
|
||||
func InsertHandlerForReader(at *auth.Token, r io.Reader, encoding string) error {
|
||||
return stream.ParseStream(r, encoding, nil, func(tss []prompb.TimeSeries, mms []prompb.MetricMetadata) error {
|
||||
|
||||
@@ -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"}`+
|
||||
|
||||
@@ -113,15 +113,15 @@ func (g *Group) Validate(validateTplFn ValidateTplFn, validateExpressions bool)
|
||||
// because correct types must be inherited after unmarshalling.
|
||||
exprValidator := g.Type.ValidateExpr
|
||||
if err := exprValidator(r.Expr); err != nil {
|
||||
return fmt.Errorf("invalid expression for rule %q: %w", ruleName, err)
|
||||
return fmt.Errorf("invalid expression for rule %q: %w", ruleName, err)
|
||||
}
|
||||
}
|
||||
if validateTplFn != nil {
|
||||
if err := validateTplFn(r.Annotations); err != nil {
|
||||
return fmt.Errorf("invalid annotations for rule %q: %w", ruleName, err)
|
||||
return fmt.Errorf("invalid annotations for rule %q: %w", ruleName, err)
|
||||
}
|
||||
if err := validateTplFn(r.Labels); err != nil {
|
||||
return fmt.Errorf("invalid labels for rule %q: %w", ruleName, err)
|
||||
return fmt.Errorf("invalid labels for rule %q: %w", ruleName, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,9 +173,9 @@ func (r *Rule) String() string {
|
||||
if r.Alert != "" {
|
||||
ruleType = "alerting"
|
||||
}
|
||||
b := strings.Builder{}
|
||||
b.WriteString(fmt.Sprintf("%s rule %q", ruleType, r.Name()))
|
||||
b.WriteString(fmt.Sprintf("; expr: %q", r.Expr))
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "%s rule %q", ruleType, r.Name())
|
||||
fmt.Fprintf(&b, "; expr: %q", r.Expr)
|
||||
|
||||
kv := sortMap(r.Labels)
|
||||
for i := range kv {
|
||||
|
||||
@@ -121,7 +121,7 @@ func TestParse_Failure(t *testing.T) {
|
||||
f([]string{"testdata/dir/rules2-bad.rules"}, "function \"unknown\" not defined")
|
||||
f([]string{"testdata/dir/rules3-bad.rules"}, "either `record` or `alert` must be set")
|
||||
f([]string{"testdata/dir/rules4-bad.rules"}, "either `record` or `alert` must be set")
|
||||
f([]string{"testdata/rules/rules1-bad.rules"}, "bad graphite expr")
|
||||
f([]string{"testdata/rules/rules1-bad.rules"}, "bad GraphiteQL expr")
|
||||
f([]string{"testdata/rules/vlog-rules0-bad.rules"}, "bad LogsQL expr")
|
||||
f([]string{"testdata/dir/rules6-bad.rules"}, "missing ':' in header")
|
||||
f([]string{"testdata/rules/rules-multi-doc-bad.rules"}, "unknown fields")
|
||||
@@ -283,7 +283,7 @@ func TestGroupValidate_Failure(t *testing.T) {
|
||||
Expr: "up | 0",
|
||||
},
|
||||
},
|
||||
}, true, "bad prometheus expr")
|
||||
}, true, "bad MetricsQL expr")
|
||||
|
||||
f(&Group{
|
||||
Name: "test graphite expr",
|
||||
@@ -293,7 +293,7 @@ func TestGroupValidate_Failure(t *testing.T) {
|
||||
"description": "some-description",
|
||||
}},
|
||||
},
|
||||
}, true, "bad graphite expr")
|
||||
}, true, "bad GraphiteQL expr")
|
||||
|
||||
f(&Group{
|
||||
Name: "test vlogs expr",
|
||||
@@ -327,7 +327,7 @@ func TestGroupValidate_Failure(t *testing.T) {
|
||||
Expr: "sum(up == 0 ) by (host)",
|
||||
},
|
||||
},
|
||||
}, true, "bad graphite expr")
|
||||
}, true, "bad GraphiteQL expr")
|
||||
|
||||
f(&Group{
|
||||
Name: "test vlogs with prometheus exp",
|
||||
@@ -351,7 +351,7 @@ func TestGroupValidate_Failure(t *testing.T) {
|
||||
For: promutil.NewDuration(10 * time.Millisecond),
|
||||
},
|
||||
},
|
||||
}, true, "bad prometheus expr")
|
||||
}, true, "bad MetricsQL expr")
|
||||
}
|
||||
|
||||
func TestGroupValidate_Success(t *testing.T) {
|
||||
|
||||
@@ -66,11 +66,11 @@ func (t *Type) ValidateExpr(expr string) error {
|
||||
switch t.String() {
|
||||
case "graphite":
|
||||
if _, err := graphiteql.Parse(expr); err != nil {
|
||||
return fmt.Errorf("bad graphite expr: %q, err: %w", expr, err)
|
||||
return fmt.Errorf("bad GraphiteQL expr: %q, err: %w", expr, err)
|
||||
}
|
||||
case "prometheus":
|
||||
if _, err := metricsql.Parse(expr); err != nil {
|
||||
return fmt.Errorf("bad prometheus expr: %q, err: %w", expr, err)
|
||||
return fmt.Errorf("bad MetricsQL expr: %q, err: %w", expr, err)
|
||||
}
|
||||
case "vlogs":
|
||||
q, err := logstorage.ParseStatsQuery(expr, 0)
|
||||
|
||||
@@ -89,7 +89,7 @@ func (pi *promInstant) Unmarshal(b []byte) error {
|
||||
labels.Visit(func(key []byte, v *fastjson.Value) {
|
||||
lv, errLocal := v.StringBytes()
|
||||
if errLocal != nil {
|
||||
err = fmt.Errorf("error when parsing label value %q: %s", v, errLocal)
|
||||
err = fmt.Errorf("error when parsing label value %q: %w", v, errLocal)
|
||||
return
|
||||
}
|
||||
r.Labels = append(r.Labels, prompb.Label{
|
||||
@@ -112,7 +112,7 @@ func (pi *promInstant) Unmarshal(b []byte) error {
|
||||
r.Timestamps = []int64{sample[0].GetInt64()}
|
||||
val, err := sample[1].StringBytes()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error when parsing `value` object %q: %s", sample[1], err)
|
||||
return fmt.Errorf("error when parsing `value` object %q: %w", sample[1], err)
|
||||
}
|
||||
f, err := strconv.ParseFloat(bytesutil.ToUnsafeString(val), 64)
|
||||
if err != nil {
|
||||
|
||||
@@ -315,6 +315,11 @@ func configReload(ctx context.Context, m *manager, groupsCfg []config.Group, sig
|
||||
|
||||
parseFn := config.Parse
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
@@ -601,7 +601,7 @@ func (ar *AlertingRule) exec(ctx context.Context, ts time.Time, limit int) ([]pr
|
||||
func (ar *AlertingRule) expandLabelTemplates(m datasource.Metric, qFn templates.QueryFn) (*labelSet, error) {
|
||||
ls, err := ar.toLabels(m, qFn)
|
||||
if err != nil {
|
||||
return ls, fmt.Errorf("failed to expand label templates: %s", err)
|
||||
return ls, fmt.Errorf("failed to expand label templates: %w", err)
|
||||
}
|
||||
return ls, nil
|
||||
}
|
||||
@@ -620,7 +620,7 @@ func (ar *AlertingRule) expandAnnotationTemplates(m datasource.Metric, qFn templ
|
||||
}
|
||||
as, err := notifier.ExecTemplate(qFn, ar.Annotations, tplData)
|
||||
if err != nil {
|
||||
return as, fmt.Errorf("failed to expand annotation templates: %s", err)
|
||||
return as, fmt.Errorf("failed to expand annotation templates: %w", err)
|
||||
}
|
||||
return as, nil
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -75,7 +77,7 @@ var (
|
||||
func marshalJson(v any, kind string) ([]byte, *httpserver.ErrorWithStatusCode) {
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, errResponse(fmt.Errorf("failed to marshal %s: %s", kind, err), http.StatusInternalServerError)
|
||||
return nil, errResponse(fmt.Errorf("failed to marshal %s: %w", kind, err), http.StatusInternalServerError)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -840,6 +840,11 @@ func authConfigReloader(sighupCh <-chan os.Signal) {
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stopCh:
|
||||
return
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case <-stopCh:
|
||||
return
|
||||
@@ -889,7 +894,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
|
||||
}
|
||||
|
||||
|
||||
@@ -317,7 +317,7 @@ func processUserRequest(w http.ResponseWriter, r *http.Request, ui *UserInfo, tk
|
||||
defer ui.endConcurrencyLimit()
|
||||
|
||||
// Process the request.
|
||||
processRequest(w, r, ui, tkn)
|
||||
processRequest(w, r, ui, tkn, userName)
|
||||
}
|
||||
|
||||
func beginConcurrencyLimit(ctx context.Context) error {
|
||||
@@ -391,7 +391,7 @@ func bufferRequestBody(ctx context.Context, r io.ReadCloser, userName string) (i
|
||||
return bb, nil
|
||||
}
|
||||
|
||||
func processRequest(w http.ResponseWriter, r *http.Request, ui *UserInfo, tkn *jwt.Token) {
|
||||
func processRequest(w http.ResponseWriter, r *http.Request, ui *UserInfo, tkn *jwt.Token, userName string) {
|
||||
u := normalizeURL(r.URL)
|
||||
up, hc := ui.getURLPrefixAndHeaders(u, r.Host, r.Header)
|
||||
isDefault := false
|
||||
@@ -409,7 +409,7 @@ func processRequest(w http.ResponseWriter, r *http.Request, ui *UserInfo, tkn *j
|
||||
if ui.DumpRequestOnErrors {
|
||||
di = debugInfo(u, r)
|
||||
}
|
||||
httpserver.Errorf(w, r, "missing route for %q%s", u.String(), di)
|
||||
httpserver.Errorf(w, r, "user %s missing route for %q%s", userName, u.String(), di)
|
||||
return
|
||||
}
|
||||
up, hc = ui.DefaultURL, ui.HeadersConf
|
||||
@@ -455,7 +455,7 @@ func processRequest(w http.ResponseWriter, r *http.Request, ui *UserInfo, tkn *j
|
||||
ui.backendErrors.Inc()
|
||||
}
|
||||
err := &httpserver.ErrorWithStatusCode{
|
||||
Err: fmt.Errorf("all the %d backends for the user %q are unavailable for proxying the request - check previous WARN logs to see the exact error for each failed backend", up.getBackendsCount(), ui.name()),
|
||||
Err: fmt.Errorf("all the %d backends for the user %q are unavailable for proxying the request - check previous WARN logs to see the exact error for each failed backend", up.getBackendsCount(), userName),
|
||||
StatusCode: http.StatusBadGateway,
|
||||
}
|
||||
httpserver.Errorf(w, r, "%s", err)
|
||||
|
||||
@@ -307,6 +307,24 @@ statusCode=200
|
||||
requested_url={BACKEND}/bar/a/b`
|
||||
f(cfgStr, requestURL, backendHandler, responseExpected)
|
||||
|
||||
// correct authorization but unexisted path, hence missing route error.
|
||||
cfgStr = `
|
||||
users:
|
||||
- username: foo
|
||||
password: secret
|
||||
url_map:
|
||||
- src_paths:
|
||||
- "/api/v1/write"
|
||||
url_prefix: "{BACKEND}/bar"`
|
||||
requestURL = "http://foo:secret@some-host.com/a/b"
|
||||
backendHandler = func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "requested_url=http://%s%s", r.Host, r.URL)
|
||||
}
|
||||
responseExpected = `
|
||||
statusCode=400
|
||||
user foo missing route for "http://foo:secret@some-host.com/a/b"`
|
||||
f(cfgStr, requestURL, backendHandler, responseExpected)
|
||||
|
||||
// verify how path cleanup works
|
||||
cfgStr = `
|
||||
unauthorized_user:
|
||||
@@ -403,7 +421,7 @@ unauthorized_user:
|
||||
}
|
||||
responseExpected = `
|
||||
statusCode=400
|
||||
missing route for "http://some-host.com/abc?de=fg"`
|
||||
user unauthorized missing route for "http://some-host.com/abc?de=fg"`
|
||||
f(cfgStr, requestURL, backendHandler, responseExpected)
|
||||
|
||||
// missing default_url and default url_prefix for unauthorized user with dump_request_on_errors enabled
|
||||
@@ -419,7 +437,7 @@ unauthorized_user:
|
||||
}
|
||||
responseExpected = `
|
||||
statusCode=400
|
||||
missing route for "http://some-host.com/abc?de=fg" (host: "some-host.com"; path: "/abc"; args: "de=fg"; headers:Connection: Some-Header,Other-Header
|
||||
user unauthorized missing route for "http://some-host.com/abc?de=fg" (host: "some-host.com"; path: "/abc"; args: "de=fg"; headers:Connection: Some-Header,Other-Header
|
||||
Pass-Header: abc
|
||||
Some-Header: foobar
|
||||
X-Forwarded-For: 12.34.56.78
|
||||
@@ -461,7 +479,7 @@ unauthorized_user:
|
||||
}
|
||||
responseExpected = `
|
||||
statusCode=502
|
||||
all the 2 backends for the user "" are unavailable for proxying the request - check previous WARN logs to see the exact error for each failed backend`
|
||||
all the 2 backends for the user "unauthorized" are unavailable for proxying the request - check previous WARN logs to see the exact error for each failed backend`
|
||||
f(cfgStr, requestURL, backendHandler, responseExpected)
|
||||
|
||||
// all the backend_urls are unavailable for authorized user
|
||||
@@ -501,7 +519,7 @@ unauthorized_user:
|
||||
}
|
||||
responseExpected = `
|
||||
statusCode=502
|
||||
all the 0 backends for the user "" are unavailable for proxying the request - check previous WARN logs to see the exact error for each failed backend`
|
||||
all the 0 backends for the user "unauthorized" are unavailable for proxying the request - check previous WARN logs to see the exact error for each failed backend`
|
||||
f(cfgStr, requestURL, backendHandler, responseExpected)
|
||||
netutil.Resolver = origResolver
|
||||
|
||||
@@ -518,7 +536,7 @@ unauthorized_user:
|
||||
}
|
||||
responseExpected = `
|
||||
statusCode=502
|
||||
all the 2 backends for the user "" are unavailable for proxying the request - check previous WARN logs to see the exact error for each failed backend`
|
||||
all the 2 backends for the user "unauthorized" are unavailable for proxying the request - check previous WARN logs to see the exact error for each failed backend`
|
||||
f(cfgStr, requestURL, backendHandler, responseExpected)
|
||||
if n := retries.Load(); n != 2 {
|
||||
t.Fatalf("unexpected number of retries; got %d; want 2", n)
|
||||
@@ -545,6 +563,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) {
|
||||
@@ -1596,7 +1639,7 @@ func (w *fakeResponseWriter) WriteHeader(statusCode int) {
|
||||
"X-Content-Type-Options": true,
|
||||
})
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot marshal headers: %s", err))
|
||||
panic(fmt.Errorf("cannot marshal headers: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ func fetchAndParseJWKs(ctx context.Context, jwksURI string) (*jwt.VerifierPool,
|
||||
|
||||
vp, err := jwt.ParseJWKs(b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse jwks keys from %q: %v", jwksURI, err)
|
||||
return nil, fmt.Errorf("failed to parse jwks keys from %q: %w", jwksURI, err)
|
||||
}
|
||||
|
||||
return vp, nil
|
||||
@@ -188,7 +188,7 @@ func getOpenIDConfiguration(ctx context.Context, issuer string) (openidConfig, e
|
||||
|
||||
var cfg openidConfig
|
||||
if err := json.NewDecoder(resp.Body).Decode(&cfg); err != nil {
|
||||
return openidConfig{}, fmt.Errorf("failed to decode openid config from %q: %s", configURL, err)
|
||||
return openidConfig{}, fmt.Errorf("failed to decode openid config from %q: %w", configURL, err)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
|
||||
@@ -131,16 +131,13 @@ func (ac *authContext) initFromBasicAuthConfig(ba *BasicAuthConfig) error {
|
||||
if ba.Username == "" {
|
||||
return fmt.Errorf("missing `username` in `basic_auth` section")
|
||||
}
|
||||
if ba.Password != "" {
|
||||
ac.getAuthHeader = func() string {
|
||||
// See https://en.wikipedia.org/wiki/Basic_access_authentication
|
||||
token := ba.Username + ":" + ba.Password
|
||||
token64 := base64.StdEncoding.EncodeToString([]byte(token))
|
||||
return "Basic " + token64
|
||||
}
|
||||
ac.authDigest = fmt.Sprintf("basic(username=%q, password=%q)", ba.Username, ba.Password)
|
||||
return nil
|
||||
ac.getAuthHeader = func() string {
|
||||
// See https://en.wikipedia.org/wiki/Basic_access_authentication
|
||||
token := ba.Username + ":" + ba.Password
|
||||
token64 := base64.StdEncoding.EncodeToString([]byte(token))
|
||||
return "Basic " + token64
|
||||
}
|
||||
ac.authDigest = fmt.Sprintf("basic(username=%q, password=%q)", ba.Username, ba.Password)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,8 @@ const (
|
||||
vmAddr = "vm-addr"
|
||||
vmUser = "vm-user"
|
||||
vmPassword = "vm-password"
|
||||
vmHeaders = "vm-headers"
|
||||
vmBearerToken = "vm-bearer-token"
|
||||
vmAccountID = "vm-account-id"
|
||||
vmConcurrency = "vm-concurrency"
|
||||
vmCompress = "vm-compress"
|
||||
@@ -112,6 +114,16 @@ var (
|
||||
Usage: "VictoriaMetrics password for basic auth",
|
||||
EnvVars: []string{"VM_PASSWORD"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: vmHeaders,
|
||||
Usage: "Optional HTTP headers to send with each request to the corresponding destination address. \n" +
|
||||
"For example, --vm-headers='My-Auth:foobar' would send 'My-Auth: foobar' HTTP header with every request to the corresponding destination address. \n" +
|
||||
"Multiple headers must be delimited by '^^': --vm-headers='header1:value1^^header2:value2'",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: vmBearerToken,
|
||||
Usage: "Optional bearer auth token to use for the corresponding --vm-addr",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: vmAccountID,
|
||||
Usage: "AccountID is an arbitrary 32-bit integer identifying namespace for data ingestion (aka tenant). \n" +
|
||||
@@ -146,7 +158,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 +513,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"
|
||||
|
||||
@@ -43,7 +43,7 @@ func newInfluxProcessor(ic *influx.Client, im *vm.Importer, cc int, separator st
|
||||
func (ip *influxProcessor) run(ctx context.Context) error {
|
||||
series, err := ip.ic.Explore()
|
||||
if err != nil {
|
||||
return fmt.Errorf("explore query failed: %s", err)
|
||||
return fmt.Errorf("explore query failed: %w", err)
|
||||
}
|
||||
if len(series) < 1 {
|
||||
return fmt.Errorf("found no timeseries to import")
|
||||
@@ -71,7 +71,7 @@ func (ip *influxProcessor) run(ctx context.Context) error {
|
||||
for s := range seriesCh {
|
||||
if err := ip.do(s); err != nil {
|
||||
influxErrorsTotal.Inc()
|
||||
errCh <- fmt.Errorf("request failed for %q.%q: %s", s.Measurement, s.Field, err)
|
||||
errCh <- fmt.Errorf("request failed for %q.%q: %w", s.Measurement, s.Field, err)
|
||||
return
|
||||
}
|
||||
influxSeriesProcessed.Inc()
|
||||
@@ -84,10 +84,10 @@ func (ip *influxProcessor) run(ctx context.Context) error {
|
||||
for _, s := range series {
|
||||
select {
|
||||
case infErr := <-errCh:
|
||||
return fmt.Errorf("influx error: %s", infErr)
|
||||
return fmt.Errorf("influx error: %w", infErr)
|
||||
case vmErr := <-ip.im.Errors():
|
||||
influxErrorsTotal.Inc()
|
||||
return fmt.Errorf("import process failed: %s", wrapErr(vmErr, ip.isVerbose))
|
||||
return fmt.Errorf("import process failed: %w", wrapErr(vmErr, ip.isVerbose))
|
||||
case seriesCh <- s:
|
||||
}
|
||||
}
|
||||
@@ -100,11 +100,11 @@ func (ip *influxProcessor) run(ctx context.Context) error {
|
||||
for vmErr := range ip.im.Errors() {
|
||||
if vmErr.Err != nil {
|
||||
influxErrorsTotal.Inc()
|
||||
return fmt.Errorf("import process failed: %s", wrapErr(vmErr, ip.isVerbose))
|
||||
return fmt.Errorf("import process failed: %w", wrapErr(vmErr, ip.isVerbose))
|
||||
}
|
||||
}
|
||||
for err := range errCh {
|
||||
return fmt.Errorf("import process failed: %s", err)
|
||||
return fmt.Errorf("import process failed: %w", err)
|
||||
}
|
||||
|
||||
log.Println("Import finished!")
|
||||
@@ -119,7 +119,7 @@ const valueField = "value"
|
||||
func (ip *influxProcessor) do(s *influx.Series) error {
|
||||
cr, err := ip.ic.FetchDataPoints(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch datapoints: %s", err)
|
||||
return fmt.Errorf("failed to fetch datapoints: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = cr.Close()
|
||||
|
||||
@@ -96,10 +96,10 @@ func NewClient(cfg Config) (*Client, error) {
|
||||
}
|
||||
hc, err := influx.NewHTTPClient(c)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to establish conn: %s", err)
|
||||
return nil, fmt.Errorf("failed to establish conn: %w", err)
|
||||
}
|
||||
if _, _, err := hc.Ping(time.Second); err != nil {
|
||||
return nil, fmt.Errorf("ping failed: %s", err)
|
||||
return nil, fmt.Errorf("ping failed: %w", err)
|
||||
}
|
||||
|
||||
chunkSize := cfg.ChunkSize
|
||||
@@ -155,7 +155,7 @@ func (c *Client) Explore() ([]*Series, error) {
|
||||
// {"measurement1": ["value1", "value2"]}
|
||||
mFields, err := c.fieldsByMeasurement()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get field keys: %s", err)
|
||||
return nil, fmt.Errorf("failed to get field keys: %w", err)
|
||||
}
|
||||
|
||||
if len(mFields) < 1 {
|
||||
@@ -165,12 +165,12 @@ func (c *Client) Explore() ([]*Series, error) {
|
||||
// {"measurement1": {"tag1", "tag2"}}
|
||||
measurementTags, err := c.getMeasurementTags()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get tags of measurements: %s", err)
|
||||
return nil, fmt.Errorf("failed to get tags of measurements: %w", err)
|
||||
}
|
||||
|
||||
series, err := c.getSeries()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get series: %s", err)
|
||||
return nil, fmt.Errorf("failed to get series: %w", err)
|
||||
}
|
||||
|
||||
var iSeries []*Series
|
||||
@@ -237,7 +237,7 @@ func (cr *ChunkedResponse) Next() ([]int64, []float64, error) {
|
||||
return nil, nil, err
|
||||
}
|
||||
if resp.Error() != nil {
|
||||
return nil, nil, fmt.Errorf("response error for %s: %s", cr.iq.Command, resp.Error())
|
||||
return nil, nil, fmt.Errorf("response error for %s: %w", cr.iq.Command, resp.Error())
|
||||
}
|
||||
if len(resp.Results) != 1 {
|
||||
return nil, nil, fmt.Errorf("unexpected number of results in response: %d", len(resp.Results))
|
||||
@@ -265,8 +265,7 @@ func (cr *ChunkedResponse) Next() ([]int64, []float64, error) {
|
||||
for i, fv := range fieldValues {
|
||||
v, err := toFloat64(fv)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to convert value %q.%v to float64: %s",
|
||||
cr.field, v, err)
|
||||
return nil, nil, fmt.Errorf("failed to convert value %q.%v to float64: %w", cr.field, v, err)
|
||||
}
|
||||
values[i] = v
|
||||
}
|
||||
@@ -294,7 +293,7 @@ func (c *Client) FetchDataPoints(s *Series) (*ChunkedResponse, error) {
|
||||
}
|
||||
cr, err := c.QueryAsChunk(iq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query %q err: %s", iq.Command, err)
|
||||
return nil, fmt.Errorf("query %q err: %w", iq.Command, err)
|
||||
}
|
||||
return &ChunkedResponse{cr, iq, s.Field}, nil
|
||||
}
|
||||
@@ -308,7 +307,7 @@ func (c *Client) fieldsByMeasurement() (map[string][]string, error) {
|
||||
log.Printf("fetching fields: %s", stringify(q))
|
||||
qValues, err := c.do(q)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while executing query %q: %s", q.Command, err)
|
||||
return nil, fmt.Errorf("error while executing query %q: %w", q.Command, err)
|
||||
}
|
||||
|
||||
var total int
|
||||
@@ -352,7 +351,7 @@ func (c *Client) getSeries() ([]*Series, error) {
|
||||
log.Printf("fetching series: %s", stringify(q))
|
||||
cr, err := c.QueryAsChunk(q)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while executing query %q: %s", q.Command, err)
|
||||
return nil, fmt.Errorf("error while executing query %q: %w", q.Command, err)
|
||||
}
|
||||
|
||||
const key = "key"
|
||||
@@ -366,7 +365,7 @@ func (c *Client) getSeries() ([]*Series, error) {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Error() != nil {
|
||||
return nil, fmt.Errorf("response error for query %q: %s", q.Command, resp.Error())
|
||||
return nil, fmt.Errorf("response error for query %q: %w", q.Command, resp.Error())
|
||||
}
|
||||
qValues, err := parseResult(resp.Results[0])
|
||||
if err != nil {
|
||||
@@ -417,7 +416,7 @@ func (c *Client) getMeasurementTags() (map[string]map[string]struct{}, error) {
|
||||
log.Printf("fetching tag keys: %s", stringify(q))
|
||||
cr, err := c.QueryAsChunk(q)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while executing query %q: %s", q.Command, err)
|
||||
return nil, fmt.Errorf("error while executing query %q: %w", q.Command, err)
|
||||
}
|
||||
|
||||
const tagKey = "tagKey"
|
||||
@@ -432,7 +431,7 @@ func (c *Client) getMeasurementTags() (map[string]map[string]struct{}, error) {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Error() != nil {
|
||||
return nil, fmt.Errorf("response error for query %q: %s", q.Command, resp.Error())
|
||||
return nil, fmt.Errorf("response error for query %q: %w", q.Command, resp.Error())
|
||||
}
|
||||
qValues, err := parseResult(resp.Results[0])
|
||||
if err != nil {
|
||||
@@ -455,10 +454,10 @@ func (c *Client) getMeasurementTags() (map[string]map[string]struct{}, error) {
|
||||
func (c *Client) do(q influx.Query) ([]queryValues, error) {
|
||||
res, err := c.Query(q)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query error: %s", err)
|
||||
return nil, fmt.Errorf("query error: %w", err)
|
||||
}
|
||||
if res.Error() != nil {
|
||||
return nil, fmt.Errorf("response error: %s", res.Error())
|
||||
return nil, fmt.Errorf("response error: %w", res.Error())
|
||||
}
|
||||
if len(res.Results) < 1 {
|
||||
return nil, fmt.Errorf("query returned 0 results")
|
||||
|
||||
@@ -71,7 +71,7 @@ func toFloat64(v any) (float64, error) {
|
||||
func parseDate(dateStr string) (int64, error) {
|
||||
startTime, err := time.Parse(time.RFC3339, dateStr)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("cannot parse %q: %s", dateStr, err)
|
||||
return 0, fmt.Errorf("cannot parse %q: %w", dateStr, err)
|
||||
}
|
||||
return startTime.UnixNano() / 1e6, nil
|
||||
}
|
||||
@@ -92,7 +92,7 @@ func (s *Series) unmarshal(v string) error {
|
||||
var err error
|
||||
s.LabelPairs, err = unmarshalTags(v[n+1:], noEscapeChars)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarhsal tags: %s", err)
|
||||
return fmt.Errorf("failed to unmarhsal tags: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -87,7 +88,7 @@ func main() {
|
||||
|
||||
tr, err := promauth.NewTLSTransport(certFile, keyFile, caFile, serverName, insecureSkipVerify, "vmctl_opentsdb")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create transport for -%s=%q: %s", otsdbAddr, addr, err)
|
||||
return fmt.Errorf("failed to create transport for -%s=%q: %w", otsdbAddr, addr, err)
|
||||
}
|
||||
oCfg := opentsdb.Config{
|
||||
Addr: addr,
|
||||
@@ -102,17 +103,17 @@ func main() {
|
||||
}
|
||||
otsdbClient, err := opentsdb.NewClient(oCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create opentsdb client: %s", err)
|
||||
return fmt.Errorf("failed to create opentsdb client: %w", err)
|
||||
}
|
||||
|
||||
vmCfg, err := initConfigVM(c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to init VM configuration: %s", err)
|
||||
return fmt.Errorf("failed to init VM configuration: %w", err)
|
||||
}
|
||||
|
||||
importer, err := vm.NewImporter(ctx, vmCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create VM importer: %s", err)
|
||||
return fmt.Errorf("failed to create VM importer: %w", err)
|
||||
}
|
||||
|
||||
otsdbProcessor := newOtsdbProcessor(otsdbClient, importer, c.Int(otsdbConcurrency), c.Bool(globalVerbose))
|
||||
@@ -136,7 +137,7 @@ func main() {
|
||||
|
||||
tc, err := promauth.NewTLSConfig(certFile, keyFile, caFile, serverName, insecureSkipVerify)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create TLS Config: %s", err)
|
||||
return fmt.Errorf("failed to create TLS Config: %w", err)
|
||||
}
|
||||
|
||||
iCfg := influx.Config{
|
||||
@@ -156,17 +157,17 @@ func main() {
|
||||
|
||||
influxClient, err := influx.NewClient(iCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create influx client: %s", err)
|
||||
return fmt.Errorf("failed to create influx client: %w", err)
|
||||
}
|
||||
|
||||
vmCfg, err := initConfigVM(c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to init VM configuration: %s", err)
|
||||
return fmt.Errorf("failed to init VM configuration: %w", err)
|
||||
}
|
||||
|
||||
importer, err = vm.NewImporter(ctx, vmCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create VM importer: %s", err)
|
||||
return fmt.Errorf("failed to create VM importer: %w", err)
|
||||
}
|
||||
|
||||
processor := newInfluxProcessor(
|
||||
@@ -202,7 +203,7 @@ func main() {
|
||||
|
||||
tr, err := promauth.NewTLSTransport(certFile, keyFile, caFile, serverName, insecureSkipVerify, "vmctl_remoteread")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create transport for -%s=%q: %s", remoteReadSrcAddr, addr, err)
|
||||
return fmt.Errorf("failed to create transport for -%s=%q: %w", remoteReadSrcAddr, addr, err)
|
||||
}
|
||||
|
||||
// Backwards compatible default values if none provided by user
|
||||
@@ -226,17 +227,17 @@ func main() {
|
||||
DisablePathAppend: c.Bool(remoteReadDisablePathAppend),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error create remote read client: %s", err)
|
||||
return fmt.Errorf("error create remote read client: %w", err)
|
||||
}
|
||||
|
||||
vmCfg, err := initConfigVM(c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to init VM configuration: %s", err)
|
||||
return fmt.Errorf("failed to init VM configuration: %w", err)
|
||||
}
|
||||
|
||||
importer, err := vm.NewImporter(ctx, vmCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create VM importer: %s", err)
|
||||
return fmt.Errorf("failed to create VM importer: %w", err)
|
||||
}
|
||||
|
||||
rmp := remoteReadProcessor{
|
||||
@@ -264,12 +265,12 @@ func main() {
|
||||
|
||||
vmCfg, err := initConfigVM(c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to init VM configuration: %s", err)
|
||||
return fmt.Errorf("failed to init VM configuration: %w", err)
|
||||
}
|
||||
|
||||
importer, err = vm.NewImporter(ctx, vmCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create VM importer: %s", err)
|
||||
return fmt.Errorf("failed to create VM importer: %w", err)
|
||||
}
|
||||
|
||||
promCfg := prometheus.Config{
|
||||
@@ -284,7 +285,7 @@ func main() {
|
||||
}
|
||||
cl, err := prometheus.NewClient(promCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create prometheus client: %s", err)
|
||||
return fmt.Errorf("failed to create prometheus client: %w", err)
|
||||
}
|
||||
|
||||
pp := prometheusProcessor{
|
||||
@@ -296,6 +297,56 @@ func main() {
|
||||
return pp.run(ctx)
|
||||
},
|
||||
},
|
||||
{
|
||||
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("Mimir import mode")
|
||||
|
||||
vmCfg, err := initConfigVM(c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to init VM configuration: %w", err)
|
||||
}
|
||||
|
||||
importer, err = vm.NewImporter(ctx, vmCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create VM importer: %w", 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: %w", 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)",
|
||||
@@ -303,17 +354,15 @@ func main() {
|
||||
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)
|
||||
return fmt.Errorf("failed to init VM configuration: %w", err)
|
||||
}
|
||||
|
||||
importer, err = vm.NewImporter(ctx, vmCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create VM importer: %s", err)
|
||||
return fmt.Errorf("failed to create VM importer: %w", err)
|
||||
}
|
||||
|
||||
thanosCfg := thanos.Config{
|
||||
Snapshot: c.String(thanosSnapshot),
|
||||
Filter: thanos.Filter{
|
||||
@@ -325,7 +374,7 @@ func main() {
|
||||
}
|
||||
cl, err := thanos.NewClient(thanosCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create thanos client: %s", err)
|
||||
return fmt.Errorf("failed to create thanos client: %w", err)
|
||||
}
|
||||
|
||||
var aggrTypes []thanos.AggrType
|
||||
@@ -333,7 +382,7 @@ func main() {
|
||||
for _, typeStr := range aggrTypesStr {
|
||||
aggrType, err := thanos.ParseAggrType(typeStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse aggregate type %q: %s", typeStr, err)
|
||||
return fmt.Errorf("failed to parse aggregate type %q: %w", typeStr, err)
|
||||
}
|
||||
aggrTypes = append(aggrTypes, aggrType)
|
||||
}
|
||||
@@ -366,7 +415,7 @@ func main() {
|
||||
bfMinDuration := c.Duration(vmNativeBackoffMinDuration)
|
||||
bf, err := backoff.New(bfRetries, bfFactor, bfMinDuration)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create backoff object: %s", err)
|
||||
return fmt.Errorf("failed to create backoff object: %w", err)
|
||||
}
|
||||
|
||||
disableKeepAlive := c.Bool(vmNativeDisableHTTPKeepAlive)
|
||||
@@ -390,7 +439,7 @@ func main() {
|
||||
|
||||
srcTC, err := promauth.NewTLSConfig(srcCertFile, srcKeyFile, srcCAFile, srcServerName, srcInsecureSkipVerify)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create TLS Config: %s", err)
|
||||
return fmt.Errorf("failed to create TLS Config: %w", err)
|
||||
}
|
||||
|
||||
trSrc := httputil.NewTransport(false, "vmctl_src")
|
||||
@@ -408,7 +457,7 @@ func main() {
|
||||
auth.WithBearer(c.String(vmNativeDstBearerToken)),
|
||||
auth.WithHeaders(c.String(vmNativeDstHeaders)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error initialize auth config for destination: %s", dstAddr)
|
||||
return fmt.Errorf("error initialize auth config for destination: %s: %w", dstAddr, err)
|
||||
}
|
||||
|
||||
// create TLS config
|
||||
@@ -420,7 +469,7 @@ func main() {
|
||||
|
||||
dstTC, err := promauth.NewTLSConfig(dstCertFile, dstKeyFile, dstCAFile, dstServerName, dstInsecureSkipVerify)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create TLS Config: %s", err)
|
||||
return fmt.Errorf("failed to create TLS Config: %w", err)
|
||||
}
|
||||
|
||||
trDst := httputil.NewTransport(false, "vmctl_dst")
|
||||
@@ -485,7 +534,7 @@ func main() {
|
||||
log.Printf("verifying block at path=%q", blockPath)
|
||||
f, err := os.OpenFile(blockPath, os.O_RDONLY, 0600)
|
||||
if err != nil {
|
||||
return cli.Exit(fmt.Errorf("cannot open exported block at path=%q err=%w", blockPath, err), 1)
|
||||
return cli.Exit(fmt.Errorf("cannot open exported block at path=%q: %w", blockPath, err), 1)
|
||||
}
|
||||
defer f.Close()
|
||||
var blocksCount atomic.Uint64
|
||||
@@ -493,7 +542,7 @@ func main() {
|
||||
blocksCount.Add(1)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return cli.Exit(fmt.Errorf("cannot parse block at path=%q, blocksCount=%d, err=%w", blockPath, blocksCount.Load(), err), 1)
|
||||
return cli.Exit(fmt.Errorf("cannot parse block at path=%q, blocksCount=%d: %w", blockPath, blocksCount.Load(), err), 1)
|
||||
}
|
||||
log.Printf("successfully verified block at path=%q, blockCount=%d", blockPath, blocksCount.Load())
|
||||
return nil
|
||||
@@ -536,7 +585,7 @@ func initConfigVM(c *cli.Context) (vm.Config, error) {
|
||||
|
||||
tr, err := promauth.NewTLSTransport(certFile, keyFile, caFile, serverName, insecureSkipVerify, "vmctl_client")
|
||||
if err != nil {
|
||||
return vm.Config{}, fmt.Errorf("failed to create transport for -%s=%q: %s", vmAddr, addr, err)
|
||||
return vm.Config{}, fmt.Errorf("failed to create transport for -%s=%q: %w", vmAddr, addr, err)
|
||||
}
|
||||
|
||||
bfRetries := c.Int(vmBackoffRetries)
|
||||
@@ -544,14 +593,21 @@ func initConfigVM(c *cli.Context) (vm.Config, error) {
|
||||
bfMinDuration := c.Duration(vmBackoffMinDuration)
|
||||
bf, err := backoff.New(bfRetries, bfFactor, bfMinDuration)
|
||||
if err != nil {
|
||||
return vm.Config{}, fmt.Errorf("failed to create backoff object: %s", err)
|
||||
return vm.Config{}, fmt.Errorf("failed to create backoff object: %w", err)
|
||||
}
|
||||
|
||||
authCfg, err := auth.Generate(
|
||||
auth.WithBasicAuth(c.String(vmUser), c.String(vmPassword)),
|
||||
auth.WithBearer(c.String(vmBearerToken)),
|
||||
auth.WithHeaders(c.String(vmHeaders)))
|
||||
if err != nil {
|
||||
return vm.Config{}, fmt.Errorf("error initialize auth config for destination: %s: %w", addr, err)
|
||||
}
|
||||
|
||||
return vm.Config{
|
||||
Addr: addr,
|
||||
Transport: tr,
|
||||
User: c.String(vmUser),
|
||||
Password: c.String(vmPassword),
|
||||
AuthCfg: authCfg,
|
||||
Concurrency: uint8(c.Int(vmConcurrency)),
|
||||
Compress: c.Bool(vmCompress),
|
||||
AccountID: c.String(vmAccountID),
|
||||
|
||||
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: %w", 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: %w", 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: %w, 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: %w", err)
|
||||
}
|
||||
err = os.Mkdir(filepath.Join(temp, "chunks"), os.ModePerm)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create temp dir: %w", 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: %w", err)
|
||||
}
|
||||
timeMax, err := utils.ParseTime(cfg.Filter.TimeMax)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse max time in filter: %w", 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: %w", 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: %w", 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: %w", 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: %w", err)
|
||||
}
|
||||
|
||||
r := bytes.NewReader(file)
|
||||
// Read all the content.
|
||||
gzipReader, err := gzip.NewReader(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create gzip reader: %w", err)
|
||||
}
|
||||
|
||||
var indexFile Index
|
||||
err = json.NewDecoder(gzipReader).Decode(&indexFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode bucket index: %w", 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)
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ func (c *Client) Explore(ctx context.Context, f Filter, tenantID string, start,
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
exploreRequestsErrorsTotal.Inc()
|
||||
return nil, fmt.Errorf("cannot create request to %q: %s", url, err)
|
||||
return nil, fmt.Errorf("cannot create request to %q: %w", url, err)
|
||||
}
|
||||
|
||||
params := req.URL.Query()
|
||||
@@ -60,14 +60,14 @@ func (c *Client) Explore(ctx context.Context, f Filter, tenantID string, start,
|
||||
if err != nil {
|
||||
exploreRequestsErrorsTotal.Inc()
|
||||
exploreDuration.UpdateDuration(startTime)
|
||||
return nil, fmt.Errorf("series request failed: %s", err)
|
||||
return nil, fmt.Errorf("series request failed: %w", err)
|
||||
}
|
||||
|
||||
var response Response
|
||||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||
exploreRequestsErrorsTotal.Inc()
|
||||
exploreDuration.UpdateDuration(startTime)
|
||||
return nil, fmt.Errorf("cannot decode series response: %s", err)
|
||||
return nil, fmt.Errorf("cannot decode series response: %w", err)
|
||||
}
|
||||
exploreDuration.UpdateDuration(startTime)
|
||||
return response.MetricNames, resp.Body.Close()
|
||||
@@ -80,19 +80,19 @@ func (c *Client) ImportPipe(ctx context.Context, dstURL string, pr *io.PipeReade
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, dstURL, pr)
|
||||
if err != nil {
|
||||
importRequestsErrorsTotal.Inc()
|
||||
return fmt.Errorf("cannot create import request to %q: %s", c.Addr, err)
|
||||
return fmt.Errorf("cannot create import request to %q: %w", c.Addr, err)
|
||||
}
|
||||
|
||||
importResp, err := c.do(req, http.StatusNoContent)
|
||||
if err != nil {
|
||||
importRequestsErrorsTotal.Inc()
|
||||
importDuration.UpdateDuration(startTime)
|
||||
return fmt.Errorf("import request failed: %s", err)
|
||||
return fmt.Errorf("import request failed: %w", err)
|
||||
}
|
||||
if err := importResp.Body.Close(); err != nil {
|
||||
importRequestsErrorsTotal.Inc()
|
||||
importDuration.UpdateDuration(startTime)
|
||||
return fmt.Errorf("cannot close import response body: %s", err)
|
||||
return fmt.Errorf("cannot close import response body: %w", err)
|
||||
}
|
||||
importDuration.UpdateDuration(startTime)
|
||||
return nil
|
||||
@@ -105,7 +105,7 @@ func (c *Client) ExportPipe(ctx context.Context, url string, f Filter) (io.ReadC
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
exportRequestsErrorsTotal.Inc()
|
||||
return nil, fmt.Errorf("cannot create request to %q: %s", c.Addr, err)
|
||||
return nil, fmt.Errorf("cannot create request to %q: %w", c.Addr, err)
|
||||
}
|
||||
|
||||
params := req.URL.Query()
|
||||
@@ -136,7 +136,7 @@ func (c *Client) GetSourceTenants(ctx context.Context, f Filter) ([]string, erro
|
||||
u := fmt.Sprintf("%s/%s", c.Addr, nativeTenantsAddr)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create request to %q: %s", u, err)
|
||||
return nil, fmt.Errorf("cannot create request to %q: %w", u, err)
|
||||
}
|
||||
|
||||
params := req.URL.Query()
|
||||
@@ -150,18 +150,18 @@ func (c *Client) GetSourceTenants(ctx context.Context, f Filter) ([]string, erro
|
||||
|
||||
resp, err := c.do(req, http.StatusOK)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tenants request failed: %s", err)
|
||||
return nil, fmt.Errorf("tenants request failed: %w", err)
|
||||
}
|
||||
|
||||
var r struct {
|
||||
Tenants []string `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
|
||||
return nil, fmt.Errorf("cannot decode tenants response: %s", err)
|
||||
return nil, fmt.Errorf("cannot decode tenants response: %w", err)
|
||||
}
|
||||
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
return nil, fmt.Errorf("cannot close tenants response body: %s", err)
|
||||
return nil, fmt.Errorf("cannot close tenants response body: %w", err)
|
||||
}
|
||||
|
||||
return r.Tenants, nil
|
||||
@@ -180,7 +180,7 @@ func (c *Client) do(req *http.Request, expSC int) (*http.Response, error) {
|
||||
if resp.StatusCode != expSC {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body for status code %d: %s", resp.StatusCode, err)
|
||||
return nil, fmt.Errorf("failed to read response body for status code %d: %w", resp.StatusCode, err)
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected response code %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func (op *otsdbProcessor) run(ctx context.Context) error {
|
||||
q := fmt.Sprintf("%s/api/suggest?type=metrics&q=%s&max=%d", op.oc.Addr, filter, op.oc.Limit)
|
||||
m, err := op.oc.FindMetrics(q)
|
||||
if err != nil {
|
||||
return fmt.Errorf("metric discovery failed for %q: %s", q, err)
|
||||
return fmt.Errorf("metric discovery failed for %q: %w", q, err)
|
||||
}
|
||||
metrics = append(metrics, m...)
|
||||
}
|
||||
@@ -76,7 +76,7 @@ func (op *otsdbProcessor) run(ctx context.Context) error {
|
||||
log.Printf("Starting work on %s", metric)
|
||||
serieslist, err := op.oc.FindSeries(metric)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't retrieve series list for %s : %s", metric, err)
|
||||
return fmt.Errorf("couldn't retrieve series list for %s: %w", metric, err)
|
||||
}
|
||||
/*
|
||||
Create channels for collecting/processing series and errors
|
||||
@@ -95,7 +95,7 @@ func (op *otsdbProcessor) run(ctx context.Context) error {
|
||||
for s := range seriesCh {
|
||||
if err := op.do(s); err != nil {
|
||||
otsdbErrorsTotal.Inc()
|
||||
errCh <- fmt.Errorf("couldn't retrieve series for %s : %s", metric, err)
|
||||
errCh <- fmt.Errorf("couldn't retrieve series for %s: %w", metric, err)
|
||||
return
|
||||
}
|
||||
otsdbSeriesProcessed.Inc()
|
||||
@@ -112,7 +112,7 @@ func (op *otsdbProcessor) run(ctx context.Context) error {
|
||||
// check for any lingering errors on the query side
|
||||
for otsdbErr := range errCh {
|
||||
if runErr == nil {
|
||||
runErr = fmt.Errorf("import process failed: \n%s", otsdbErr)
|
||||
runErr = fmt.Errorf("import process failed:\n%w", otsdbErr)
|
||||
}
|
||||
}
|
||||
bar.Finish()
|
||||
@@ -125,7 +125,7 @@ func (op *otsdbProcessor) run(ctx context.Context) error {
|
||||
for vmErr := range op.im.Errors() {
|
||||
if vmErr.Err != nil {
|
||||
otsdbErrorsTotal.Inc()
|
||||
return fmt.Errorf("import process failed: %s", wrapErr(vmErr, op.isVerbose))
|
||||
return fmt.Errorf("import process failed: %w", wrapErr(vmErr, op.isVerbose))
|
||||
}
|
||||
}
|
||||
log.Println("Import finished!")
|
||||
@@ -141,12 +141,12 @@ func (op *otsdbProcessor) sendQueries(ctx context.Context, serieslist []opentsdb
|
||||
for _, tr := range rt.QueryRanges {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("context canceled: %s", ctx.Err())
|
||||
return fmt.Errorf("context canceled: %w", ctx.Err())
|
||||
case otsdbErr := <-errCh:
|
||||
otsdbErrorsTotal.Inc()
|
||||
return fmt.Errorf("opentsdb error: %s", otsdbErr)
|
||||
return fmt.Errorf("opentsdb error: %w", otsdbErr)
|
||||
case vmErr := <-op.im.Errors():
|
||||
return fmt.Errorf("import process failed: %s", wrapErr(vmErr, op.isVerbose))
|
||||
return fmt.Errorf("import process failed: %w", wrapErr(vmErr, op.isVerbose))
|
||||
case seriesCh <- queryObj{
|
||||
Tr: tr, StartTime: startTime,
|
||||
Series: series, Rt: opentsdb.RetentionMeta{
|
||||
@@ -166,7 +166,7 @@ func (op *otsdbProcessor) do(s queryObj) error {
|
||||
end := s.StartTime - s.Tr.End
|
||||
data, err := op.oc.GetData(s.Series, s.Rt, start, end, op.oc.MsecsTime)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to collect data for %v in %v:%v :: %v", s.Series, s.Rt, s.Tr, err)
|
||||
return fmt.Errorf("failed to collect data for %v in %v:%v :: %w", s.Series, s.Rt, s.Tr, err)
|
||||
}
|
||||
if len(data.Timestamps) < 1 || len(data.Values) < 1 {
|
||||
log.Printf("no data found for %v in %v:%v...skipping", s.Series, s.Rt, s.Tr)
|
||||
|
||||
@@ -106,7 +106,7 @@ func (c Client) FindMetrics(q string) ([]string, error) {
|
||||
|
||||
resp, err := c.c.Get(q)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to send GET request to %q: %s", q, err)
|
||||
return nil, fmt.Errorf("failed to send GET request to %q: %w", q, err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode != 200 {
|
||||
@@ -114,12 +114,12 @@ func (c Client) FindMetrics(q string) ([]string, error) {
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not retrieve metric data from %q: %s", q, err)
|
||||
return nil, fmt.Errorf("could not retrieve metric data from %q: %w", q, err)
|
||||
}
|
||||
var metriclist []string
|
||||
err = json.Unmarshal(body, &metriclist)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response from %q: %s", q, err)
|
||||
return nil, fmt.Errorf("failed to read response from %q: %w", q, err)
|
||||
}
|
||||
return metriclist, nil
|
||||
}
|
||||
@@ -130,7 +130,7 @@ func (c Client) FindSeries(metric string) ([]Meta, error) {
|
||||
q := fmt.Sprintf("%s/api/search/lookup?m=%s&limit=%d", c.Addr, metric, c.Limit)
|
||||
resp, err := c.c.Get(q)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to send GET request to %q: %s", q, err)
|
||||
return nil, fmt.Errorf("failed to send GET request to %q: %w", q, err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode != 200 {
|
||||
@@ -138,12 +138,12 @@ func (c Client) FindSeries(metric string) ([]Meta, error) {
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not retrieve series data from %q: %s", q, err)
|
||||
return nil, fmt.Errorf("could not retrieve series data from %q: %w", q, err)
|
||||
}
|
||||
var results MetaResults
|
||||
err = json.Unmarshal(body, &results)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response from %q: %s", q, err)
|
||||
return nil, fmt.Errorf("failed to read response from %q: %w", q, err)
|
||||
}
|
||||
return results.Results, nil
|
||||
}
|
||||
@@ -183,7 +183,7 @@ func (c Client) GetData(series Meta, rt RetentionMeta, start int64, end int64, m
|
||||
q := fmt.Sprintf("%s/api/query?%s", c.Addr, queryStr)
|
||||
resp, err := c.c.Get(q)
|
||||
if err != nil {
|
||||
return Metric{}, fmt.Errorf("failed to send GET request to %q: %s", q, err)
|
||||
return Metric{}, fmt.Errorf("failed to send GET request to %q: %w", q, err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
/*
|
||||
@@ -303,7 +303,7 @@ func NewClient(cfg Config) (*Client, error) {
|
||||
for _, r := range cfg.Retentions {
|
||||
ret, err := convertRetention(r, offsetSecs, cfg.MsecsTime)
|
||||
if err != nil {
|
||||
return &Client{}, fmt.Errorf("couldn't parse retention %q :: %v", r, err)
|
||||
return &Client{}, fmt.Errorf("couldn't parse retention %q :: %w", r, err)
|
||||
}
|
||||
retentions = append(retentions, ret)
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ func convertRetention(retention string, offset int64, msecTime bool) (Retention,
|
||||
}
|
||||
queryLengthDuration, err := convertDuration(chunks[2])
|
||||
if err != nil {
|
||||
return Retention{}, fmt.Errorf("invalid ttl (second order) duration string: %q: %s", chunks[2], err)
|
||||
return Retention{}, fmt.Errorf("invalid ttl (second order) duration string: %q: %w", chunks[2], err)
|
||||
}
|
||||
// set ttl in milliseconds, unless we aren't using millisecond time in OpenTSDB...then use seconds
|
||||
queryLength := queryLengthDuration.Milliseconds()
|
||||
@@ -110,7 +110,7 @@ func convertRetention(retention string, offset int64, msecTime bool) (Retention,
|
||||
|
||||
aggTimeDuration, err := convertDuration(aggregates[1])
|
||||
if err != nil {
|
||||
return Retention{}, fmt.Errorf("invalid aggregation time duration string: %q: %s", aggregates[1], err)
|
||||
return Retention{}, fmt.Errorf("invalid aggregation time duration string: %q: %w", aggregates[1], err)
|
||||
}
|
||||
aggTime := aggTimeDuration.Milliseconds()
|
||||
if !msecTime {
|
||||
@@ -119,7 +119,7 @@ func convertRetention(retention string, offset int64, msecTime bool) (Retention,
|
||||
|
||||
rowLengthDuration, err := convertDuration(chunks[1])
|
||||
if err != nil {
|
||||
return Retention{}, fmt.Errorf("invalid row length (first order) duration string: %q: %s", chunks[1], err)
|
||||
return Retention{}, fmt.Errorf("invalid row length (first order) duration string: %q: %w", chunks[1], err)
|
||||
}
|
||||
// set length of each row in milliseconds, unless we aren't using millisecond time in OpenTSDB...then use seconds
|
||||
rowLength := rowLengthDuration.Milliseconds()
|
||||
|
||||
@@ -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
|
||||
@@ -38,7 +46,7 @@ type prometheusProcessor struct {
|
||||
func (pp *prometheusProcessor) run(ctx context.Context) error {
|
||||
blocks, err := pp.cl.Explore()
|
||||
if err != nil {
|
||||
return fmt.Errorf("explore failed: %s", err)
|
||||
return fmt.Errorf("explore failed: %w", err)
|
||||
}
|
||||
if len(blocks) < 1 {
|
||||
return fmt.Errorf("found no blocks to import")
|
||||
@@ -48,8 +56,8 @@ func (pp *prometheusProcessor) run(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := pp.processBlocks(blocks); err != nil {
|
||||
return fmt.Errorf("migration failed: %s", err)
|
||||
if err := pp.processBlocks(ctx, blocks); err != nil {
|
||||
return fmt.Errorf("migration failed: %w", err)
|
||||
}
|
||||
|
||||
log.Println("Import finished!")
|
||||
@@ -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)
|
||||
return fmt.Errorf("failed to read block: %w", 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: %w", 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()
|
||||
}
|
||||
@@ -145,11 +164,11 @@ func (pp *prometheusProcessor) processBlocks(blocks []tsdb.BlockReader) error {
|
||||
select {
|
||||
case promErr := <-errCh:
|
||||
close(blockReadersCh)
|
||||
return fmt.Errorf("prometheus error: %s", promErr)
|
||||
return fmt.Errorf("prometheus error: %w", promErr)
|
||||
case vmErr := <-pp.im.Errors():
|
||||
close(blockReadersCh)
|
||||
promErrorsTotal.Inc()
|
||||
return fmt.Errorf("import process failed: %s", wrapErr(vmErr, pp.isVerbose))
|
||||
return fmt.Errorf("import process failed: %w", wrapErr(vmErr, pp.isVerbose))
|
||||
case blockReadersCh <- br:
|
||||
}
|
||||
}
|
||||
@@ -163,11 +182,11 @@ func (pp *prometheusProcessor) processBlocks(blocks []tsdb.BlockReader) error {
|
||||
for vmErr := range pp.im.Errors() {
|
||||
if vmErr.Err != nil {
|
||||
promErrorsTotal.Inc()
|
||||
return fmt.Errorf("import process failed: %s", wrapErr(vmErr, pp.isVerbose))
|
||||
return fmt.Errorf("import process failed: %w", wrapErr(vmErr, pp.isVerbose))
|
||||
}
|
||||
}
|
||||
for err := range errCh {
|
||||
return fmt.Errorf("import process failed: %s", err)
|
||||
return fmt.Errorf("import process failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -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
|
||||
@@ -57,16 +59,16 @@ func (f filter) inRange(minV, maxV int64) bool {
|
||||
func NewClient(cfg Config) (*Client, error) {
|
||||
db, err := tsdb.OpenDBReadOnly(cfg.Snapshot, cfg.TemporaryDir, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open snapshot %q: %s", cfg.Snapshot, err)
|
||||
return nil, fmt.Errorf("failed to open snapshot %q: %w", cfg.Snapshot, err)
|
||||
}
|
||||
c := &Client{DBReadOnly: db}
|
||||
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)
|
||||
return nil, fmt.Errorf("failed to parse time in filter: %w", err)
|
||||
}
|
||||
c.filter = filter{
|
||||
min: minTime,
|
||||
max: maxTime,
|
||||
min: timeMin,
|
||||
max: timeMax,
|
||||
label: cfg.Filter.Label,
|
||||
labelValue: cfg.Filter.LabelValue,
|
||||
}
|
||||
@@ -81,9 +83,9 @@ func NewClient(cfg Config) (*Client, error) {
|
||||
func (c *Client) Explore() ([]tsdb.BlockReader, error) {
|
||||
blocks, err := c.Blocks()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch blocks: %s", err)
|
||||
return nil, fmt.Errorf("failed to fetch blocks: %w", err)
|
||||
}
|
||||
s := &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) {
|
||||
@@ -134,14 +142,14 @@ func parseTime(start, end string) (int64, int64, error) {
|
||||
if start != "" {
|
||||
v, err := time.Parse(time.RFC3339, start)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("failed to parse %q: %s", start, err)
|
||||
return 0, 0, fmt.Errorf("failed to parse %q: %w", start, err)
|
||||
}
|
||||
s = v.UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
if end != "" {
|
||||
v, err := time.Parse(time.RFC3339, end)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("failed to parse %q: %s", end, err)
|
||||
return 0, 0, fmt.Errorf("failed to parse %q: %w", end, err)
|
||||
}
|
||||
e = v.UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func (rrp *remoteReadProcessor) run(ctx context.Context) error {
|
||||
|
||||
ranges, err := stepper.SplitDateRange(*rrp.filter.timeStart, *rrp.filter.timeEnd, rrp.filter.chunk, rrp.filter.timeReverse)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create date ranges for the given time filters: %v", err)
|
||||
return fmt.Errorf("failed to create date ranges for the given time filters: %w", err)
|
||||
}
|
||||
|
||||
question := fmt.Sprintf("Selected time range %q - %q will be split into %d ranges according to %q step. Continue?",
|
||||
@@ -74,7 +74,7 @@ func (rrp *remoteReadProcessor) run(ctx context.Context) error {
|
||||
for r := range rangeC {
|
||||
if err := rrp.do(ctx, r); err != nil {
|
||||
remoteReadErrorsTotal.Inc()
|
||||
errCh <- fmt.Errorf("request failed for: %s", err)
|
||||
errCh <- fmt.Errorf("request failed for: %w", err)
|
||||
return
|
||||
}
|
||||
remoteReadRangesProcessed.Inc()
|
||||
@@ -86,10 +86,10 @@ func (rrp *remoteReadProcessor) run(ctx context.Context) error {
|
||||
for _, r := range ranges {
|
||||
select {
|
||||
case infErr := <-errCh:
|
||||
return fmt.Errorf("remote read error: %s", infErr)
|
||||
return fmt.Errorf("remote read error: %w", infErr)
|
||||
case vmErr := <-rrp.dst.Errors():
|
||||
remoteReadErrorsTotal.Inc()
|
||||
return fmt.Errorf("import process failed: %s", wrapErr(vmErr, rrp.isVerbose))
|
||||
return fmt.Errorf("import process failed: %w", wrapErr(vmErr, rrp.isVerbose))
|
||||
case rangeC <- &remoteread.Filter{
|
||||
StartTimestampMs: r[0].UnixMilli(),
|
||||
EndTimestampMs: r[1].UnixMilli(),
|
||||
@@ -105,11 +105,11 @@ func (rrp *remoteReadProcessor) run(ctx context.Context) error {
|
||||
for vmErr := range rrp.dst.Errors() {
|
||||
if vmErr.Err != nil {
|
||||
remoteReadErrorsTotal.Inc()
|
||||
return fmt.Errorf("import process failed: %s", wrapErr(vmErr, rrp.isVerbose))
|
||||
return fmt.Errorf("import process failed: %w", wrapErr(vmErr, rrp.isVerbose))
|
||||
}
|
||||
}
|
||||
for err := range errCh {
|
||||
return fmt.Errorf("import process failed: %s", err)
|
||||
return fmt.Errorf("import process failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -119,7 +119,7 @@ func (rrp *remoteReadProcessor) do(ctx context.Context, filter *remoteread.Filte
|
||||
return rrp.src.Read(ctx, filter, func(series *vm.TimeSeries) error {
|
||||
if err := rrp.dst.Input(series); err != nil {
|
||||
return fmt.Errorf(
|
||||
"failed to read data for time range start: %d, end: %d, %s",
|
||||
"failed to read data for time range start: %d, end: %d: %w",
|
||||
filter.StartTimestampMs, filter.EndTimestampMs, err)
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -157,7 +157,7 @@ func (c *Client) Read(ctx context.Context, filter *Filter, streamCb StreamCallba
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return fmt.Errorf("fetch request has ben cancelled")
|
||||
}
|
||||
return fmt.Errorf("error while fetching data from remote storage: %s", err)
|
||||
return fmt.Errorf("error while fetching data from remote storage: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func (f filter) inRange(minV, maxV int64) bool {
|
||||
func NewClient(cfg Config) (*Client, error) {
|
||||
minTime, maxTime, err := parseTime(cfg.Filter.TimeMin, cfg.Filter.TimeMax)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse time in filter: %s", err)
|
||||
return nil, fmt.Errorf("failed to parse time in filter: %w", err)
|
||||
}
|
||||
return &Client{
|
||||
snapshotPath: cfg.Snapshot,
|
||||
@@ -183,14 +183,14 @@ func parseTime(start, end string) (int64, int64, error) {
|
||||
if start != "" {
|
||||
v, err := time.Parse(time.RFC3339, start)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("failed to parse %q: %s", start, err)
|
||||
return 0, 0, fmt.Errorf("failed to parse %q: %w", start, err)
|
||||
}
|
||||
s = v.UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
if end != "" {
|
||||
v, err := time.Parse(time.RFC3339, end)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("failed to parse %q: %s", end, err)
|
||||
return 0, 0, fmt.Errorf("failed to parse %q: %w", end, err)
|
||||
}
|
||||
e = v.UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func (tp *thanosProcessor) run(ctx context.Context) error {
|
||||
// Use the first aggregate type to explore blocks (block list is the same for all types)
|
||||
blocks, err := tp.cl.Explore(tp.aggrTypes[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("explore failed: %s", err)
|
||||
return fmt.Errorf("explore failed: %w", err)
|
||||
}
|
||||
if len(blocks) < 1 {
|
||||
return fmt.Errorf("found no blocks to import")
|
||||
@@ -84,7 +84,7 @@ func (tp *thanosProcessor) run(ctx context.Context) error {
|
||||
log.Println("Processing raw blocks (resolution=0)...")
|
||||
stats, err := tp.processBlocks(rawBlocks, thanos.AggrTypeNone, bar)
|
||||
if err != nil {
|
||||
return fmt.Errorf("migration failed for raw blocks: %s", err)
|
||||
return fmt.Errorf("migration failed for raw blocks: %w", err)
|
||||
}
|
||||
phases = append(phases, phaseStats{
|
||||
name: "raw",
|
||||
@@ -108,7 +108,7 @@ func (tp *thanosProcessor) run(ctx context.Context) error {
|
||||
|
||||
aggrBlocks, err := tp.cl.Explore(aggrType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("explore failed for aggr type %s: %s", aggrType, err)
|
||||
return fmt.Errorf("explore failed for aggr type %s: %w", aggrType, err)
|
||||
}
|
||||
|
||||
var downsampledOnly []thanos.BlockInfo
|
||||
@@ -128,7 +128,7 @@ func (tp *thanosProcessor) run(ctx context.Context) error {
|
||||
stats, err := tp.processBlocks(downsampledOnly, aggrType, bar)
|
||||
thanos.CloseBlocks(aggrBlocks)
|
||||
if err != nil {
|
||||
return fmt.Errorf("migration failed for aggr type %s: %s", aggrType, err)
|
||||
return fmt.Errorf("migration failed for aggr type %s: %w", aggrType, err)
|
||||
}
|
||||
phases = append(phases, phaseStats{
|
||||
name: aggrType.String(),
|
||||
@@ -153,7 +153,7 @@ func (tp *thanosProcessor) run(ctx context.Context) error {
|
||||
for vmErr := range tp.im.Errors() {
|
||||
if vmErr.Err != nil {
|
||||
thanosErrorsTotal.Inc()
|
||||
return fmt.Errorf("import process failed: %s", wrapErr(vmErr, tp.isVerbose))
|
||||
return fmt.Errorf("import process failed: %w", wrapErr(vmErr, tp.isVerbose))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ func (tp *thanosProcessor) processBlocks(blocks []thanos.BlockInfo, aggrType tha
|
||||
seriesCount, samplesCount, err := tp.do(bi, aggrType)
|
||||
if err != nil {
|
||||
thanosErrorsTotal.Inc()
|
||||
errCh <- fmt.Errorf("read failed for block %q with aggr %s: %s", bi.Block.Meta().ULID, aggrType, err)
|
||||
errCh <- fmt.Errorf("read failed for block %q with aggr %s: %w", bi.Block.Meta().ULID, aggrType, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -209,12 +209,12 @@ func (tp *thanosProcessor) processBlocks(blocks []thanos.BlockInfo, aggrType tha
|
||||
case thanosErr := <-errCh:
|
||||
close(blockReadersCh)
|
||||
wg.Wait()
|
||||
return processBlocksStats{}, fmt.Errorf("thanos error: %s", thanosErr)
|
||||
return processBlocksStats{}, fmt.Errorf("thanos error: %w", thanosErr)
|
||||
case vmErr := <-tp.im.Errors():
|
||||
close(blockReadersCh)
|
||||
wg.Wait()
|
||||
thanosErrorsTotal.Inc()
|
||||
return processBlocksStats{}, fmt.Errorf("import process failed: %s", wrapErr(vmErr, tp.isVerbose))
|
||||
return processBlocksStats{}, fmt.Errorf("import process failed: %w", wrapErr(vmErr, tp.isVerbose))
|
||||
case blockReadersCh <- bi:
|
||||
}
|
||||
}
|
||||
@@ -223,7 +223,7 @@ func (tp *thanosProcessor) processBlocks(blocks []thanos.BlockInfo, aggrType tha
|
||||
wg.Wait()
|
||||
close(errCh)
|
||||
for err := range errCh {
|
||||
return processBlocksStats{}, fmt.Errorf("import process failed: %s", err)
|
||||
return processBlocksStats{}, fmt.Errorf("import process failed: %w", err)
|
||||
}
|
||||
|
||||
return processBlocksStats{
|
||||
@@ -236,7 +236,7 @@ func (tp *thanosProcessor) processBlocks(blocks []thanos.BlockInfo, aggrType tha
|
||||
func (tp *thanosProcessor) do(bi thanos.BlockInfo, aggrType thanos.AggrType) (uint64, uint64, error) {
|
||||
ss, err := tp.cl.Read(bi)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("failed to read block: %s", err)
|
||||
return 0, 0, fmt.Errorf("failed to read block: %w", err)
|
||||
}
|
||||
defer ss.Close() // Ensure querier is closed even on early returns
|
||||
|
||||
|
||||
@@ -74,9 +74,9 @@ func wrapErr(vmErr *vm.ImportError, verbose bool) error {
|
||||
verboseMsg = "(enable `--verbose` output to get more details)"
|
||||
}
|
||||
if vmErr.Err == nil {
|
||||
return fmt.Errorf("%s\n\tLatest delivered batch for timestamps range %d - %d %s\n%s",
|
||||
return fmt.Errorf("%w\n\tLatest delivered batch for timestamps range %d - %d %s\n%s",
|
||||
vmErr.Err, minTS, maxTS, verboseMsg, errTS)
|
||||
}
|
||||
return fmt.Errorf("%s\n\tImporting batch failed for timestamps range %d - %d %s\n%s",
|
||||
return fmt.Errorf("%w\n\tImporting batch failed for timestamps range %d - %d %s\n%s",
|
||||
vmErr.Err, minTS, maxTS, verboseMsg, errTS)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/auth"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/backoff"
|
||||
@@ -27,6 +28,8 @@ type Config struct {
|
||||
// --httpListenAddr value for single node version
|
||||
// --httpListenAddr value of vmselect component for cluster version
|
||||
Addr string
|
||||
|
||||
AuthCfg *auth.Config
|
||||
// Transport allows specifying custom http.Transport
|
||||
Transport *http.Transport
|
||||
// Concurrency defines number of worker
|
||||
@@ -40,10 +43,6 @@ type Config struct {
|
||||
// BatchSize defines how many samples
|
||||
// importer collects before sending the import request
|
||||
BatchSize int
|
||||
// User name for basic auth
|
||||
User string
|
||||
// Password for basic auth
|
||||
Password string
|
||||
// SignificantFigures defines the number of significant figures to leave
|
||||
// in metric values before importing.
|
||||
// Zero value saves all the significant decimal places
|
||||
@@ -65,11 +64,10 @@ type Config struct {
|
||||
// see https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#how-to-import-time-series-data
|
||||
type Importer struct {
|
||||
addr string
|
||||
authCfg *auth.Config
|
||||
client *http.Client
|
||||
importPath string
|
||||
compress bool
|
||||
user string
|
||||
password string
|
||||
|
||||
close chan struct{}
|
||||
input chan *TimeSeries
|
||||
@@ -148,8 +146,7 @@ func NewImporter(ctx context.Context, cfg Config) (*Importer, error) {
|
||||
client: client,
|
||||
importPath: importPath,
|
||||
compress: cfg.Compress,
|
||||
user: cfg.User,
|
||||
password: cfg.Password,
|
||||
authCfg: cfg.AuthCfg,
|
||||
rl: limiter.NewLimiter(cfg.RateLimit),
|
||||
close: make(chan struct{}),
|
||||
input: make(chan *TimeSeries, cfg.Concurrency*4),
|
||||
@@ -163,7 +160,7 @@ func NewImporter(ctx context.Context, cfg Config) (*Importer, error) {
|
||||
importDuration: metrics.GetOrCreateHistogram(`vmctl_importer_request_duration_seconds`),
|
||||
}
|
||||
if err := im.Ping(); err != nil {
|
||||
return nil, fmt.Errorf("ping to %q failed: %s", addr, err)
|
||||
return nil, fmt.Errorf("ping to %q failed: %w", addr, err)
|
||||
}
|
||||
|
||||
if cfg.BatchSize < 1 {
|
||||
@@ -289,7 +286,7 @@ func (im *Importer) flush(ctx context.Context, b []*TimeSeries) error {
|
||||
retryableFunc := func() error { return im.Import(b) }
|
||||
attempts, err := im.backoff.Retry(ctx, retryableFunc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("import failed with %d retries: %s", attempts, err)
|
||||
return fmt.Errorf("import failed with %d retries: %w", attempts, err)
|
||||
}
|
||||
im.s.Lock()
|
||||
im.s.retries = attempts
|
||||
@@ -302,10 +299,10 @@ func (im *Importer) Ping() error {
|
||||
url := fmt.Sprintf("%s/health", im.addr)
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create request to %q: %s", im.addr, err)
|
||||
return fmt.Errorf("cannot create request to %q: %w", im.addr, err)
|
||||
}
|
||||
if im.user != "" {
|
||||
req.SetBasicAuth(im.user, im.password)
|
||||
if im.authCfg != nil {
|
||||
im.authCfg.SetHeaders(req, true)
|
||||
}
|
||||
resp, err := im.client.Do(req)
|
||||
if err != nil {
|
||||
@@ -332,10 +329,10 @@ func (im *Importer) Import(tsBatch []*TimeSeries) error {
|
||||
req, err := http.NewRequest(http.MethodPost, im.importPath, pr)
|
||||
if err != nil {
|
||||
im.importRequestsErrorsTotal.Inc()
|
||||
return fmt.Errorf("cannot create request to %q: %s", im.addr, err)
|
||||
return fmt.Errorf("cannot create request to %q: %w", im.addr, err)
|
||||
}
|
||||
if im.user != "" {
|
||||
req.SetBasicAuth(im.user, im.password)
|
||||
if im.authCfg != nil {
|
||||
im.authCfg.SetHeaders(req, true)
|
||||
}
|
||||
if im.compress {
|
||||
req.Header.Set("Content-Encoding", "gzip")
|
||||
@@ -352,7 +349,7 @@ func (im *Importer) Import(tsBatch []*TimeSeries) error {
|
||||
zw, err := gzip.NewWriterLevel(w, 1)
|
||||
if err != nil {
|
||||
im.importRequestsErrorsTotal.Inc()
|
||||
return fmt.Errorf("unexpected error when creating gzip writer: %s", err)
|
||||
return fmt.Errorf("unexpected error when creating gzip writer: %w", err)
|
||||
}
|
||||
w = zw
|
||||
}
|
||||
@@ -411,7 +408,7 @@ var ErrBadRequest = errors.New("bad request")
|
||||
func (im *Importer) do(req *http.Request) error {
|
||||
resp, err := im.client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unexpected error when performing request: %s", err)
|
||||
return fmt.Errorf("unexpected error when performing request: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
@@ -419,7 +416,7 @@ func (im *Importer) do(req *http.Request) error {
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read response body for status code %d: %s", resp.StatusCode, err)
|
||||
return fmt.Errorf("failed to read response body for status code %d: %w", resp.StatusCode, err)
|
||||
}
|
||||
if resp.StatusCode == http.StatusBadRequest {
|
||||
return fmt.Errorf("%w: unexpected response code %d: %s", ErrBadRequest, resp.StatusCode, string(body))
|
||||
|
||||
@@ -55,14 +55,14 @@ func (p *vmNativeProcessor) run(ctx context.Context) error {
|
||||
|
||||
start, err := vmctlutil.ParseTime(p.filter.TimeStart)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse %s, provided: %s, error: %w", vmNativeFilterTimeStart, p.filter.TimeStart, err)
|
||||
return fmt.Errorf("failed to parse %s, provided: %s: %w", vmNativeFilterTimeStart, p.filter.TimeStart, err)
|
||||
}
|
||||
|
||||
end := time.Now().In(start.Location())
|
||||
if p.filter.TimeEnd != "" {
|
||||
end, err = vmctlutil.ParseTime(p.filter.TimeEnd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse %s, provided: %s, error: %w", vmNativeFilterTimeEnd, p.filter.TimeEnd, err)
|
||||
return fmt.Errorf("failed to parse %s, provided: %s: %w", vmNativeFilterTimeEnd, p.filter.TimeEnd, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ func (p *vmNativeProcessor) run(ctx context.Context) error {
|
||||
err := p.runBackfilling(ctx, tenantID, ranges)
|
||||
if err != nil {
|
||||
migrationErrorsTotal.Inc()
|
||||
return fmt.Errorf("migration failed: %s", err)
|
||||
return fmt.Errorf("migration failed: %w", err)
|
||||
}
|
||||
|
||||
if p.interCluster {
|
||||
@@ -157,7 +157,7 @@ func (p *vmNativeProcessor) runSingle(ctx context.Context, f native.Filter, srcU
|
||||
}
|
||||
default:
|
||||
}
|
||||
return fmt.Errorf("failed to write into %q: %s", p.dst.Addr, err)
|
||||
return fmt.Errorf("failed to write into %q: %w", p.dst.Addr, err)
|
||||
}
|
||||
|
||||
p.s.Lock()
|
||||
@@ -184,7 +184,7 @@ func (p *vmNativeProcessor) runBackfilling(ctx context.Context, tenantID string,
|
||||
|
||||
importAddr, err := vm.AddExtraLabelsToImportPath(importAddr, p.dst.ExtraLabels)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add labels to import path: %s", err)
|
||||
return fmt.Errorf("failed to add labels to import path: %w", err)
|
||||
}
|
||||
dstURL := fmt.Sprintf("%s/%s", p.dst.Addr, importAddr)
|
||||
|
||||
@@ -222,7 +222,7 @@ func (p *vmNativeProcessor) runBackfilling(ctx context.Context, tenantID string,
|
||||
format = fmt.Sprintf(nativeWithBackoffTpl, barPrefix)
|
||||
metricsMap, err = p.explore(ctx, p.src, tenantID, ranges)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to explore metric names: %s", err)
|
||||
return fmt.Errorf("failed to explore metric names: %w", err)
|
||||
}
|
||||
if len(metricsMap) == 0 {
|
||||
errMsg := "no metrics found"
|
||||
@@ -295,7 +295,7 @@ func (p *vmNativeProcessor) runBackfilling(ctx context.Context, tenantID string,
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("context canceled")
|
||||
case infErr := <-errCh:
|
||||
return fmt.Errorf("export/import error: %s", infErr)
|
||||
return fmt.Errorf("export/import error: %w", infErr)
|
||||
case filterCh <- native.Filter{
|
||||
Match: match,
|
||||
TimeStart: times[0].Format(time.RFC3339),
|
||||
@@ -313,7 +313,7 @@ func (p *vmNativeProcessor) runBackfilling(ctx context.Context, tenantID string,
|
||||
close(errCh)
|
||||
|
||||
for err := range errCh {
|
||||
return fmt.Errorf("import process failed: %s", err)
|
||||
return fmt.Errorf("import process failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -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"+
|
||||
@@ -26,7 +26,7 @@ var (
|
||||
// NewVMinsertServer creates and start vminsert server at the given addr
|
||||
func NewVMinsertServer(addr string, tc *tls.Config) (*vminsertapi.VMInsertServer, error) {
|
||||
api := &vminsertAPI{}
|
||||
return vminsertapi.NewVMInsertServer(addr, *vminsertConnsShutdownDuration, "clusternative", api, tc)
|
||||
return vminsertapi.NewServer(addr, *vminsertConnsShutdownDuration, "clusternative", api, tc)
|
||||
}
|
||||
|
||||
type vminsertAPI struct {
|
||||
|
||||
@@ -125,6 +125,7 @@ func main() {
|
||||
|
||||
relabel.Init()
|
||||
timeserieslimits.Init(*maxLabelsPerTimeseries, *maxLabelNameLen, *maxLabelValueLen)
|
||||
opentelemetry.Init()
|
||||
protoparserutil.StartUnmarshalWorkers()
|
||||
if len(*clusternativeListenAddr) > 0 {
|
||||
s, err := clusternative.NewVMinsertServer(*clusternativeListenAddr, nil)
|
||||
|
||||
@@ -24,6 +24,11 @@ var (
|
||||
metadataInserted = metrics.NewCounter(`vm_metadata_rows_inserted_total{type="opentelemetry"}`)
|
||||
)
|
||||
|
||||
// Init must be called after flag.Parse and before using the opentelemetry package.
|
||||
func Init() {
|
||||
stream.InitDecodeOptions()
|
||||
}
|
||||
|
||||
// InsertHandler processes opentelemetry metrics.
|
||||
func InsertHandler(at *auth.Token, req *http.Request) error {
|
||||
extraLabels, err := protoparserutil.GetExtraLabels(req)
|
||||
|
||||
@@ -36,9 +36,6 @@ var (
|
||||
func NewVMSelectServer(addr string) (*vmselectapi.Server, error) {
|
||||
api := &vmstorageAPI{}
|
||||
limits := vmselectapi.Limits{
|
||||
MaxLabelNames: *maxTagKeys,
|
||||
MaxLabelValues: *maxTagValues,
|
||||
MaxTagValueSuffixes: *maxTagValueSuffixesPerSearch,
|
||||
MaxConcurrentRequests: *maxConcurrentRequests,
|
||||
MaxConcurrentRequestsFlagName: "clusternative.maxConcurrentRequests",
|
||||
MaxQueueDuration: *maxQueueDuration,
|
||||
@@ -69,6 +66,9 @@ func (api *vmstorageAPI) SearchMetricNames(qt *querytracer.Tracer, sq *storage.S
|
||||
}
|
||||
|
||||
func (api *vmstorageAPI) LabelValues(qt *querytracer.Tracer, sq *storage.SearchQuery, labelName string, maxLabelValues int, deadline uint64) ([]string, error) {
|
||||
if maxLabelValues <= 0 || maxLabelValues > *maxTagValues {
|
||||
maxLabelValues = *maxTagValues
|
||||
}
|
||||
dl := searchutil.DeadlineFromTimestamp(deadline)
|
||||
labelValues, _, err := netstorage.LabelValues(qt, true, labelName, sq, maxLabelValues, dl)
|
||||
return labelValues, wrapClusterNativeError(err)
|
||||
@@ -76,12 +76,18 @@ func (api *vmstorageAPI) LabelValues(qt *querytracer.Tracer, sq *storage.SearchQ
|
||||
|
||||
func (api *vmstorageAPI) TagValueSuffixes(qt *querytracer.Tracer, accountID, projectID uint32, tr storage.TimeRange, tagKey, tagValuePrefix string, delimiter byte,
|
||||
maxSuffixes int, deadline uint64) ([]string, error) {
|
||||
if maxSuffixes <= 0 || maxSuffixes > *maxTagValueSuffixesPerSearch {
|
||||
maxSuffixes = *maxTagValueSuffixesPerSearch
|
||||
}
|
||||
dl := searchutil.DeadlineFromTimestamp(deadline)
|
||||
suffixes, _, err := netstorage.TagValueSuffixes(qt, accountID, projectID, true, tr, tagKey, tagValuePrefix, delimiter, maxSuffixes, dl)
|
||||
return suffixes, wrapClusterNativeError(err)
|
||||
}
|
||||
|
||||
func (api *vmstorageAPI) LabelNames(qt *querytracer.Tracer, sq *storage.SearchQuery, maxLabelNames int, deadline uint64) ([]string, error) {
|
||||
if maxLabelNames <= 0 || maxLabelNames > *maxTagKeys {
|
||||
maxLabelNames = *maxTagKeys
|
||||
}
|
||||
dl := searchutil.DeadlineFromTimestamp(deadline)
|
||||
labelNames, _, err := netstorage.LabelNames(qt, true, sq, maxLabelNames, dl)
|
||||
return labelNames, wrapClusterNativeError(err)
|
||||
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
nethttputil "net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -39,6 +37,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/tenantmetrics"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timerpool"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/vmalertproxy"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/vmselectapi"
|
||||
)
|
||||
|
||||
@@ -63,8 +62,12 @@ var (
|
||||
|
||||
logSlowQueryDuration = flag.Duration("search.logSlowQueryDuration", 5*time.Second, "Log queries with execution time exceeding this value. Zero disables slow query logging. "+
|
||||
"See also -search.logQueryMemoryUsage")
|
||||
vmalertProxyURL = flag.String("vmalert.proxyURL", "", "Optional URL for proxying requests to vmalert. For example, if -vmalert.proxyURL=http://vmalert:8880 , then alerting API requests such as /api/v1/rules from Grafana will be proxied to http://vmalert:8880/api/v1/rules")
|
||||
storageNodes = flagutil.NewArrayString("storageNode", "Comma-separated addresses of vmstorage nodes; usage: -storageNode=vmstorage-host1,...,vmstorage-hostN . "+
|
||||
|
||||
vmalertProxyURL = flag.String("vmalert.proxyURL", "", "Optional URL for proxying requests to vmalert. For example, if -vmalert.proxyURL=http://vmalert:8880 , "+
|
||||
"then alerting API requests such as /api/v1/rules from Grafana will be proxied to http://vmalert:8880/api/v1/rules . "+
|
||||
"See https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#vmalert")
|
||||
|
||||
storageNodes = flagutil.NewArrayString("storageNode", "Comma-separated addresses of vmstorage nodes; usage: -storageNode=vmstorage-host1,...,vmstorage-hostN . "+
|
||||
"Enterprise version of VictoriaMetrics supports automatic discovery of vmstorage addresses via DNS SRV records. For example, -storageNode=srv+vmstorage.addrs . "+
|
||||
"See https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#automatic-vmstorage-discovery")
|
||||
|
||||
@@ -126,7 +129,9 @@ func main() {
|
||||
}
|
||||
concurrencyLimitCh = make(chan struct{}, *maxConcurrentRequests)
|
||||
initVMUIConfig()
|
||||
initVMAlertProxy()
|
||||
|
||||
vmalertproxy.Init(*vmalertProxyURL)
|
||||
flagutil.RegisterSecretFlag("vmalert.proxyURL")
|
||||
|
||||
var vmselectapiServer *vmselectapi.Server
|
||||
if *clusternativeListenAddr != "" {
|
||||
@@ -723,10 +728,12 @@ func handleStaticAndSimpleRequests(w http.ResponseWriter, r *http.Request, path
|
||||
if len(*vmalertProxyURL) == 0 {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, "%s", `{"status":"error","msg":"for accessing vmalert flag '-vmalert.proxyURL' must be configured"}`)
|
||||
fmt.Fprintf(w, "%s", `{"status":"error","msg":"the '-vmalert.proxyURL' command-line must be configured; `+
|
||||
`see https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#vmalert"}`)
|
||||
return true
|
||||
}
|
||||
proxyVMAlertRequests(w, r, p.Suffix)
|
||||
path := strings.TrimPrefix(p.Suffix, "prometheus")
|
||||
vmalertproxy.HandleRequest(w, r, path)
|
||||
return true
|
||||
}
|
||||
switch p.Suffix {
|
||||
@@ -777,7 +784,8 @@ func handleStaticAndSimpleRequests(w http.ResponseWriter, r *http.Request, path
|
||||
case "prometheus/api/v1/rules", "prometheus/rules":
|
||||
rulesRequests.Inc()
|
||||
if len(*vmalertProxyURL) > 0 {
|
||||
proxyVMAlertRequests(w, r, p.Suffix)
|
||||
path := strings.TrimPrefix(p.Suffix, "prometheus")
|
||||
vmalertproxy.HandleRequest(w, r, path)
|
||||
return true
|
||||
}
|
||||
// Return dumb placeholder for https://prometheus.io/docs/prometheus/latest/querying/api/#rules
|
||||
@@ -787,7 +795,8 @@ func handleStaticAndSimpleRequests(w http.ResponseWriter, r *http.Request, path
|
||||
case "prometheus/api/v1/alerts", "prometheus/alerts":
|
||||
alertsRequests.Inc()
|
||||
if len(*vmalertProxyURL) > 0 {
|
||||
proxyVMAlertRequests(w, r, p.Suffix)
|
||||
path := strings.TrimPrefix(p.Suffix, "prometheus")
|
||||
vmalertproxy.HandleRequest(w, r, path)
|
||||
return true
|
||||
}
|
||||
// Return dumb placeholder for https://prometheus.io/docs/prometheus/latest/querying/api/#alerts
|
||||
@@ -797,7 +806,8 @@ func handleStaticAndSimpleRequests(w http.ResponseWriter, r *http.Request, path
|
||||
case "prometheus/api/v1/notifiers", "prometheus/notifiers":
|
||||
notifiersRequests.Inc()
|
||||
if len(*vmalertProxyURL) > 0 {
|
||||
proxyVMAlertRequests(w, r, p.Suffix)
|
||||
path := strings.TrimPrefix(p.Suffix, "prometheus")
|
||||
vmalertproxy.HandleRequest(w, r, path)
|
||||
return true
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -992,48 +1002,7 @@ See the docs at https://docs.victoriametrics.com/victoriametrics/cluster-victori
|
||||
flagutil.Usage(s)
|
||||
}
|
||||
|
||||
func proxyVMAlertRequests(w http.ResponseWriter, r *http.Request, path string) {
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err == nil || err == http.ErrAbortHandler {
|
||||
// Suppress http.ErrAbortHandler panic.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1353
|
||||
return
|
||||
}
|
||||
// Forward other panics to the caller.
|
||||
panic(err)
|
||||
}()
|
||||
req := r.Clone(r.Context())
|
||||
req.URL.Path = strings.TrimPrefix(path, "prometheus")
|
||||
req.Host = vmalertProxyHost
|
||||
|
||||
if strings.HasPrefix(r.Header.Get(`User-Agent`), `Grafana`) {
|
||||
// Grafana currently supports only Prometheus-style alerts. If other alert types
|
||||
// (e.g. logs or traces) are returned, it may fail with "Error loading alerts".
|
||||
//
|
||||
// Grafana queries the vmalert API directly, bypassing the VictoriaMetrics datasource,
|
||||
// so query params (such as datasource_type) cannot be enforced on the Grafana side.
|
||||
//
|
||||
// To ensure compatibility, we detect Grafana requests via the User-Agent and enforce
|
||||
// `datasource_type=prometheus`.
|
||||
//
|
||||
// See:
|
||||
// - https://github.com/VictoriaMetrics/victoriametrics-datasource/issues/329#issuecomment-3847585443
|
||||
// - https://github.com/VictoriaMetrics/victoriametrics-datasource/issues/59
|
||||
q := req.URL.Query()
|
||||
q.Set("datasource_type", "prometheus")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
req.RequestURI = ""
|
||||
}
|
||||
|
||||
vmalertProxy.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
var (
|
||||
vmalertProxyHost string
|
||||
vmalertProxy *nethttputil.ReverseProxy
|
||||
vmuiConfig string
|
||||
)
|
||||
var vmuiConfig string
|
||||
|
||||
func initVMUIConfig() {
|
||||
var cfg struct {
|
||||
@@ -1066,22 +1035,6 @@ func initVMUIConfig() {
|
||||
vmuiConfig = string(data)
|
||||
}
|
||||
|
||||
// initVMAlertProxy must be called after flag.Parse(), since it uses command-line flags.
|
||||
func initVMAlertProxy() {
|
||||
|
||||
flagutil.RegisterSecretFlag("vmalert.proxyURL")
|
||||
if len(*vmalertProxyURL) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
proxyURL, err := url.Parse(*vmalertProxyURL)
|
||||
if err != nil {
|
||||
logger.Fatalf("cannot parse -vmalert.proxyURL=%q: %s", *vmalertProxyURL, err)
|
||||
}
|
||||
vmalertProxyHost = proxyURL.Host
|
||||
vmalertProxy = nethttputil.NewSingleHostReverseProxy(proxyURL)
|
||||
}
|
||||
|
||||
func checkDuplicates(arr []string) string {
|
||||
visited := make(map[string]struct{})
|
||||
for _, s := range arr {
|
||||
|
||||
@@ -2171,23 +2171,16 @@ func (snr *storageNodesRequest) collectResults(partialResultsCounter *metrics.Co
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Verify whether at least a single node per each group successfully returned result in order to be able returning partial result.
|
||||
missingGroups := 0
|
||||
var firstErr error
|
||||
for g, errsPartial := range errsPartialPerGroup {
|
||||
if len(errsPartial) == g.nodesCount {
|
||||
missingGroups++
|
||||
if firstErr == nil {
|
||||
// Return only the first error, since it has no sense in returning all errors.
|
||||
firstErr = errsPartial[0]
|
||||
}
|
||||
}
|
||||
if len(errsPartial) > 0 {
|
||||
partialErrorsLogger.Warnf("%d out of %d vmstorage nodes at group %q were unavailable during the query; a sample error: %s", len(errsPartial), len(sns), g.name, errsPartial[0])
|
||||
if firstErr == nil {
|
||||
// Return only the first error, since it has no sense in returning all errors.
|
||||
firstErr = errsPartial[0]
|
||||
}
|
||||
partialErrorsLogger.Warnf("%d out of %d vmstorage nodes at group %q were unavailable during the query; a sample error: %s", len(errsPartial), g.nodesCount, g.name, errsPartial[0])
|
||||
}
|
||||
if missingGroups >= *globalReplicationFactor {
|
||||
// Too many groups contain all the non-working vmstorage nodes.
|
||||
if snr.denyPartialResponse || len(resultsCollectedPerGroup) == 0 {
|
||||
// The response cannot be returned as partial.
|
||||
// Returns 503 status code, so the caller could retry it if needed.
|
||||
err := &httpserver.ErrorWithStatusCode{
|
||||
Err: firstErr,
|
||||
@@ -3626,9 +3619,8 @@ func readMetadataRows(bc *handshake.BufferedConn) ([]*metricsmetadata.Row, error
|
||||
return nil, fmt.Errorf("cannot read the number of metadata records: %w", err)
|
||||
}
|
||||
records := make([]*metricsmetadata.Row, 0, n)
|
||||
var dataBuf []byte
|
||||
for i := range n {
|
||||
dataBuf, err = readBytes(dataBuf[:0], bc, maxLabelValueSize)
|
||||
dataBuf, err := readBytes(nil, bc, maxLabelValueSize)
|
||||
if err != nil {
|
||||
return records, fmt.Errorf("cannot read record metricName: %w", err)
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ func applyFiltersToTenants(tenants []storage.TenantToken, filters [][]storage.Ta
|
||||
filter := tagFiltersToString(tags)
|
||||
err := promIfs[i].Parse(filter)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse if expression from filters %v: %s", filter, err)
|
||||
return nil, fmt.Errorf("cannot parse if expression from filters %v: %w", filter, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -112,6 +112,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, at *auth.Token, w http.ResponseWriter, r *http.Request) error {
|
||||
defer federateDuration.UpdateDuration(startTime)
|
||||
@@ -144,6 +149,21 @@ func FederateHandler(startTime time.Time, at *auth.Token, w http.ResponseWriter,
|
||||
return fmt.Errorf("cannot export federated metrics, because some of vmstorage nodes are unavailable")
|
||||
}
|
||||
|
||||
// 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)
|
||||
@@ -153,7 +173,7 @@ func FederateHandler(startTime time.Time, at *auth.Token, w http.ResponseWriter,
|
||||
return err
|
||||
}
|
||||
bb := sw.getBuffer(workerID)
|
||||
WriteFederate(bb, rs)
|
||||
WriteFederate(bb, rs, escapeScheme)
|
||||
return sw.maybeFlushBuffer(bb)
|
||||
})
|
||||
if err == nil {
|
||||
@@ -544,6 +564,7 @@ func DeleteHandler(startTime time.Time, at *auth.Token, r *http.Request) error {
|
||||
// TODO: reset only cache for (account, project)
|
||||
resetRollupResultCaches()
|
||||
}
|
||||
logger.Infof("/api/v1/admin/tsdb/delete_series has been called for %q. Deleted %d series.", sq.FiltersString(), deletedCount)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2444,8 +2444,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,
|
||||
|
||||
1
app/vmselect/vmui/assets/index-BBUnmLOr.css
Normal file
1
app/vmselect/vmui/assets/index-BBUnmLOr.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -37,11 +37,11 @@
|
||||
<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-CoGukb-x.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">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-BL7jEFBa.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-BBUnmLOr.css">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
@@ -9,12 +9,10 @@ import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage/servers"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/buildinfo"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/envflag"
|
||||
@@ -29,10 +27,12 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/pushmetrics"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/stringsutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/vminsertapi"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/vmselectapi"
|
||||
)
|
||||
|
||||
var (
|
||||
storageDataPath = flag.String("storageDataPath", "vmstorage-data", "Path to storage data")
|
||||
retentionPeriod = flagutil.NewRetentionDuration("retentionPeriod", "1M", "Data with timestamps outside the retentionPeriod is automatically deleted. The minimum retentionPeriod is 24h or 1d. "+
|
||||
"See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#retention. See also -retentionFilter")
|
||||
futureRetention = flagutil.NewRetentionDuration("futureRetention", "2d", "Data with timestamps bigger than now+futureRetention is automatically deleted. "+
|
||||
@@ -41,13 +41,23 @@ var (
|
||||
useProxyProtocol = flagutil.NewArrayBool("httpListenAddr.useProxyProtocol", "Whether to use proxy protocol for connections accepted at the given -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")
|
||||
storageDataPath = flag.String("storageDataPath", "vmstorage-data", "Path to storage data")
|
||||
vminsertAddr = flag.String("vminsertAddr", ":8400", "TCP address to accept connections from vminsert services")
|
||||
vmselectAddr = flag.String("vmselectAddr", ":8401", "TCP address to accept connections from vmselect services")
|
||||
snapshotAuthKey = flagutil.NewPassword("snapshotAuthKey", "authKey, which must be passed in query string to /snapshot* pages")
|
||||
forceMergeAuthKey = flagutil.NewPassword("forceMergeAuthKey", "authKey, which must be passed in query string to /internal/force_merge pages")
|
||||
forceFlushAuthKey = flagutil.NewPassword("forceFlushAuthKey", "authKey, which must be passed in query string to /internal/force_flush pages")
|
||||
snapshotsMaxAge = flagutil.NewRetentionDuration("snapshotsMaxAge", "3d", "Automatically delete snapshots older than -snapshotsMaxAge if it is set to non-zero duration. Make sure that backup process has enough time to finish the backup before the corresponding snapshot is automatically deleted")
|
||||
vminsertAddr = flag.String("vminsertAddr", ":8400", "TCP address to accept connections from vminsert services")
|
||||
vminsertConnsShutdownDuration = flag.Duration("storage.vminsertConnsShutdownDuration", 10*time.Second, "The time needed for gradual closing of vminsert connections during "+
|
||||
"graceful shutdown. Bigger duration reduces spikes in CPU, RAM and disk IO load on the remaining vmstorage nodes during rolling restart. "+
|
||||
"Smaller duration reduces the time needed to close all the vminsert connections, thus reducing the time for graceful shutdown. "+
|
||||
"Configured value must always be lower than the graceful shutdown period configured by the orchestration platform (terminationGracePeriodSeconds for Kubernetes). "+
|
||||
"See https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#improving-re-routing-performance-during-restart")
|
||||
vmselectAddr = flag.String("vmselectAddr", ":8401", "TCP address to accept connections from vmselect services")
|
||||
vmselectMaxConcurrentRequests = flag.Int("search.maxConcurrentRequests", 2*cgroup.AvailableCPUs(), "The maximum number of concurrent vmselect requests "+
|
||||
"the vmstorage can process at -vmselectAddr. It shouldn't be high, since a single request usually saturates a CPU core, and many concurrently executed requests "+
|
||||
"may require high amounts of memory. See also -search.maxQueueDuration")
|
||||
vmselectMaxQueueDuration = flag.Duration("search.maxQueueDuration", 10*time.Second, "The maximum time the incoming vmselect request waits for execution "+
|
||||
"when -search.maxConcurrentRequests limit is reached")
|
||||
vmselectDisableRPCCompression = flag.Bool("rpc.disableCompression", false, "Whether to disable compression of the data sent from vmstorage to vmselect. "+
|
||||
"This reduces CPU usage at the cost of higher network bandwidth usage")
|
||||
snapshotAuthKey = flagutil.NewPassword("snapshotAuthKey", "authKey, which must be passed in query string to /snapshot* pages. It overrides -httpAuth.*")
|
||||
forceMergeAuthKey = flagutil.NewPassword("forceMergeAuthKey", "authKey, which must be passed in query string to /internal/force_merge pages. It overrides -httpAuth.*")
|
||||
forceFlushAuthKey = flagutil.NewPassword("forceFlushAuthKey", "authKey, which must be passed in query string to /internal/force_flush pages. It overrides -httpAuth.*")
|
||||
_ = flag.Duration("snapshotCreateTimeout", 0, "Deprecated: this flag does nothing")
|
||||
|
||||
_ = flag.Duration("finalMergeDelay", 0, "Deprecated: this flag does nothing")
|
||||
@@ -58,7 +68,7 @@ var (
|
||||
"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 https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#deduplication for details")
|
||||
"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). "+
|
||||
@@ -119,7 +129,7 @@ var (
|
||||
)
|
||||
|
||||
func main() {
|
||||
// vmstoage is optimized for reduced memory allocations,
|
||||
// vmstorage is optimized for reduced memory allocations,
|
||||
// so it can run with the reduced GOGC in order to reduce the used memory,
|
||||
// while keeping CPU usage spent in GC at low levels.
|
||||
//
|
||||
@@ -178,7 +188,7 @@ func main() {
|
||||
LogNewSeries: *logNewSeries,
|
||||
}
|
||||
strg := storage.MustOpenStorage(*storageDataPath, opts)
|
||||
initStaleSnapshotsRemover(strg)
|
||||
vmStorage := newVMStorage(strg, *vmselectMaxConcurrentRequests)
|
||||
|
||||
var m storage.Metrics
|
||||
strg.UpdateMetrics(&m)
|
||||
@@ -192,19 +202,23 @@ func main() {
|
||||
|
||||
// register storage metrics
|
||||
storageMetrics := metrics.NewSet()
|
||||
storageMetrics.RegisterMetricsWriter(func(w io.Writer) {
|
||||
writeStorageMetrics(w, strg)
|
||||
})
|
||||
storageMetrics.RegisterMetricsWriter(vmStorage.writeStorageMetrics)
|
||||
metrics.RegisterSet(storageMetrics)
|
||||
|
||||
protoparserutil.StartUnmarshalWorkers()
|
||||
|
||||
servers.GetMaxUniqueTimeSeries() // for init and logging only.
|
||||
vminsertSrv, err := servers.NewVMInsertServer(*vminsertAddr, strg)
|
||||
vminsertSrv, err := vminsertapi.NewServer(*vminsertAddr, *vminsertConnsShutdownDuration, "vminsert", vmStorage, nil)
|
||||
if err != nil {
|
||||
logger.Fatalf("cannot create a server with -vminsertAddr=%s: %s", *vminsertAddr, err)
|
||||
|
||||
}
|
||||
vmselectSrv, err := servers.NewVMSelectServer(*vmselectAddr, strg)
|
||||
limits := vmselectapi.Limits{
|
||||
MaxConcurrentRequests: *vmselectMaxConcurrentRequests,
|
||||
MaxConcurrentRequestsFlagName: "search.maxConcurrentRequests",
|
||||
MaxQueueDuration: *vmselectMaxQueueDuration,
|
||||
MaxQueueDurationFlagName: "search.maxQueueDuration",
|
||||
}
|
||||
vmselectSrv, err := vmselectapi.NewServer(*vmselectAddr, vmStorage, limits, *vmselectDisableRPCCompression)
|
||||
if err != nil {
|
||||
logger.Fatalf("cannot create a server with -vmselectAddr=%s: %s", *vmselectAddr, err)
|
||||
}
|
||||
@@ -213,8 +227,7 @@ func main() {
|
||||
if len(listenAddrs) == 0 {
|
||||
listenAddrs = []string{":8482"}
|
||||
}
|
||||
requestHandler := newRequestHandler(strg)
|
||||
go httpserver.Serve(listenAddrs, requestHandler, httpserver.ServeOptions{UseProxyProtocol: useProxyProtocol})
|
||||
go httpserver.Serve(listenAddrs, vmStorage.requestHandler, httpserver.ServeOptions{UseProxyProtocol: useProxyProtocol})
|
||||
|
||||
pushmetrics.Init()
|
||||
sig := procutil.WaitForSigterm()
|
||||
@@ -234,7 +247,6 @@ func main() {
|
||||
metrics.UnregisterSet(storageMetrics, true)
|
||||
storageMetrics = nil
|
||||
|
||||
stopStaleSnapshotsRemover()
|
||||
vmselectSrv.MustStop()
|
||||
vminsertSrv.MustStop()
|
||||
protoparserutil.StopUnmarshalWorkers()
|
||||
@@ -242,31 +254,29 @@ func main() {
|
||||
|
||||
logger.Infof("gracefully closing the storage at %s", *storageDataPath)
|
||||
startTime = time.Now()
|
||||
strg.MustClose()
|
||||
vmStorage.Stop()
|
||||
logger.Infof("successfully closed the storage in %.3f seconds", time.Since(startTime).Seconds())
|
||||
|
||||
fs.MustStopDirRemover()
|
||||
logger.Infof("the vmstorage has been stopped")
|
||||
}
|
||||
|
||||
func newRequestHandler(strg *storage.Storage) httpserver.RequestHandler {
|
||||
return func(w http.ResponseWriter, r *http.Request) bool {
|
||||
if r.URL.Path == "/" {
|
||||
if r.Method != http.MethodGet {
|
||||
return false
|
||||
}
|
||||
w.Header().Add("Content-Type", "text/html; charset=utf-8")
|
||||
fmt.Fprintf(w, `vmstorage - a component of VictoriaMetrics cluster<br/>
|
||||
// requestHandler is a storage request handler.
|
||||
// TODO(@rtm0): Move to a separate file, request_handler.go
|
||||
func (vms *VMStorage) requestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
path := r.URL.Path
|
||||
|
||||
if path == "/" {
|
||||
if r.Method != http.MethodGet {
|
||||
return false
|
||||
}
|
||||
w.Header().Add("Content-Type", "text/html; charset=utf-8")
|
||||
fmt.Fprintf(w, `vmstorage - a component of VictoriaMetrics cluster<br/>
|
||||
<a href="https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/">docs</a><br>
|
||||
`)
|
||||
return true
|
||||
}
|
||||
return requestHandler(w, r, strg)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func requestHandler(w http.ResponseWriter, r *http.Request, strg *storage.Storage) bool {
|
||||
path := r.URL.Path
|
||||
if path == "/internal/force_merge" {
|
||||
if !httpserver.CheckAuthFlag(w, r, forceMergeAuthKey) {
|
||||
return true
|
||||
@@ -278,7 +288,7 @@ func requestHandler(w http.ResponseWriter, r *http.Request, strg *storage.Storag
|
||||
defer activeForceMerges.Dec()
|
||||
logger.Infof("forced merge for partition_prefix=%q has been started", partitionNamePrefix)
|
||||
startTime := time.Now()
|
||||
if err := strg.ForceMergePartitions(partitionNamePrefix); err != nil {
|
||||
if err := vms.s.ForceMergePartitions(partitionNamePrefix); err != nil {
|
||||
logger.Errorf("error in forced merge for partition_prefix=%q: %s", partitionNamePrefix, err)
|
||||
return
|
||||
}
|
||||
@@ -291,7 +301,7 @@ func requestHandler(w http.ResponseWriter, r *http.Request, strg *storage.Storag
|
||||
return true
|
||||
}
|
||||
logger.Infof("flushing storage to make pending data available for reading")
|
||||
strg.DebugFlush()
|
||||
vms.s.DebugFlush()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -305,13 +315,13 @@ func requestHandler(w http.ResponseWriter, r *http.Request, strg *storage.Storag
|
||||
dealine, err = strconv.Atoi(deadlineStr)
|
||||
if err != nil {
|
||||
logger.Errorf("cannot parse `seconds` arg %q: %s", deadlineStr, err)
|
||||
jsonResponseError(w, fmt.Errorf("cannot parse `seconds` arg %q: %s", deadlineStr, err))
|
||||
jsonResponseError(w, fmt.Errorf("cannot parse `seconds` arg %q: %w", deadlineStr, err))
|
||||
return true
|
||||
}
|
||||
}
|
||||
logger.Infof("enabling logging of new series for the next %s. This may increase resource usage during this period.", time.Duration(dealine)*time.Second)
|
||||
endTime := fasttime.UnixTimestamp() + uint64(dealine)
|
||||
strg.SetLogNewSeriesUntil(endTime)
|
||||
vms.s.SetLogNewSeriesUntil(endTime)
|
||||
fmt.Fprintf(w, `{"status":"success","data":{"logEndTime":%q}}`, time.Unix(int64(endTime), 0))
|
||||
return true
|
||||
}
|
||||
@@ -327,13 +337,13 @@ func requestHandler(w http.ResponseWriter, r *http.Request, strg *storage.Storag
|
||||
case "/create":
|
||||
snapshotsCreateTotal.Inc()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
snapshotName := strg.MustCreateSnapshot()
|
||||
snapshotName := vms.s.MustCreateSnapshot()
|
||||
|
||||
// Verify whether the client already closed the connection.
|
||||
// In this case it is better to drop the created snapshot, since the client isn't interested in it.
|
||||
if err := r.Context().Err(); err != nil {
|
||||
logger.Infof("deleting already created snapshot at %s because the client canceled the request", snapshotName)
|
||||
if err := deleteSnapshot(strg, snapshotName); err != nil {
|
||||
if err := vms.deleteSnapshot(snapshotName); err != nil {
|
||||
logger.Infof("cannot delete just created snapshot: %s", err)
|
||||
return true
|
||||
}
|
||||
@@ -345,7 +355,7 @@ func requestHandler(w http.ResponseWriter, r *http.Request, strg *storage.Storag
|
||||
case "/list":
|
||||
snapshotsListTotal.Inc()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
snapshots := strg.MustListSnapshots()
|
||||
snapshots := vms.s.MustListSnapshots()
|
||||
fmt.Fprintf(w, `{"status":"ok","snapshots":[`)
|
||||
if len(snapshots) > 0 {
|
||||
for _, snapshot := range snapshots[:len(snapshots)-1] {
|
||||
@@ -359,7 +369,7 @@ func requestHandler(w http.ResponseWriter, r *http.Request, strg *storage.Storag
|
||||
snapshotsDeleteTotal.Inc()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
snapshotName := r.FormValue("snapshot")
|
||||
if err := deleteSnapshot(strg, snapshotName); err != nil {
|
||||
if err := vms.deleteSnapshot(snapshotName); err != nil {
|
||||
jsonResponseError(w, err)
|
||||
snapshotsDeleteErrorsTotal.Inc()
|
||||
return true
|
||||
@@ -369,9 +379,9 @@ func requestHandler(w http.ResponseWriter, r *http.Request, strg *storage.Storag
|
||||
case "/delete_all":
|
||||
snapshotsDeleteAllTotal.Inc()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
snapshots := strg.MustListSnapshots()
|
||||
snapshots := vms.s.MustListSnapshots()
|
||||
for _, snapshotName := range snapshots {
|
||||
if err := strg.DeleteSnapshot(snapshotName); err != nil {
|
||||
if err := vms.s.DeleteSnapshot(snapshotName); err != nil {
|
||||
err = fmt.Errorf("cannot delete snapshot %q: %w", snapshotName, err)
|
||||
jsonResponseError(w, err)
|
||||
snapshotsDeleteAllErrorsTotal.Inc()
|
||||
@@ -385,50 +395,6 @@ func requestHandler(w http.ResponseWriter, r *http.Request, strg *storage.Storag
|
||||
}
|
||||
}
|
||||
|
||||
func deleteSnapshot(strg *storage.Storage, snapshotName string) error {
|
||||
snapshots := strg.MustListSnapshots()
|
||||
for _, snName := range snapshots {
|
||||
if snName == snapshotName {
|
||||
if err := strg.DeleteSnapshot(snName); err != nil {
|
||||
return fmt.Errorf("cannot delete snapshot %q: %w", snName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("cannot find snapshot %q", snapshotName)
|
||||
}
|
||||
|
||||
func initStaleSnapshotsRemover(strg *storage.Storage) {
|
||||
staleSnapshotsRemoverCh = make(chan struct{})
|
||||
if snapshotsMaxAge.Duration() <= 0 {
|
||||
return
|
||||
}
|
||||
snapshotsMaxAgeDur := snapshotsMaxAge.Duration()
|
||||
staleSnapshotsRemoverWG.Go(func() {
|
||||
d := timeutil.AddJitterToDuration(time.Second * 11)
|
||||
t := time.NewTicker(d)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-staleSnapshotsRemoverCh:
|
||||
return
|
||||
case <-t.C:
|
||||
}
|
||||
strg.MustDeleteStaleSnapshots(snapshotsMaxAgeDur)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func stopStaleSnapshotsRemover() {
|
||||
close(staleSnapshotsRemoverCh)
|
||||
staleSnapshotsRemoverWG.Wait()
|
||||
}
|
||||
|
||||
var (
|
||||
staleSnapshotsRemoverCh chan struct{}
|
||||
staleSnapshotsRemoverWG sync.WaitGroup
|
||||
)
|
||||
|
||||
var (
|
||||
activeForceMerges = metrics.NewCounter("vm_active_force_merges")
|
||||
|
||||
@@ -443,7 +409,9 @@ var (
|
||||
snapshotsDeleteAllErrorsTotal = metrics.NewCounter(`vm_http_request_errors_total{path="/snapshot/delete_all"}`)
|
||||
)
|
||||
|
||||
func writeStorageMetrics(w io.Writer, strg *storage.Storage) {
|
||||
// TODO(@rtm0): Move to metrics.go.
|
||||
func (vms *VMStorage) writeStorageMetrics(w io.Writer) {
|
||||
strg := vms.s
|
||||
var m storage.Metrics
|
||||
strg.UpdateMetrics(&m)
|
||||
tm := &m.TableMetrics
|
||||
@@ -667,7 +635,7 @@ func writeStorageMetrics(w io.Writer, strg *storage.Storage) {
|
||||
metrics.WriteGaugeUint64(w, `vm_downsampling_partitions_scheduled`, tm.ScheduledDownsamplingPartitions)
|
||||
metrics.WriteGaugeUint64(w, `vm_downsampling_partitions_scheduled_size_bytes`, tm.ScheduledDownsamplingPartitionsSize)
|
||||
|
||||
metrics.WriteGaugeUint64(w, `vm_search_max_unique_timeseries`, uint64(servers.GetMaxUniqueTimeSeries()))
|
||||
metrics.WriteGaugeUint64(w, `vm_search_max_unique_timeseries`, uint64(vms.maxUniqueTimeSeriesCalculated))
|
||||
|
||||
metrics.WriteGaugeUint64(w, `vm_metrics_metadata_storage_items`, m.MetadataStorageItemsCurrent)
|
||||
metrics.WriteCounterUint64(w, `vm_metrics_metadata_storage_size_bytes`, m.MetadataStorageCurrentSizeBytes)
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
package servers
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/encoding"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage/metricsmetadata"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/vminsertapi"
|
||||
)
|
||||
|
||||
var (
|
||||
precisionBits = flag.Int("precisionBits", 64, "The number of precision bits to store per each value. Lower precision bits improves data compression "+
|
||||
"at the cost of precision loss")
|
||||
vminsertConnsShutdownDuration = flag.Duration("storage.vminsertConnsShutdownDuration", 10*time.Second, "The time needed for gradual closing of vminsert connections during "+
|
||||
"graceful shutdown. Bigger duration reduces spikes in CPU, RAM and disk IO load on the remaining vmstorage nodes during rolling restart. "+
|
||||
"Smaller duration reduces the time needed to close all the vminsert connections, thus reducing the time for graceful shutdown. "+
|
||||
"Configured value must always be lower than the graceful shutdown period configured by the orchestration platform (terminationGracePeriodSeconds for Kubernetes). "+
|
||||
"See https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#improving-re-routing-performance-during-restart")
|
||||
)
|
||||
|
||||
// NewVMInsertServer starts vminsertapi.VMInsertServer at the given addr serving the given storage.
|
||||
func NewVMInsertServer(addr string, storage *storage.Storage) (*vminsertapi.VMInsertServer, error) {
|
||||
if err := encoding.CheckPrecisionBits(uint8(*precisionBits)); err != nil {
|
||||
return nil, fmt.Errorf("invalid -precisionBits: %w", err)
|
||||
}
|
||||
api := &vminsertAPI{
|
||||
storage: storage,
|
||||
}
|
||||
|
||||
return vminsertapi.NewVMInsertServer(addr, *vminsertConnsShutdownDuration, "vminsert", api, nil)
|
||||
}
|
||||
|
||||
type vminsertAPI struct {
|
||||
storage *storage.Storage
|
||||
}
|
||||
|
||||
// WriteRows implements lib/vminsertapi.API interface
|
||||
func (v *vminsertAPI) WriteRows(rows []storage.MetricRow) error {
|
||||
v.storage.AddRows(rows, uint8(*precisionBits))
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteMetadata implements lib/vminsertapi.API interface
|
||||
func (v *vminsertAPI) WriteMetadata(rows []metricsmetadata.Row) error {
|
||||
v.storage.AddMetadataRows(rows)
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsReadOnly implements lib/vminsertapi.API interface
|
||||
func (v *vminsertAPI) IsReadOnly() bool {
|
||||
return v.storage.IsReadOnly()
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
package servers
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/memory"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage/metricnamestats"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage/metricsmetadata"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/vmselectapi"
|
||||
)
|
||||
|
||||
var (
|
||||
maxUniqueTimeseries = flag.Int("search.maxUniqueTimeseries", 0, "The maximum number of unique time series, which can be scanned during every query. "+
|
||||
"This allows protecting against heavy queries, which select unexpectedly high number of series. When set to zero, the limit is automatically calculated based on -search.maxConcurrentRequests (inversely proportional) and memory available to the process (proportional). See also -search.max* command-line flags at vmselect")
|
||||
maxTagKeys = flag.Int("search.maxTagKeys", 100e3, "The maximum number of tag keys returned per search. "+
|
||||
"See also -search.maxLabelsAPISeries and -search.maxLabelsAPIDuration")
|
||||
maxTagValues = flag.Int("search.maxTagValues", 100e3, "The maximum number of tag values returned per search. "+
|
||||
"See also -search.maxLabelsAPISeries and -search.maxLabelsAPIDuration")
|
||||
maxTagValueSuffixesPerSearch = flag.Int("search.maxTagValueSuffixesPerSearch", 100e3, "The maximum number of tag value suffixes returned from /metrics/find")
|
||||
maxConcurrentRequests = flag.Int("search.maxConcurrentRequests", 2*cgroup.AvailableCPUs(), "The maximum number of concurrent vmselect requests "+
|
||||
"the vmstorage can process at -vmselectAddr. It shouldn't be high, since a single request usually saturates a CPU core, and many concurrently executed requests "+
|
||||
"may require high amounts of memory. See also -search.maxQueueDuration")
|
||||
maxQueueDuration = flag.Duration("search.maxQueueDuration", 10*time.Second, "The maximum time the incoming vmselect request waits for execution "+
|
||||
"when -search.maxConcurrentRequests limit is reached")
|
||||
|
||||
disableRPCCompression = flag.Bool("rpc.disableCompression", false, "Whether to disable compression of the data sent from vmstorage to vmselect. "+
|
||||
"This reduces CPU usage at the cost of higher network bandwidth usage")
|
||||
)
|
||||
|
||||
var (
|
||||
maxUniqueTimeseriesValue int
|
||||
maxUniqueTimeseriesValueOnce sync.Once
|
||||
)
|
||||
|
||||
// NewVMSelectServer starts new server at the given addr, which serves vmselect requests from the given s.
|
||||
func NewVMSelectServer(addr string, s *storage.Storage) (*vmselectapi.Server, error) {
|
||||
api := &vmstorageAPI{
|
||||
s: s,
|
||||
}
|
||||
limits := vmselectapi.Limits{
|
||||
MaxLabelNames: *maxTagKeys,
|
||||
MaxLabelValues: *maxTagValues,
|
||||
MaxTagValueSuffixes: *maxTagValueSuffixesPerSearch,
|
||||
MaxConcurrentRequests: *maxConcurrentRequests,
|
||||
MaxConcurrentRequestsFlagName: "search.maxConcurrentRequests",
|
||||
MaxQueueDuration: *maxQueueDuration,
|
||||
MaxQueueDurationFlagName: "search.maxQueueDuration",
|
||||
}
|
||||
return vmselectapi.NewServer(addr, api, limits, *disableRPCCompression)
|
||||
}
|
||||
|
||||
// vmstorageAPI impelements vmselectapi.API
|
||||
type vmstorageAPI struct {
|
||||
s *storage.Storage
|
||||
}
|
||||
|
||||
func (api *vmstorageAPI) InitSearch(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline uint64) (vmselectapi.BlockIterator, error) {
|
||||
tr := sq.GetTimeRange()
|
||||
maxMetrics := getMaxMetrics(sq.MaxMetrics)
|
||||
tfss, err := api.setupTfss(qt, sq, tr, maxMetrics, deadline)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(tfss) == 0 {
|
||||
return nil, fmt.Errorf("missing tag filters")
|
||||
}
|
||||
bi := getBlockIterator()
|
||||
bi.sr.Init(qt, api.s, tfss, tr, maxMetrics, deadline)
|
||||
if err := bi.sr.Error(); err != nil {
|
||||
bi.MustClose()
|
||||
return nil, err
|
||||
}
|
||||
return bi, nil
|
||||
}
|
||||
|
||||
func (api *vmstorageAPI) SearchMetricNames(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline uint64) ([]string, error) {
|
||||
tr := sq.GetTimeRange()
|
||||
maxMetrics := sq.MaxMetrics
|
||||
if maxMetrics <= 0 {
|
||||
// fallback to maxUniqueTimeSeries if no limit is provided,
|
||||
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7857
|
||||
maxMetrics = GetMaxUniqueTimeSeries()
|
||||
}
|
||||
tfss, err := api.setupTfss(qt, sq, tr, maxMetrics, deadline)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(tfss) == 0 {
|
||||
return nil, fmt.Errorf("missing tag filters")
|
||||
}
|
||||
return api.s.SearchMetricNames(qt, tfss, tr, maxMetrics, deadline)
|
||||
}
|
||||
|
||||
func (api *vmstorageAPI) LabelValues(qt *querytracer.Tracer, sq *storage.SearchQuery, labelName string, maxLabelValues int, deadline uint64) ([]string, error) {
|
||||
tr := sq.GetTimeRange()
|
||||
maxMetrics := sq.MaxMetrics
|
||||
if maxMetrics <= 0 {
|
||||
// fallback to maxUniqueTimeSeries if no limit is provided,
|
||||
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7857
|
||||
maxMetrics = GetMaxUniqueTimeSeries()
|
||||
}
|
||||
tfss, err := api.setupTfss(qt, sq, tr, maxMetrics, deadline)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return api.s.SearchLabelValues(qt, sq.AccountID, sq.ProjectID, labelName, tfss, tr, maxLabelValues, maxMetrics, deadline)
|
||||
}
|
||||
|
||||
func (api *vmstorageAPI) TagValueSuffixes(qt *querytracer.Tracer, accountID, projectID uint32, tr storage.TimeRange, tagKey, tagValuePrefix string, delimiter byte,
|
||||
maxSuffixes int, deadline uint64) ([]string, error) {
|
||||
suffixes, err := api.s.SearchTagValueSuffixes(qt, accountID, projectID, tr, tagKey, tagValuePrefix, delimiter, maxSuffixes, deadline)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(suffixes) >= maxSuffixes {
|
||||
return nil, fmt.Errorf("more than -search.maxTagValueSuffixesPerSearch=%d suffixes returned; "+
|
||||
"either narrow down the search or increase -search.maxTagValueSuffixesPerSearch command-line flag value", maxSuffixes)
|
||||
}
|
||||
return suffixes, nil
|
||||
}
|
||||
|
||||
func (api *vmstorageAPI) LabelNames(qt *querytracer.Tracer, sq *storage.SearchQuery, maxLabelNames int, deadline uint64) ([]string, error) {
|
||||
tr := sq.GetTimeRange()
|
||||
maxMetrics := sq.MaxMetrics
|
||||
if maxMetrics <= 0 {
|
||||
// fallback to maxUniqueTimeSeries if no limit is provided,
|
||||
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7857
|
||||
maxMetrics = GetMaxUniqueTimeSeries()
|
||||
}
|
||||
tfss, err := api.setupTfss(qt, sq, tr, maxMetrics, deadline)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return api.s.SearchLabelNames(qt, sq.AccountID, sq.ProjectID, tfss, tr, maxLabelNames, maxMetrics, deadline)
|
||||
}
|
||||
|
||||
func (api *vmstorageAPI) SeriesCount(_ *querytracer.Tracer, accountID, projectID uint32, deadline uint64) (uint64, error) {
|
||||
return api.s.GetSeriesCount(accountID, projectID, deadline)
|
||||
}
|
||||
|
||||
func (api *vmstorageAPI) Tenants(qt *querytracer.Tracer, tr storage.TimeRange, deadline uint64) ([]string, error) {
|
||||
return api.s.SearchTenants(qt, tr, deadline)
|
||||
}
|
||||
|
||||
func (api *vmstorageAPI) TSDBStatus(qt *querytracer.Tracer, sq *storage.SearchQuery, focusLabel string, topN int, deadline uint64) (*storage.TSDBStatus, error) {
|
||||
tr := sq.GetTimeRange()
|
||||
maxMetrics := sq.MaxMetrics
|
||||
if maxMetrics <= 0 {
|
||||
// fallback to maxUniqueTimeSeries if no limit is provided,
|
||||
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7857
|
||||
maxMetrics = GetMaxUniqueTimeSeries()
|
||||
}
|
||||
tfss, err := api.setupTfss(qt, sq, tr, maxMetrics, deadline)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
date := uint64(sq.MinTimestamp) / (24 * 3600 * 1000)
|
||||
return api.s.GetTSDBStatus(qt, sq.AccountID, sq.ProjectID, tfss, date, focusLabel, topN, maxMetrics, deadline)
|
||||
}
|
||||
|
||||
func (api *vmstorageAPI) DeleteSeries(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline uint64) (int, error) {
|
||||
tr := sq.GetTimeRange()
|
||||
maxMetrics := sq.MaxMetrics
|
||||
if maxMetrics <= 0 {
|
||||
// fallback to maxUniqueTimeSeries if no limit is provided,
|
||||
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7857
|
||||
maxMetrics = GetMaxUniqueTimeSeries()
|
||||
}
|
||||
tfss, err := api.setupTfss(qt, sq, tr, maxMetrics, deadline)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(tfss) == 0 {
|
||||
return 0, fmt.Errorf("missing tag filters")
|
||||
}
|
||||
return api.s.DeleteSeries(qt, tfss, maxMetrics)
|
||||
}
|
||||
|
||||
func (api *vmstorageAPI) RegisterMetricNames(qt *querytracer.Tracer, mrs []storage.MetricRow, _ uint64) error {
|
||||
api.s.RegisterMetricNames(qt, mrs)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *vmstorageAPI) GetMetricNamesUsageStats(qt *querytracer.Tracer, tt *storage.TenantToken, limit, le int, matchPattern string, _ uint64) (metricnamestats.StatsResult, error) {
|
||||
return api.s.GetMetricNamesStats(qt, tt, limit, le, matchPattern), nil
|
||||
}
|
||||
|
||||
func (api *vmstorageAPI) ResetMetricNamesUsageStats(qt *querytracer.Tracer, _ uint64) error {
|
||||
api.s.ResetMetricNamesStats(qt)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *vmstorageAPI) setupTfss(qt *querytracer.Tracer, sq *storage.SearchQuery, tr storage.TimeRange, maxMetrics int, deadline uint64) ([]*storage.TagFilters, error) {
|
||||
tfss := make([]*storage.TagFilters, 0, len(sq.TagFilterss))
|
||||
accountID := sq.AccountID
|
||||
projectID := sq.ProjectID
|
||||
for _, tagFilters := range sq.TagFilterss {
|
||||
tfs := storage.NewTagFilters(accountID, projectID)
|
||||
for i := range tagFilters {
|
||||
tf := &tagFilters[i]
|
||||
if string(tf.Key) == "__graphite__" {
|
||||
query := tf.Value
|
||||
qtChild := qt.NewChild("searching for series matching __graphite__=%q", query)
|
||||
paths, err := api.s.SearchGraphitePaths(qtChild, accountID, projectID, tr, query, maxMetrics, deadline)
|
||||
qtChild.Donef("found %d series", len(paths))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error when searching for Graphite paths for query %q: %w", query, err)
|
||||
}
|
||||
if len(paths) >= maxMetrics {
|
||||
return nil, fmt.Errorf("more than %d time series match Graphite query %q; "+
|
||||
"either narrow down the query or increase the corresponding -search.max* command-line flag value at vmselect nodes; "+
|
||||
"see https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#resource-usage-limits", maxMetrics, query)
|
||||
}
|
||||
tfs.AddGraphiteQuery(query, paths, tf.IsNegative)
|
||||
continue
|
||||
}
|
||||
if err := tfs.Add(tf.Key, tf.Value, tf.IsNegative, tf.IsRegexp); err != nil {
|
||||
return nil, fmt.Errorf("cannot parse tag filter %s: %w", tf, err)
|
||||
}
|
||||
}
|
||||
tfss = append(tfss, tfs)
|
||||
}
|
||||
return tfss, nil
|
||||
}
|
||||
|
||||
func (api *vmstorageAPI) GetMetadataRecords(qt *querytracer.Tracer, tt *storage.TenantToken, limit int, metricName string, deadline uint64) ([]*metricsmetadata.Row, error) {
|
||||
return api.s.GetMetadataRows(qt, tt, limit, metricName, deadline)
|
||||
}
|
||||
|
||||
// blockIterator implements vmselectapi.BlockIterator
|
||||
type blockIterator struct {
|
||||
sr storage.Search
|
||||
mb storage.MetricBlock
|
||||
}
|
||||
|
||||
var blockIteratorsPool sync.Pool
|
||||
|
||||
func (bi *blockIterator) MustClose() {
|
||||
bi.sr.MustClose()
|
||||
bi.mb.MetricName = nil
|
||||
bi.mb.Block.Reset()
|
||||
blockIteratorsPool.Put(bi)
|
||||
}
|
||||
|
||||
func getBlockIterator() *blockIterator {
|
||||
v := blockIteratorsPool.Get()
|
||||
if v == nil {
|
||||
v = &blockIterator{}
|
||||
}
|
||||
return v.(*blockIterator)
|
||||
}
|
||||
|
||||
func (bi *blockIterator) NextBlock(dst []byte) ([]byte, bool) {
|
||||
if !bi.sr.NextMetricBlock() {
|
||||
return dst, false
|
||||
}
|
||||
mb := bi.mb
|
||||
mb.MetricName = bi.sr.MetricBlockRef.MetricName
|
||||
bi.sr.MetricBlockRef.BlockRef.MustReadBlock(&mb.Block)
|
||||
dst = mb.Marshal(dst[:0])
|
||||
return dst, true
|
||||
}
|
||||
|
||||
func (bi *blockIterator) Error() error {
|
||||
return bi.sr.Error()
|
||||
}
|
||||
|
||||
func getMaxMetrics(searchQueryLimit int) int {
|
||||
if searchQueryLimit <= 0 {
|
||||
return GetMaxUniqueTimeSeries()
|
||||
}
|
||||
// searchQueryLimit cannot exceed `-search.maxUniqueTimeseries`
|
||||
if *maxUniqueTimeseries != 0 && searchQueryLimit > *maxUniqueTimeseries {
|
||||
searchQueryLimit = *maxUniqueTimeseries
|
||||
}
|
||||
return searchQueryLimit
|
||||
}
|
||||
|
||||
// GetMaxUniqueTimeSeries returns `-search.maxUniqueTimeseries` or the auto-calculated value based on available resources.
|
||||
// The calculation is split into calculateMaxUniqueTimeSeriesForResource for unit testing.
|
||||
func GetMaxUniqueTimeSeries() int {
|
||||
maxUniqueTimeseriesValueOnce.Do(func() {
|
||||
maxUniqueTimeseriesValue = *maxUniqueTimeseries
|
||||
if maxUniqueTimeseriesValue <= 0 {
|
||||
maxUniqueTimeseriesValue = calculateMaxUniqueTimeSeriesForResource(*maxConcurrentRequests, memory.Remaining())
|
||||
}
|
||||
})
|
||||
return maxUniqueTimeseriesValue
|
||||
}
|
||||
|
||||
// calculateMaxUniqueTimeSeriesForResource calculate the max metrics limit calculated by available resources.
|
||||
func calculateMaxUniqueTimeSeriesForResource(maxConcurrentRequests, remainingMemory int) int {
|
||||
if maxConcurrentRequests <= 0 {
|
||||
// This line should NOT be reached unless the user has set an incorrect `search.maxConcurrentRequests`.
|
||||
// In such cases, fallback to unlimited.
|
||||
logger.Warnf("limiting -search.maxUniqueTimeseries to %v because -search.maxConcurrentRequests=%d.", 2e9, maxConcurrentRequests)
|
||||
return 2e9
|
||||
}
|
||||
|
||||
// Calculate the max metrics limit for a single request in the worst-case concurrent scenario.
|
||||
// The approximate size of 1 unique series that could occupy in the vmstorage is 200 bytes.
|
||||
mts := remainingMemory / 200 / maxConcurrentRequests
|
||||
logger.Infof("limiting -search.maxUniqueTimeseries to %d according to -search.maxConcurrentRequests=%d and remaining memory=%d bytes. To increase the limit, reduce -search.maxConcurrentRequests or increase memory available to the process.", mts, maxConcurrentRequests, remainingMemory)
|
||||
return mts
|
||||
}
|
||||
391
app/vmstorage/vmstorage.go
Normal file
391
app/vmstorage/vmstorage.go
Normal file
@@ -0,0 +1,391 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/encoding"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/memory"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage/metricnamestats"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage/metricsmetadata"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/vmselectapi"
|
||||
)
|
||||
|
||||
var (
|
||||
precisionBits = flag.Int("precisionBits", 64, "The number of precision bits to store per each value. Lower precision bits improves data compression "+
|
||||
"at the cost of precision loss")
|
||||
maxUniqueTimeseries = flag.Int("search.maxUniqueTimeseries", 0, "The maximum number of unique time series, which can be scanned during every query. "+
|
||||
"This allows protecting against heavy queries, which select unexpectedly high number of series. When set to zero, the limit is automatically calculated based on -search.maxConcurrentRequests (inversely proportional) and memory available to the process (proportional). See also -search.max* command-line flags at vmselect")
|
||||
maxTagKeys = flag.Int("search.maxTagKeys", 100e3, "The maximum number of tag keys returned per search. "+
|
||||
"See also -search.maxLabelsAPISeries and -search.maxLabelsAPIDuration")
|
||||
maxTagValues = flag.Int("search.maxTagValues", 100e3, "The maximum number of tag values returned per search. "+
|
||||
"See also -search.maxLabelsAPISeries and -search.maxLabelsAPIDuration")
|
||||
maxTagValueSuffixesPerSearch = flag.Int("search.maxTagValueSuffixesPerSearch", 100e3, "The maximum number of tag value suffixes returned from /metrics/find")
|
||||
snapshotsMaxAge = flagutil.NewRetentionDuration("snapshotsMaxAge", "3d", "Automatically delete snapshots older than -snapshotsMaxAge if it is set to non-zero duration. Make sure that backup process has enough time to finish the backup before the corresponding snapshot is automatically deleted")
|
||||
)
|
||||
|
||||
// newVMStorage creates a new instance of of VMStorage.
|
||||
//
|
||||
// The created VMStorage instance takes ownership of s.
|
||||
func newVMStorage(s *storage.Storage, vmselectMaxConcurrentRequests int) *VMStorage {
|
||||
if err := encoding.CheckPrecisionBits(uint8(*precisionBits)); err != nil {
|
||||
logger.Fatalf("invalid -precisionBits=%d: %s", *precisionBits, err)
|
||||
}
|
||||
|
||||
maxUniqueTimeseriesCalculated := *maxUniqueTimeseries
|
||||
if maxUniqueTimeseriesCalculated <= 0 {
|
||||
maxUniqueTimeseriesCalculated = calculateMaxUniqueTimeseries(vmselectMaxConcurrentRequests, memory.Remaining())
|
||||
}
|
||||
|
||||
vms := &VMStorage{
|
||||
s: s,
|
||||
maxUniqueTimeseries: *maxUniqueTimeseries,
|
||||
maxUniqueTimeSeriesCalculated: maxUniqueTimeseriesCalculated,
|
||||
staleSnapshotsRemoverCh: make(chan struct{}),
|
||||
}
|
||||
vms.initStaleSnapshotsRemover()
|
||||
return vms
|
||||
}
|
||||
|
||||
// calculateMaxUniqueTimeseries calculates the maxUniqueTimeseries based on the
|
||||
// available system resources.
|
||||
func calculateMaxUniqueTimeseries(maxConcurrentRequests, remainingMemory int) int {
|
||||
if maxConcurrentRequests <= 0 {
|
||||
// This line should NOT be reached unless the user has set an incorrect `search.maxConcurrentRequests`.
|
||||
// In such cases, fallback to unlimited.
|
||||
logger.Warnf("limiting -search.maxUniqueTimeseries to %v because -search.maxConcurrentRequests=%d.", 2e9, maxConcurrentRequests)
|
||||
return 2e9
|
||||
}
|
||||
|
||||
// Calculate the max metrics limit for a single request in the worst-case concurrent scenario.
|
||||
// The approximate size of 1 unique series that could occupy in the vmstorage is 200 bytes.
|
||||
mts := remainingMemory / 200 / maxConcurrentRequests
|
||||
logger.Infof("limiting -search.maxUniqueTimeseries to %d according to -search.maxConcurrentRequests=%d and remaining memory=%d bytes. To increase the limit, reduce -search.maxConcurrentRequests or increase memory available to the process.", mts, maxConcurrentRequests, remainingMemory)
|
||||
return mts
|
||||
}
|
||||
|
||||
// VMStorage impelements vmselectapi.API and vminsertapi.API.
|
||||
type VMStorage struct {
|
||||
s *storage.Storage
|
||||
maxUniqueTimeseries int
|
||||
maxUniqueTimeSeriesCalculated int
|
||||
staleSnapshotsRemoverCh chan struct{}
|
||||
staleSnapshotsRemoverWG sync.WaitGroup
|
||||
}
|
||||
|
||||
func (vms *VMStorage) initStaleSnapshotsRemover() {
|
||||
if snapshotsMaxAge.Duration() <= 0 {
|
||||
return
|
||||
}
|
||||
snapshotsMaxAgeDuration := snapshotsMaxAge.Duration()
|
||||
vms.staleSnapshotsRemoverWG.Go(func() {
|
||||
d := timeutil.AddJitterToDuration(time.Second * 11)
|
||||
t := time.NewTicker(d)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-vms.staleSnapshotsRemoverCh:
|
||||
return
|
||||
case <-t.C:
|
||||
}
|
||||
vms.s.MustDeleteStaleSnapshots(snapshotsMaxAgeDuration)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (vms *VMStorage) Stop() {
|
||||
close(vms.staleSnapshotsRemoverCh)
|
||||
vms.staleSnapshotsRemoverWG.Wait()
|
||||
vms.s.MustClose()
|
||||
}
|
||||
|
||||
// WriteRows writes metric rows to the storage.
|
||||
//
|
||||
// The caller should limit the number of concurrent calls to WriteRows() in
|
||||
// order to limit memory usage.
|
||||
func (vms *VMStorage) WriteRows(rows []storage.MetricRow) error {
|
||||
vms.s.AddRows(rows, uint8(*precisionBits))
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteMetadata writes metrics metadata to storage.
|
||||
//
|
||||
// The caller should limit the number of concurrent calls to WriteMetadata() in
|
||||
// order to limit memory usage.
|
||||
func (vms *VMStorage) WriteMetadata(rows []metricsmetadata.Row) error {
|
||||
vms.s.AddMetadataRows(rows)
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsReadOnly returns true is the storage is in read-only mode.
|
||||
func (vms *VMStorage) IsReadOnly() bool {
|
||||
return vms.s.IsReadOnly()
|
||||
}
|
||||
|
||||
func (vms *VMStorage) InitSearch(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline uint64) (vmselectapi.BlockIterator, error) {
|
||||
tr := sq.GetTimeRange()
|
||||
maxMetrics := vms.getMaxMetrics(sq.MaxMetrics)
|
||||
tfss, err := vms.setupTfss(qt, sq, tr, maxMetrics, deadline)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(tfss) == 0 {
|
||||
return nil, fmt.Errorf("missing tag filters")
|
||||
}
|
||||
bi := getBlockIterator()
|
||||
bi.sr.Init(qt, vms.s, tfss, tr, maxMetrics, deadline)
|
||||
if err := bi.sr.Error(); err != nil {
|
||||
bi.MustClose()
|
||||
return nil, err
|
||||
}
|
||||
return bi, nil
|
||||
}
|
||||
|
||||
func (vms *VMStorage) getMaxMetrics(searchQueryLimit int) int {
|
||||
if searchQueryLimit <= 0 {
|
||||
return vms.maxUniqueTimeSeriesCalculated
|
||||
}
|
||||
// searchQueryLimit cannot exceed `-search.maxUniqueTimeseries`
|
||||
if vms.maxUniqueTimeseries != 0 && searchQueryLimit > vms.maxUniqueTimeseries {
|
||||
searchQueryLimit = vms.maxUniqueTimeseries
|
||||
}
|
||||
return searchQueryLimit
|
||||
}
|
||||
|
||||
// blockIterator implements vmselectapi.BlockIterator
|
||||
type blockIterator struct {
|
||||
sr storage.Search
|
||||
mb storage.MetricBlock
|
||||
}
|
||||
|
||||
var blockIteratorsPool sync.Pool
|
||||
|
||||
func (bi *blockIterator) MustClose() {
|
||||
bi.sr.MustClose()
|
||||
bi.mb.MetricName = nil
|
||||
bi.mb.Block.Reset()
|
||||
blockIteratorsPool.Put(bi)
|
||||
}
|
||||
|
||||
func getBlockIterator() *blockIterator {
|
||||
v := blockIteratorsPool.Get()
|
||||
if v == nil {
|
||||
v = &blockIterator{}
|
||||
}
|
||||
return v.(*blockIterator)
|
||||
}
|
||||
|
||||
func (bi *blockIterator) NextBlock(dst []byte) ([]byte, bool) {
|
||||
if !bi.sr.NextMetricBlock() {
|
||||
return dst, false
|
||||
}
|
||||
mb := bi.mb
|
||||
mb.MetricName = bi.sr.MetricBlockRef.MetricName
|
||||
bi.sr.MetricBlockRef.BlockRef.MustReadBlock(&mb.Block)
|
||||
dst = mb.Marshal(dst[:0])
|
||||
return dst, true
|
||||
}
|
||||
|
||||
func (bi *blockIterator) Error() error {
|
||||
return bi.sr.Error()
|
||||
}
|
||||
|
||||
// SearchMetricNames returns metric names for the given tfss on the given tr.
|
||||
func (vms *VMStorage) SearchMetricNames(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline uint64) ([]string, error) {
|
||||
tr := sq.GetTimeRange()
|
||||
maxMetrics := sq.MaxMetrics
|
||||
if maxMetrics <= 0 {
|
||||
// fallback to maxUniqueTimeSeries if no limit is provided,
|
||||
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7857
|
||||
maxMetrics = vms.maxUniqueTimeSeriesCalculated
|
||||
}
|
||||
tfss, err := vms.setupTfss(qt, sq, tr, maxMetrics, deadline)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(tfss) == 0 {
|
||||
return nil, fmt.Errorf("missing tag filters")
|
||||
}
|
||||
return vms.s.SearchMetricNames(qt, tfss, tr, maxMetrics, deadline)
|
||||
}
|
||||
|
||||
// SearchLabelValues searches for label values for the given labelName, tfss and
|
||||
// tr.
|
||||
func (vms *VMStorage) LabelValues(qt *querytracer.Tracer, sq *storage.SearchQuery, labelName string, maxLabelValues int, deadline uint64) ([]string, error) {
|
||||
tr := sq.GetTimeRange()
|
||||
if maxLabelValues <= 0 || maxLabelValues > *maxTagValues {
|
||||
maxLabelValues = *maxTagValues
|
||||
}
|
||||
maxMetrics := sq.MaxMetrics
|
||||
if maxMetrics <= 0 {
|
||||
// fallback to maxUniqueTimeSeries if no limit is provided,
|
||||
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7857
|
||||
maxMetrics = vms.maxUniqueTimeSeriesCalculated
|
||||
}
|
||||
tfss, err := vms.setupTfss(qt, sq, tr, maxMetrics, deadline)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return vms.s.SearchLabelValues(qt, sq.AccountID, sq.ProjectID, labelName, tfss, tr, maxLabelValues, maxMetrics, deadline)
|
||||
}
|
||||
|
||||
// TagValueSuffixes returns all the tag value suffixes for the given tagKey and
|
||||
// tagValuePrefix on the given tr.
|
||||
//
|
||||
// This allows implementing
|
||||
// https://graphite-api.readthedocs.io/en/latest/api.html#metrics-find or
|
||||
// similar APIs.
|
||||
func (vms *VMStorage) TagValueSuffixes(qt *querytracer.Tracer, accountID, projectID uint32, tr storage.TimeRange, tagKey, tagValuePrefix string, delimiter byte,
|
||||
maxSuffixes int, deadline uint64) ([]string, error) {
|
||||
if maxSuffixes <= 0 || maxSuffixes > *maxTagValueSuffixesPerSearch {
|
||||
maxSuffixes = *maxTagValueSuffixesPerSearch
|
||||
}
|
||||
suffixes, err := vms.s.SearchTagValueSuffixes(qt, accountID, projectID, tr, tagKey, tagValuePrefix, delimiter, maxSuffixes, deadline)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(suffixes) >= maxSuffixes {
|
||||
return nil, fmt.Errorf("more than -search.maxTagValueSuffixesPerSearch=%d suffixes returned; "+
|
||||
"either narrow down the search or increase -search.maxTagValueSuffixesPerSearch command-line flag value", maxSuffixes)
|
||||
}
|
||||
return suffixes, nil
|
||||
}
|
||||
|
||||
// SearchLabelNames searches for tag keys matching the given tfss on tr.
|
||||
func (vms *VMStorage) LabelNames(qt *querytracer.Tracer, sq *storage.SearchQuery, maxLabelNames int, deadline uint64) ([]string, error) {
|
||||
tr := sq.GetTimeRange()
|
||||
if maxLabelNames <= 0 || maxLabelNames > *maxTagKeys {
|
||||
maxLabelNames = *maxTagKeys
|
||||
}
|
||||
maxMetrics := sq.MaxMetrics
|
||||
if maxMetrics <= 0 {
|
||||
// fallback to maxUniqueTimeSeries if no limit is provided,
|
||||
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7857
|
||||
maxMetrics = vms.maxUniqueTimeSeriesCalculated
|
||||
}
|
||||
tfss, err := vms.setupTfss(qt, sq, tr, maxMetrics, deadline)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return vms.s.SearchLabelNames(qt, sq.AccountID, sq.ProjectID, tfss, tr, maxLabelNames, maxMetrics, deadline)
|
||||
}
|
||||
|
||||
func (vms *VMStorage) SeriesCount(_ *querytracer.Tracer, accountID, projectID uint32, deadline uint64) (uint64, error) {
|
||||
return vms.s.GetSeriesCount(accountID, projectID, deadline)
|
||||
}
|
||||
|
||||
func (vms *VMStorage) Tenants(qt *querytracer.Tracer, tr storage.TimeRange, deadline uint64) ([]string, error) {
|
||||
return vms.s.SearchTenants(qt, tr, deadline)
|
||||
}
|
||||
|
||||
// GetTSDBStatus returns TSDB status for given filters on the given date.
|
||||
func (vms *VMStorage) TSDBStatus(qt *querytracer.Tracer, sq *storage.SearchQuery, focusLabel string, topN int, deadline uint64) (*storage.TSDBStatus, error) {
|
||||
tr := sq.GetTimeRange()
|
||||
maxMetrics := sq.MaxMetrics
|
||||
if maxMetrics <= 0 {
|
||||
// fallback to maxUniqueTimeSeries if no limit is provided,
|
||||
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7857
|
||||
maxMetrics = vms.maxUniqueTimeSeriesCalculated
|
||||
}
|
||||
tfss, err := vms.setupTfss(qt, sq, tr, maxMetrics, deadline)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
date := uint64(sq.MinTimestamp) / (24 * 3600 * 1000)
|
||||
return vms.s.GetTSDBStatus(qt, sq.AccountID, sq.ProjectID, tfss, date, focusLabel, topN, maxMetrics, deadline)
|
||||
}
|
||||
|
||||
// DeleteSeries deletes series matching tfss.
|
||||
//
|
||||
// Returns the number of deleted series.
|
||||
func (vms *VMStorage) DeleteSeries(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline uint64) (int, error) {
|
||||
tr := sq.GetTimeRange()
|
||||
maxMetrics := sq.MaxMetrics
|
||||
if maxMetrics <= 0 {
|
||||
// fallback to maxUniqueTimeSeries if no limit is provided,
|
||||
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7857
|
||||
maxMetrics = vms.maxUniqueTimeSeriesCalculated
|
||||
}
|
||||
tfss, err := vms.setupTfss(qt, sq, tr, maxMetrics, deadline)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(tfss) == 0 {
|
||||
return 0, fmt.Errorf("missing tag filters")
|
||||
}
|
||||
return vms.s.DeleteSeries(qt, tfss, maxMetrics)
|
||||
}
|
||||
|
||||
func (vms *VMStorage) RegisterMetricNames(qt *querytracer.Tracer, mrs []storage.MetricRow, _ uint64) error {
|
||||
vms.s.RegisterMetricNames(qt, mrs)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMetricNamesUsageStats returns metric name usage stats.
|
||||
func (vms *VMStorage) GetMetricNamesUsageStats(qt *querytracer.Tracer, tt *storage.TenantToken, limit, le int, matchPattern string, _ uint64) (metricnamestats.StatsResult, error) {
|
||||
return vms.s.GetMetricNamesStats(qt, tt, limit, le, matchPattern), nil
|
||||
}
|
||||
|
||||
// ResetMetricNamesStats resets state for metric names usage tracker
|
||||
func (vms *VMStorage) ResetMetricNamesUsageStats(qt *querytracer.Tracer, _ uint64) error {
|
||||
vms.s.ResetMetricNamesStats(qt)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (vms *VMStorage) setupTfss(qt *querytracer.Tracer, sq *storage.SearchQuery, tr storage.TimeRange, maxMetrics int, deadline uint64) ([]*storage.TagFilters, error) {
|
||||
tfss := make([]*storage.TagFilters, 0, len(sq.TagFilterss))
|
||||
accountID := sq.AccountID
|
||||
projectID := sq.ProjectID
|
||||
for _, tagFilters := range sq.TagFilterss {
|
||||
tfs := storage.NewTagFilters(accountID, projectID)
|
||||
for i := range tagFilters {
|
||||
tf := &tagFilters[i]
|
||||
if string(tf.Key) == "__graphite__" {
|
||||
query := tf.Value
|
||||
qtChild := qt.NewChild("searching for series matching __graphite__=%q", query)
|
||||
paths, err := vms.s.SearchGraphitePaths(qtChild, accountID, projectID, tr, query, maxMetrics, deadline)
|
||||
qtChild.Donef("found %d series", len(paths))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error when searching for Graphite paths for query %q: %w", query, err)
|
||||
}
|
||||
if len(paths) >= maxMetrics {
|
||||
return nil, fmt.Errorf("more than %d time series match Graphite query %q; "+
|
||||
"either narrow down the query or increase the corresponding -search.max* command-line flag value at vmselect nodes; "+
|
||||
"see https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#resource-usage-limits", maxMetrics, query)
|
||||
}
|
||||
tfs.AddGraphiteQuery(query, paths, tf.IsNegative)
|
||||
continue
|
||||
}
|
||||
if err := tfs.Add(tf.Key, tf.Value, tf.IsNegative, tf.IsRegexp); err != nil {
|
||||
return nil, fmt.Errorf("cannot parse tag filter %s: %w", tf, err)
|
||||
}
|
||||
}
|
||||
tfss = append(tfss, tfs)
|
||||
}
|
||||
return tfss, nil
|
||||
}
|
||||
|
||||
func (vms *VMStorage) GetMetadataRecords(qt *querytracer.Tracer, tt *storage.TenantToken, limit int, metricName string, deadline uint64) ([]*metricsmetadata.Row, error) {
|
||||
return vms.s.GetMetadataRows(qt, tt, limit, metricName, deadline)
|
||||
}
|
||||
|
||||
// deleteSnapshot deletes a snapshot by its name.
|
||||
func (vms *VMStorage) deleteSnapshot(snapshotName string) error {
|
||||
snapshots := vms.s.MustListSnapshots()
|
||||
for _, snName := range snapshots {
|
||||
if snName == snapshotName {
|
||||
if err := vms.s.DeleteSnapshot(snName); err != nil {
|
||||
return fmt.Errorf("cannot delete snapshot %q: %w", snName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("cannot find snapshot %q", snapshotName)
|
||||
}
|
||||
@@ -1,22 +1,26 @@
|
||||
package servers
|
||||
package main
|
||||
|
||||
import (
|
||||
"math"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||
)
|
||||
|
||||
func TestCalculateMaxMetricsLimitByResource(t *testing.T) {
|
||||
f := func(maxConcurrentRequest, remainingMemory, expect int) {
|
||||
t.Helper()
|
||||
maxMetricsLimit := calculateMaxUniqueTimeSeriesForResource(maxConcurrentRequest, remainingMemory)
|
||||
maxMetricsLimit := calculateMaxUniqueTimeseries(maxConcurrentRequest, remainingMemory)
|
||||
if maxMetricsLimit != expect {
|
||||
t.Fatalf("unexpected max metrics limit: got %d, want %d", maxMetricsLimit, expect)
|
||||
}
|
||||
}
|
||||
|
||||
// Skip when GOARCH=386
|
||||
if runtime.GOARCH != "386" {
|
||||
// 64-bit architectures support memory sizes > 4GB.
|
||||
if strconv.IntSize == 64 {
|
||||
// 8 CPU & 32 GiB
|
||||
f(16, int(math.Round(32*1024*1024*1024*0.4)), 4294967)
|
||||
// 4 CPU & 32 GiB
|
||||
@@ -36,11 +40,17 @@ func TestGetMaxMetrics(t *testing.T) {
|
||||
originalMaxUniqueTimeSeries := *maxUniqueTimeseries
|
||||
defer func() {
|
||||
*maxUniqueTimeseries = originalMaxUniqueTimeSeries
|
||||
fs.MustRemoveDir(t.Name())
|
||||
}()
|
||||
|
||||
maxConcurrentRequests := 2 * cgroup.AvailableCPUs()
|
||||
f := func(searchQueryLimit, storageMaxUniqueTimeseries, expect int) {
|
||||
t.Helper()
|
||||
*maxUniqueTimeseries = storageMaxUniqueTimeseries
|
||||
maxMetrics := getMaxMetrics(searchQueryLimit)
|
||||
s := storage.MustOpenStorage(t.Name(), storage.OpenOptions{})
|
||||
vms := newVMStorage(s, maxConcurrentRequests)
|
||||
defer vms.Stop()
|
||||
maxMetrics := vms.getMaxMetrics(searchQueryLimit)
|
||||
if maxMetrics != expect {
|
||||
t.Fatalf("unexpected max metrics: got %d, want %d", maxMetrics, expect)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.26.3 AS build-web-stage
|
||||
FROM golang:1.26.4 AS build-web-stage
|
||||
COPY build /build
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
1511
app/vmui/packages/vmui/package-lock.json
generated
1511
app/vmui/packages/vmui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -21,16 +21,16 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"classnames": "^2.5.1",
|
||||
"dayjs": "^1.11.20",
|
||||
"dayjs": "^1.11.21",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"marked": "^18.0.2",
|
||||
"preact": "^10.29.1",
|
||||
"qs": "^6.15.1",
|
||||
"marked": "^18.0.5",
|
||||
"preact": "^10.29.2",
|
||||
"qs": "^6.15.2",
|
||||
"react-input-mask": "^2.0.4",
|
||||
"react-router-dom": "^7.14.1",
|
||||
"react-router-dom": "^7.17.0",
|
||||
"uplot": "^1.6.32",
|
||||
"vite": "^8.0.8",
|
||||
"web-vitals": "^5.2.0"
|
||||
"vite": "^8.0.16",
|
||||
"web-vitals": "^5.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.5",
|
||||
@@ -39,24 +39,24 @@
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/preact": "^3.2.4",
|
||||
"@types/lodash.debounce": "^4.0.9",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/qs": "^6.15.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/node": "^25.9.2",
|
||||
"@types/qs": "^6.15.1",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-input-mask": "^3.0.6",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.58.2",
|
||||
"@typescript-eslint/parser": "^8.58.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.61.0",
|
||||
"@typescript-eslint/parser": "^8.61.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-unused-imports": "^4.4.1",
|
||||
"globals": "^17.5.0",
|
||||
"http-proxy-middleware": "^3.0.5",
|
||||
"jsdom": "^29.0.2",
|
||||
"postcss": "^8.5.10",
|
||||
"sass-embedded": "^1.99.0",
|
||||
"typescript": "^6.0.2",
|
||||
"vitest": "^4.1.4"
|
||||
"globals": "^17.6.0",
|
||||
"http-proxy-middleware": "^4.1.0",
|
||||
"jsdom": "^29.1.1",
|
||||
"postcss": "^8.5.15",
|
||||
"sass-embedded": "^1.100.0",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^4.1.8"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
@@ -5,7 +5,7 @@ import uPlot from "uplot";
|
||||
import Button from "../../Main/Button/Button";
|
||||
import { CloseIcon, DragIcon } from "../../Main/Icons";
|
||||
import { SeriesItemStatsFormatted } from "../../../types";
|
||||
import { STATS_ORDER } from "../../../constants/graph";
|
||||
import { STATS_ORDER_TOOLTIP } from "../../../constants/graph";
|
||||
|
||||
export interface ChartTooltipProps {
|
||||
u?: uPlot;
|
||||
@@ -164,7 +164,7 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
||||
</div>
|
||||
{statsFormatted && (
|
||||
<table className="vm-chart-tooltip-stats">
|
||||
{STATS_ORDER.map((key, i) => (
|
||||
{STATS_ORDER_TOOLTIP.map((key, i) => (
|
||||
<div
|
||||
className="vm-chart-tooltip-stats-row"
|
||||
key={i}
|
||||
|
||||
@@ -61,7 +61,7 @@ const LegendConfigs: FC<Props> = ({ data, isCompact }) => {
|
||||
label: "Hide Statistics",
|
||||
value: hideStats,
|
||||
onChange: onChangeStats,
|
||||
info: "If enabled, hides the display of min, median, and max values.",
|
||||
info: "If enabled, hides the display of min, median, max, and last values.",
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import "./style.scss";
|
||||
import classNames from "classnames";
|
||||
import { getFreeFields } from "./helpers";
|
||||
import useCopyToClipboard from "../../../../../hooks/useCopyToClipboard";
|
||||
import { STATS_ORDER } from "../../../../../constants/graph";
|
||||
import { STATS_ORDER_LEGEND } from "../../../../../constants/graph";
|
||||
import { useShowStats } from "../hooks/useShowStats";
|
||||
import { useLegendFormat } from "../hooks/useLegendFormat";
|
||||
import { getLabelAlias } from "../../../../../utils/metric";
|
||||
@@ -80,7 +80,7 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange, duplicateFields })
|
||||
</div>
|
||||
{!hideStats && showStats && (
|
||||
<div className="vm-legend-item-stats">
|
||||
{STATS_ORDER.map((key, i) => (
|
||||
{STATS_ORDER_LEGEND.map((key, i) => (
|
||||
<div
|
||||
className="vm-legend-item-stats-row"
|
||||
key={i}
|
||||
|
||||
@@ -4,11 +4,11 @@ import "./style.scss";
|
||||
import { LegendItemType } from "../../../../../types";
|
||||
import { MouseEvent } from "react";
|
||||
import classNames from "classnames";
|
||||
import { STATS_ORDER } from "../../../../../constants/graph";
|
||||
import { STATS_ORDER_LEGEND } from "../../../../../constants/graph";
|
||||
import { useShowStats } from "../hooks/useShowStats";
|
||||
import { getValueByPath } from "../../../../../utils/object";
|
||||
|
||||
const statsColumns = STATS_ORDER.map(k => ({
|
||||
const statsColumns = STATS_ORDER_LEGEND.map(k => ({
|
||||
key: `statsFormatted.${k}`,
|
||||
title: k
|
||||
}));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,4 +26,5 @@ export const GRAPH_SIZES: GraphSize[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const STATS_ORDER: (keyof SeriesItemStatsFormatted)[] = ["min", "median", "max"];
|
||||
export const STATS_ORDER_LEGEND: (keyof SeriesItemStatsFormatted)[] = ["min", "median", "max", "last"];
|
||||
export const STATS_ORDER_TOOLTIP: (keyof SeriesItemStatsFormatted)[] = ["min", "median", "max"];
|
||||
|
||||
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
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface SeriesItemStatsFormatted {
|
||||
min: string,
|
||||
max: string,
|
||||
median: string,
|
||||
last: string,
|
||||
}
|
||||
|
||||
export interface SeriesItem extends Series {
|
||||
|
||||
@@ -7,6 +7,7 @@ export const ALL_STORAGE_KEYS = [
|
||||
"SERIES_LIMITS",
|
||||
"LEGEND_AUTO_COLLAPSE",
|
||||
"TABLE_COMPACT",
|
||||
"REDUCE_MEM_USAGE",
|
||||
"TIMEZONE",
|
||||
"DISABLED_DEFAULT_TIMEZONE",
|
||||
"THEME",
|
||||
|
||||
@@ -53,6 +53,7 @@ const getSeriesStatistics = (d: MetricResult) => {
|
||||
min: formatPrettyNumber(min, min, max),
|
||||
max: formatPrettyNumber(max, min, max),
|
||||
median: formatPrettyNumber(median, min, max),
|
||||
last: formatPrettyNumber(values.at(-1), min, max),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ var (
|
||||
vminsertAddrRE = regexp.MustCompile(`accepting vminsert conns at (.*:\d{1,5})$`)
|
||||
vminsertClusterNativeAddrRE = regexp.MustCompile(`started TCP clusternative server at "(.*:\d{1,5})"`)
|
||||
vmselectAddrRE = regexp.MustCompile(`accepting vmselect conns at (.*:\d{1,5})$`)
|
||||
vmauthHttpListenAddrRE = regexp.MustCompile(`pprof handlers are exposed at http://(.*:\d{1,5})/debug/pprof/`)
|
||||
)
|
||||
|
||||
// app represents an instance of some VictoriaMetrics server (such as vmstorage,
|
||||
|
||||
@@ -156,14 +156,14 @@ func readAllAndClose(t *testing.T, responseBody io.ReadCloser) string {
|
||||
//
|
||||
// This type is expected to be embedded by the apps that serve metrics.
|
||||
type metricsClient struct {
|
||||
metricsCli *Client
|
||||
url string
|
||||
cli *Client
|
||||
url string
|
||||
}
|
||||
|
||||
func newMetricsClient(cli *Client, addr string) *metricsClient {
|
||||
return &metricsClient{
|
||||
metricsCli: cli,
|
||||
url: fmt.Sprintf("http://%s/metrics", addr),
|
||||
cli: cli,
|
||||
url: fmt.Sprintf("http://%s/metrics", addr),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ func (c *metricsClient) GetIntMetric(t *testing.T, metricName string) int {
|
||||
func (c *metricsClient) GetMetric(t *testing.T, metricName string) float64 {
|
||||
t.Helper()
|
||||
|
||||
metrics, statusCode := c.metricsCli.Get(t, c.url, nil)
|
||||
metrics, statusCode := c.cli.Get(t, c.url, nil)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusOK)
|
||||
}
|
||||
@@ -205,7 +205,7 @@ func (c *metricsClient) GetMetricsByPrefix(t *testing.T, prefix string) []float6
|
||||
|
||||
values := []float64{}
|
||||
|
||||
metrics, statusCode := c.metricsCli.Get(t, c.url, nil)
|
||||
metrics, statusCode := c.cli.Get(t, c.url, nil)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusOK)
|
||||
}
|
||||
@@ -234,7 +234,7 @@ func (c *metricsClient) GetMetricsByRegexp(t *testing.T, re *regexp.Regexp) []fl
|
||||
|
||||
values := []float64{}
|
||||
|
||||
metrics, statusCode := c.metricsCli.Get(t, c.url, nil)
|
||||
metrics, statusCode := c.cli.Get(t, c.url, nil)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusOK)
|
||||
}
|
||||
@@ -270,7 +270,7 @@ func (c *metricsClient) rpcRowsSentTotal(t *testing.T) int {
|
||||
}
|
||||
|
||||
type vmselectClient struct {
|
||||
vmselectCli *Client
|
||||
cli *Client
|
||||
url func(op, path string, opts QueryOpts) string
|
||||
metricNamesStatsResetURL string
|
||||
tenantsURL string
|
||||
@@ -287,7 +287,7 @@ func (c *vmselectClient) PrometheusAPIV1Export(t *testing.T, query string, opts
|
||||
values := opts.asURLValues()
|
||||
values.Add("match[]", query)
|
||||
values.Add("format", "promapi")
|
||||
res, _ := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
res, _ := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
return NewPrometheusAPIV1QueryResponse(t, res)
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ func (c *vmselectClient) PrometheusAPIV1ExportNative(t *testing.T, query string,
|
||||
values := opts.asURLValues()
|
||||
values.Add("match[]", query)
|
||||
values.Add("format", "promapi")
|
||||
res, _ := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
res, _ := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
return []byte(res)
|
||||
}
|
||||
|
||||
@@ -315,7 +315,7 @@ func (c *vmselectClient) PrometheusAPIV1Query(t *testing.T, query string, opts Q
|
||||
url := c.url("select", "prometheus/api/v1/query", opts)
|
||||
values := opts.asURLValues()
|
||||
values.Add("query", query)
|
||||
res, _ := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
res, _ := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
return NewPrometheusAPIV1QueryResponse(t, res)
|
||||
}
|
||||
|
||||
@@ -329,7 +329,7 @@ func (c *vmselectClient) PrometheusAPIV1QueryRange(t *testing.T, query string, o
|
||||
url := c.url("select", "prometheus/api/v1/query_range", opts)
|
||||
values := opts.asURLValues()
|
||||
values.Add("query", query)
|
||||
res, _ := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
res, _ := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
return NewPrometheusAPIV1QueryResponse(t, res)
|
||||
}
|
||||
|
||||
@@ -342,7 +342,7 @@ func (c *vmselectClient) PrometheusAPIV1Series(t *testing.T, matchQuery string,
|
||||
url := c.url("select", "prometheus/api/v1/series", opts)
|
||||
values := opts.asURLValues()
|
||||
values.Add("match[]", matchQuery)
|
||||
res, _ := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
res, _ := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
return NewPrometheusAPIV1SeriesResponse(t, res)
|
||||
}
|
||||
|
||||
@@ -354,7 +354,7 @@ func (c *vmselectClient) PrometheusAPIV1SeriesCount(t *testing.T, opts QueryOpts
|
||||
t.Helper()
|
||||
url := c.url("select", "prometheus/api/v1/series/count", opts)
|
||||
values := opts.asURLValues()
|
||||
res, _ := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
res, _ := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
return NewPrometheusAPIV1SeriesCountResponse(t, res)
|
||||
}
|
||||
|
||||
@@ -367,7 +367,7 @@ func (c *vmselectClient) PrometheusAPIV1Labels(t *testing.T, matchQuery string,
|
||||
url := c.url("select", "prometheus/api/v1/labels", opts)
|
||||
values := opts.asURLValues()
|
||||
values.Add("match[]", matchQuery)
|
||||
res, _ := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
res, _ := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
return NewPrometheusAPIV1LabelsResponse(t, res)
|
||||
}
|
||||
|
||||
@@ -382,7 +382,7 @@ func (c *vmselectClient) PrometheusAPIV1LabelValues(t *testing.T, labelName, mat
|
||||
url := c.url("select", path, opts)
|
||||
values := opts.asURLValues()
|
||||
values.Add("match[]", matchQuery)
|
||||
res, _ := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
res, _ := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
return NewPrometheusAPIV1LabelValuesResponse(t, res)
|
||||
}
|
||||
|
||||
@@ -394,7 +394,7 @@ func (c *vmselectClient) PrometheusAPIV1Metadata(t *testing.T, metric string, li
|
||||
values := opts.asURLValues()
|
||||
values.Add("metric", metric)
|
||||
values.Add("limit", strconv.Itoa(limit))
|
||||
res, _ := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
res, _ := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
return NewPrometheusAPIV1Metadata(t, res)
|
||||
}
|
||||
|
||||
@@ -408,7 +408,7 @@ func (c *vmselectClient) PrometheusAPIV1AdminTSDBDeleteSeries(t *testing.T, matc
|
||||
url := c.url("delete", "prometheus/api/v1/admin/tsdb/delete_series", opts)
|
||||
values := opts.asURLValues()
|
||||
values.Add("match[]", matchQuery)
|
||||
res, statusCode := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
res, statusCode := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
if statusCode != http.StatusNoContent {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", statusCode, http.StatusNoContent, res)
|
||||
}
|
||||
@@ -426,7 +426,7 @@ func (c *vmselectClient) PrometheusAPIV1StatusMetricNamesStats(t *testing.T, lim
|
||||
values.Add("limit", limit)
|
||||
values.Add("le", le)
|
||||
values.Add("match_pattern", matchPattern)
|
||||
res, statusCode := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
res, statusCode := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", statusCode, http.StatusOK, res)
|
||||
}
|
||||
@@ -455,7 +455,7 @@ func (c *vmselectClient) PrometheusAPIV1StatusTSDB(t *testing.T, matchQuery stri
|
||||
addNonEmpty("match[]", matchQuery)
|
||||
addNonEmpty("topN", topN)
|
||||
addNonEmpty("date", date)
|
||||
res, statusCode := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
res, statusCode := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", statusCode, http.StatusOK, res)
|
||||
}
|
||||
@@ -476,7 +476,7 @@ func (c *vmselectClient) GraphiteMetricsIndex(t *testing.T, opts QueryOpts) Grap
|
||||
t.Helper()
|
||||
|
||||
url := c.url("select", "graphite/metrics/index.json", opts)
|
||||
res, statusCode := c.vmselectCli.Get(t, url, opts.Headers)
|
||||
res, statusCode := c.cli.Get(t, url, opts.Headers)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", statusCode, http.StatusOK, res)
|
||||
}
|
||||
@@ -499,7 +499,7 @@ func (c *vmselectClient) GraphiteMetricsFind(t *testing.T, query string, opts Qu
|
||||
url := c.url("select", "graphite/metrics/find", opts)
|
||||
values := opts.asURLValues()
|
||||
values.Add("query", query)
|
||||
resText, statusCode := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
resText, statusCode := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", statusCode, http.StatusOK, resText)
|
||||
}
|
||||
@@ -522,7 +522,7 @@ func (c *vmselectClient) GraphiteMetricsExpand(t *testing.T, query string, opts
|
||||
url := c.url("select", "graphite/metrics/expand", opts)
|
||||
values := opts.asURLValues()
|
||||
values.Add("query", query)
|
||||
resText, statusCode := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
resText, statusCode := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", statusCode, http.StatusOK, resText)
|
||||
}
|
||||
@@ -546,7 +546,7 @@ func (c *vmselectClient) GraphiteRender(t *testing.T, target string, opts QueryO
|
||||
values := opts.asURLValues()
|
||||
values.Add("format", "json")
|
||||
values.Add("target", target)
|
||||
resText, statusCode := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
resText, statusCode := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", statusCode, http.StatusOK, resText)
|
||||
}
|
||||
@@ -567,7 +567,7 @@ func (c *vmselectClient) GraphiteTagsTagSeries(t *testing.T, record string, opts
|
||||
url := c.url("select", "graphite/tags/tagSeries", opts)
|
||||
values := opts.asURLValues()
|
||||
values.Add("path", record)
|
||||
_, statusCode := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
_, statusCode := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
if got, want := statusCode, http.StatusNotImplemented; got != want {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", got, want)
|
||||
}
|
||||
@@ -584,7 +584,7 @@ func (c *vmselectClient) GraphiteTagsTagMultiSeries(t *testing.T, records []stri
|
||||
for _, rec := range records {
|
||||
values.Add("path", rec)
|
||||
}
|
||||
_, statusCode := c.vmselectCli.PostForm(t, url, values, opts.Headers)
|
||||
_, statusCode := c.cli.PostForm(t, url, values, opts.Headers)
|
||||
if got, want := statusCode, http.StatusNotImplemented; got != want {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", got, want)
|
||||
}
|
||||
@@ -598,7 +598,7 @@ func (c *vmselectClient) GraphiteTagsTagMultiSeries(t *testing.T, records []stri
|
||||
func (c *vmselectClient) PrometheusAPIV1AdminStatusMetricNamesStatsReset(t *testing.T, opts QueryOpts) {
|
||||
t.Helper()
|
||||
values := opts.asURLValues()
|
||||
res, statusCode := c.vmselectCli.PostForm(t, c.metricNamesStatsResetURL, values, opts.Headers)
|
||||
res, statusCode := c.cli.PostForm(t, c.metricNamesStatsResetURL, values, opts.Headers)
|
||||
if statusCode != http.StatusNoContent {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", statusCode, http.StatusNoContent, res)
|
||||
}
|
||||
@@ -608,7 +608,7 @@ func (c *vmselectClient) PrometheusAPIV1AdminStatusMetricNamesStatsReset(t *test
|
||||
// /admin/tenants endpoint.
|
||||
func (c *vmselectClient) APIV1AdminTenants(t *testing.T, opts QueryOpts) *AdminTenantsResponse {
|
||||
t.Helper()
|
||||
res, statusCode := c.vmselectCli.Get(t, c.tenantsURL, opts.Headers)
|
||||
res, statusCode := c.cli.Get(t, c.tenantsURL, opts.Headers)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", statusCode, http.StatusOK, res)
|
||||
}
|
||||
@@ -622,7 +622,7 @@ func (c *vmselectClient) APIV1AdminTenants(t *testing.T, opts QueryOpts) *AdminT
|
||||
}
|
||||
|
||||
type vminsertClient struct {
|
||||
vminsertCli *Client
|
||||
cli *Client
|
||||
url func(op, path string, opts QueryOpts) string
|
||||
openTSDBURL func(op, path string, opts QueryOpts) string
|
||||
graphiteListenAddr string
|
||||
@@ -647,7 +647,7 @@ func (c *vminsertClient) PrometheusAPIV1ImportCSV(t *testing.T, records []string
|
||||
headers := opts.getHeaders()
|
||||
headers.Set("Content-Type", "text/plain")
|
||||
c.sendBlocking(t, len(records), func() {
|
||||
_, statusCode := c.vminsertCli.Post(t, url, data, headers)
|
||||
_, statusCode := c.cli.Post(t, url, data, headers)
|
||||
if statusCode != http.StatusNoContent {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusNoContent)
|
||||
}
|
||||
@@ -671,7 +671,7 @@ func (c *vminsertClient) PrometheusAPIV1ImportNative(t *testing.T, data []byte,
|
||||
headers := opts.getHeaders()
|
||||
headers.Set("Content-Type", "text/plain")
|
||||
c.sendBlocking(t, 1, func() {
|
||||
_, statusCode := c.vminsertCli.Post(t, url, data, headers)
|
||||
_, statusCode := c.cli.Post(t, url, data, headers)
|
||||
if statusCode != http.StatusNoContent {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusNoContent)
|
||||
}
|
||||
@@ -693,7 +693,7 @@ func (c *vminsertClient) PrometheusAPIV1Write(t *testing.T, wr prompb.WriteReque
|
||||
headers := opts.getHeaders()
|
||||
headers.Set("Content-Type", "application/x-protobuf")
|
||||
c.sendBlocking(t, recordsCount, func() {
|
||||
_, statusCode := c.vminsertCli.Post(t, url, data, headers)
|
||||
_, statusCode := c.cli.Post(t, url, data, headers)
|
||||
if statusCode != http.StatusNoContent {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusNoContent)
|
||||
}
|
||||
@@ -745,7 +745,7 @@ func (c *vminsertClient) PrometheusAPIV1ImportPrometheus(t *testing.T, records [
|
||||
headers := opts.getHeaders()
|
||||
headers.Set("Content-Type", "text/plain")
|
||||
c.sendBlocking(t, recordsCount, func() {
|
||||
_, statusCode := c.vminsertCli.Post(t, url, data, headers)
|
||||
_, statusCode := c.cli.Post(t, url, data, headers)
|
||||
if statusCode != http.StatusNoContent {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusNoContent)
|
||||
}
|
||||
@@ -771,7 +771,7 @@ func (c *vminsertClient) InfluxWrite(t *testing.T, records []string, opts QueryO
|
||||
headers.Set("Content-Type", "text/plain")
|
||||
c.sendBlocking(t, len(records), func() {
|
||||
t.Helper()
|
||||
_, statusCode := c.vminsertCli.Post(t, url, data, headers)
|
||||
_, statusCode := c.cli.Post(t, url, data, headers)
|
||||
if statusCode != http.StatusNoContent {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusNoContent)
|
||||
}
|
||||
@@ -805,7 +805,7 @@ func (c *vminsertClient) OpentelemetryV1Metrics(t *testing.T, md otlppb.MetricsD
|
||||
headers := opts.getHeaders()
|
||||
headers.Set("Content-Type", "application/x-protobuf")
|
||||
c.sendBlocking(t, recordsCount, func() {
|
||||
_, statusCode := c.vminsertCli.Post(t, url, data, headers)
|
||||
_, statusCode := c.cli.Post(t, url, data, headers)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusOK)
|
||||
}
|
||||
@@ -830,7 +830,7 @@ func (c *vminsertClient) OpenTSDBAPIPut(t *testing.T, records []string, opts Que
|
||||
headers := opts.getHeaders()
|
||||
headers.Set("Content-Type", "application/json")
|
||||
c.sendBlocking(t, len(records), func() {
|
||||
_, statusCode := c.vminsertCli.Post(t, url, data, headers)
|
||||
_, statusCode := c.cli.Post(t, url, data, headers)
|
||||
if statusCode != http.StatusNoContent {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusNoContent)
|
||||
}
|
||||
@@ -853,7 +853,7 @@ func (c *vminsertClient) ZabbixConnectorHistory(t *testing.T, records []string,
|
||||
headers := opts.getHeaders()
|
||||
headers.Set("Content-Type", "application/json")
|
||||
c.sendBlocking(t, len(records), func() {
|
||||
_, statusCode := c.vminsertCli.Post(t, url, data, headers)
|
||||
_, statusCode := c.cli.Post(t, url, data, headers)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusOK)
|
||||
}
|
||||
@@ -867,11 +867,11 @@ func (c *vminsertClient) ZabbixConnectorHistory(t *testing.T, records []string,
|
||||
// See https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#ingesting
|
||||
func (c *vminsertClient) GraphiteWrite(t *testing.T, records []string, _ QueryOpts) {
|
||||
t.Helper()
|
||||
c.vminsertCli.Write(t, c.graphiteListenAddr, records)
|
||||
c.cli.Write(t, c.graphiteListenAddr, records)
|
||||
}
|
||||
|
||||
type vmstorageClient struct {
|
||||
vmstorageCli *Client
|
||||
cli *Client
|
||||
httpListenAddr string
|
||||
}
|
||||
|
||||
@@ -881,7 +881,7 @@ func (c *vmstorageClient) ForceFlush(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
url := fmt.Sprintf("http://%s/internal/force_flush", c.httpListenAddr)
|
||||
_, statusCode := c.vmstorageCli.Get(t, url, nil)
|
||||
_, statusCode := c.cli.Get(t, url, nil)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusOK)
|
||||
}
|
||||
@@ -892,7 +892,7 @@ func (c *vmstorageClient) ForceMerge(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
url := fmt.Sprintf("http://%s/internal/force_merge", c.httpListenAddr)
|
||||
_, statusCode := c.vmstorageCli.Get(t, url, nil)
|
||||
_, statusCode := c.cli.Get(t, url, nil)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusOK)
|
||||
}
|
||||
@@ -905,7 +905,7 @@ func (c *vmstorageClient) ForceMerge(t *testing.T) {
|
||||
func (c *vmstorageClient) SnapshotCreate(t *testing.T) *SnapshotCreateResponse {
|
||||
t.Helper()
|
||||
|
||||
data, statusCode := c.vmstorageCli.Post(t, c.SnapshotCreateURL(), nil, nil)
|
||||
data, statusCode := c.cli.Post(t, c.SnapshotCreateURL(), nil, nil)
|
||||
if got, want := statusCode, http.StatusOK; got != want {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", got, want, data)
|
||||
}
|
||||
@@ -931,7 +931,7 @@ func (c *vmstorageClient) APIV1AdminTSDBSnapshot(t *testing.T) *APIV1AdminTSDBSn
|
||||
t.Helper()
|
||||
|
||||
url := fmt.Sprintf("http://%s/api/v1/admin/tsdb/snapshot", c.httpListenAddr)
|
||||
data, statusCode := c.vmstorageCli.Post(t, url, nil, nil)
|
||||
data, statusCode := c.cli.Post(t, url, nil, nil)
|
||||
if got, want := statusCode, http.StatusOK; got != want {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", got, want, data)
|
||||
}
|
||||
@@ -952,7 +952,7 @@ func (c *vmstorageClient) SnapshotList(t *testing.T) *SnapshotListResponse {
|
||||
t.Helper()
|
||||
|
||||
url := fmt.Sprintf("http://%s/snapshot/list", c.httpListenAddr)
|
||||
data, statusCode := c.vmstorageCli.Get(t, url, nil)
|
||||
data, statusCode := c.cli.Get(t, url, nil)
|
||||
if got, want := statusCode, http.StatusOK; got != want {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", got, want, data)
|
||||
}
|
||||
@@ -973,7 +973,7 @@ func (c *vmstorageClient) SnapshotDelete(t *testing.T, snapshotName string) *Sna
|
||||
t.Helper()
|
||||
|
||||
url := fmt.Sprintf("http://%s/snapshot/delete?snapshot=%s", c.httpListenAddr, snapshotName)
|
||||
data, statusCode := c.vmstorageCli.Delete(t, url)
|
||||
data, statusCode := c.cli.Delete(t, url)
|
||||
wantStatusCodes := map[int]bool{
|
||||
http.StatusOK: true,
|
||||
http.StatusInternalServerError: true,
|
||||
@@ -998,7 +998,7 @@ func (c *vmstorageClient) SnapshotDeleteAll(t *testing.T) *SnapshotDeleteAllResp
|
||||
t.Helper()
|
||||
|
||||
url := fmt.Sprintf("http://%s/snapshot/delete_all", c.httpListenAddr)
|
||||
data, statusCode := c.vmstorageCli.Post(t, url, nil, nil)
|
||||
data, statusCode := c.cli.Post(t, url, nil, nil)
|
||||
if got, want := statusCode, http.StatusOK; got != want {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", got, want, data)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -88,19 +88,11 @@ func (tc *TestCase) MustStartDefaultVmsingle() *Vmsingle {
|
||||
}
|
||||
|
||||
// MustStartVmsingle is a test helper function that starts an instance of
|
||||
// vmsingle located at ../../bin/victoria-metrics-race and fails the test if the app
|
||||
// fails to start.
|
||||
// vmsingle (latest version) and fails the test if the app fails to start.
|
||||
func (tc *TestCase) MustStartVmsingle(instance string, flags []string) *Vmsingle {
|
||||
tc.t.Helper()
|
||||
return tc.MustStartVmsingleAt(instance, "../../bin/victoria-metrics-race", flags)
|
||||
}
|
||||
|
||||
// MustStartVmsingleAt is a test helper function that starts an instance of
|
||||
// vmsingle and fails the test if the app fails to start.
|
||||
func (tc *TestCase) MustStartVmsingleAt(instance, binary string, flags []string) *Vmsingle {
|
||||
tc.t.Helper()
|
||||
|
||||
app, err := StartVmsingleAt(instance, binary, flags, tc.cli, tc.output)
|
||||
app, err := StartVmsingle(instance, flags, tc.cli, tc.output)
|
||||
if err != nil {
|
||||
tc.t.Fatalf("Could not start %s: %v", instance, err)
|
||||
}
|
||||
@@ -109,19 +101,11 @@ func (tc *TestCase) MustStartVmsingleAt(instance, binary string, flags []string)
|
||||
}
|
||||
|
||||
// MustStartVmstorage is a test helper function that starts an instance of
|
||||
// vmstorage located at ../../bin/vmstorage-race and fails the test if the app fails
|
||||
// to start.
|
||||
// vmstorage (latest version) and fails the test if the app fails to start.
|
||||
func (tc *TestCase) MustStartVmstorage(instance string, flags []string) *Vmstorage {
|
||||
tc.t.Helper()
|
||||
return tc.MustStartVmstorageAt(instance, "../../bin/vmstorage-race", flags)
|
||||
}
|
||||
|
||||
// MustStartVmstorageAt is a test helper function that starts an instance of
|
||||
// vmstorage and fails the test if the app fails to start.
|
||||
func (tc *TestCase) MustStartVmstorageAt(instance string, binary string, flags []string) *Vmstorage {
|
||||
tc.t.Helper()
|
||||
|
||||
app, err := StartVmstorageAt(instance, binary, flags, tc.cli, tc.output)
|
||||
app, err := StartVmstorage(instance, flags, tc.cli, tc.output)
|
||||
if err != nil {
|
||||
tc.t.Fatalf("Could not start %s: %v", instance, err)
|
||||
}
|
||||
@@ -130,7 +114,7 @@ func (tc *TestCase) MustStartVmstorageAt(instance string, binary string, flags [
|
||||
}
|
||||
|
||||
// MustStartVmselect is a test helper function that starts an instance of
|
||||
// vmselect and fails the test if the app fails to start.
|
||||
// vmselect (latest version) and fails the test if the app fails to start.
|
||||
func (tc *TestCase) MustStartVmselect(instance string, flags []string) *Vmselect {
|
||||
tc.t.Helper()
|
||||
|
||||
@@ -290,10 +274,8 @@ func (tc *TestCase) MustStartDefaultCluster() *Vmcluster {
|
||||
// tests usually come paired with corresponding vmsingle tests.
|
||||
type ClusterOptions struct {
|
||||
Vmstorage1Instance string
|
||||
Vmstorage1Binary string
|
||||
Vmstorage1Flags []string
|
||||
Vmstorage2Instance string
|
||||
Vmstorage2Binary string
|
||||
Vmstorage2Flags []string
|
||||
VminsertInstance string
|
||||
VminsertFlags []string
|
||||
@@ -305,15 +287,8 @@ type ClusterOptions struct {
|
||||
func (tc *TestCase) MustStartCluster(opts *ClusterOptions) *Vmcluster {
|
||||
tc.t.Helper()
|
||||
|
||||
if opts.Vmstorage1Binary == "" {
|
||||
opts.Vmstorage1Binary = "../../bin/vmstorage-race"
|
||||
}
|
||||
vmstorage1 := tc.MustStartVmstorageAt(opts.Vmstorage1Instance, opts.Vmstorage1Binary, opts.Vmstorage1Flags)
|
||||
|
||||
if opts.Vmstorage2Binary == "" {
|
||||
opts.Vmstorage2Binary = "../../bin/vmstorage-race"
|
||||
}
|
||||
vmstorage2 := tc.MustStartVmstorageAt(opts.Vmstorage2Instance, opts.Vmstorage2Binary, opts.Vmstorage2Flags)
|
||||
vmstorage1 := tc.MustStartVmstorage(opts.Vmstorage1Instance, opts.Vmstorage1Flags)
|
||||
vmstorage2 := tc.MustStartVmstorage(opts.Vmstorage2Instance, opts.Vmstorage2Flags)
|
||||
|
||||
opts.VminsertFlags = append(opts.VminsertFlags, []string{
|
||||
"-storageNode=" + vmstorage1.VminsertAddr() + "," + vmstorage2.VminsertAddr(),
|
||||
|
||||
50
apptest/testcase_legacy.go
Normal file
50
apptest/testcase_legacy.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package apptest
|
||||
|
||||
// MustStartVmsingle_v1_132_0 is a test helper function that starts an instance
|
||||
// of vmsingle-v1.132.0 (last version that uses legacy index) and fails the test
|
||||
// if the app fails to start.
|
||||
func (tc *TestCase) MustStartVmsingle_v1_132_0(instance string, flags []string) *Vmsingle {
|
||||
tc.t.Helper()
|
||||
|
||||
app, err := StartVmsingle_v1_132_0(instance, flags, tc.cli, tc.output)
|
||||
if err != nil {
|
||||
tc.t.Fatalf("Could not start %s: %v", instance, err)
|
||||
}
|
||||
tc.addApp(instance, app)
|
||||
return app
|
||||
}
|
||||
|
||||
// MustStartVmstorage_v1_132_0 is a test helper function that starts an instance
|
||||
// of vmstorage-v1.132.0 (last version that uses legacy index) and fails the
|
||||
// test if the app fails to start.
|
||||
func (tc *TestCase) MustStartVmstorage_v1_132_0(instance string, flags []string) *Vmstorage {
|
||||
tc.t.Helper()
|
||||
|
||||
app, err := StartVmstorage_v1_132_0(instance, flags, tc.cli, tc.output)
|
||||
if err != nil {
|
||||
tc.t.Fatalf("Could not start %s: %v", instance, err)
|
||||
}
|
||||
tc.addApp(instance, app)
|
||||
return app
|
||||
}
|
||||
|
||||
// MustStartCluster_v1_132_0 starts a cluster with vmstorage-v1.132.0 with
|
||||
// custom flags.
|
||||
func (tc *TestCase) MustStartCluster_v1_132_0(opts *ClusterOptions) *Vmcluster {
|
||||
tc.t.Helper()
|
||||
|
||||
vmstorage1 := tc.MustStartVmstorage_v1_132_0(opts.Vmstorage1Instance, opts.Vmstorage1Flags)
|
||||
vmstorage2 := tc.MustStartVmstorage_v1_132_0(opts.Vmstorage2Instance, opts.Vmstorage2Flags)
|
||||
|
||||
opts.VminsertFlags = append(opts.VminsertFlags, []string{
|
||||
"-storageNode=" + vmstorage1.VminsertAddr() + "," + vmstorage2.VminsertAddr(),
|
||||
}...)
|
||||
vminsert := tc.MustStartVminsert(opts.VminsertInstance, opts.VminsertFlags)
|
||||
|
||||
opts.VmselectFlags = append(opts.VmselectFlags, []string{
|
||||
"-storageNode=" + vmstorage1.VmselectAddr() + "," + vmstorage2.VmselectAddr(),
|
||||
}...)
|
||||
vmselect := tc.MustStartVmselect(opts.VmselectInstance, opts.VmselectFlags)
|
||||
|
||||
return &Vmcluster{vminsert, vmselect, []*Vmstorage{vmstorage1, vmstorage2}}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
@@ -11,11 +10,6 @@ import (
|
||||
at "github.com/VictoriaMetrics/VictoriaMetrics/apptest"
|
||||
)
|
||||
|
||||
var (
|
||||
legacyVmsinglePath = os.Getenv("VM_LEGACY_VMSINGLE_PATH")
|
||||
legacyVmstoragePath = os.Getenv("VM_LEGACY_VMSTORAGE_PATH")
|
||||
)
|
||||
|
||||
type testLegacyDeleteSeriesOpts struct {
|
||||
startLegacySUT func() at.PrometheusWriteQuerier
|
||||
startNewSUT func() at.PrometheusWriteQuerier
|
||||
@@ -31,7 +25,7 @@ func TestLegacySingleDeleteSeries(t *testing.T) {
|
||||
|
||||
opts := testLegacyDeleteSeriesOpts{
|
||||
startLegacySUT: func() at.PrometheusWriteQuerier {
|
||||
return tc.MustStartVmsingleAt("vmsingle-legacy", legacyVmsinglePath, []string{
|
||||
return tc.MustStartVmsingle_v1_132_0("vmsingle-legacy", []string{
|
||||
"-storageDataPath=" + storageDataPath,
|
||||
"-retentionPeriod=100y",
|
||||
"-search.maxStalenessInterval=1m",
|
||||
@@ -64,15 +58,13 @@ func TestLegacyClusterDeleteSeries(t *testing.T) {
|
||||
|
||||
opts := testLegacyDeleteSeriesOpts{
|
||||
startLegacySUT: func() at.PrometheusWriteQuerier {
|
||||
return tc.MustStartCluster(&at.ClusterOptions{
|
||||
return tc.MustStartCluster_v1_132_0(&at.ClusterOptions{
|
||||
Vmstorage1Instance: "vmstorage1-legacy",
|
||||
Vmstorage1Binary: legacyVmstoragePath,
|
||||
Vmstorage1Flags: []string{
|
||||
"-storageDataPath=" + storage1DataPath,
|
||||
"-retentionPeriod=100y",
|
||||
},
|
||||
Vmstorage2Instance: "vmstorage2-legacy",
|
||||
Vmstorage2Binary: legacyVmstoragePath,
|
||||
Vmstorage2Flags: []string{
|
||||
"-storageDataPath=" + storage2DataPath,
|
||||
"-retentionPeriod=100y",
|
||||
@@ -255,7 +247,7 @@ func TestLegacySingleBackupRestore(t *testing.T) {
|
||||
|
||||
opts := testLegacyBackupRestoreOpts{
|
||||
startLegacySUT: func() at.PrometheusWriteQuerier {
|
||||
return tc.MustStartVmsingleAt("vmsingle-legacy", legacyVmsinglePath, []string{
|
||||
return tc.MustStartVmsingle_v1_132_0("vmsingle-legacy", []string{
|
||||
"-storageDataPath=" + storageDataPath,
|
||||
"-retentionPeriod=100y",
|
||||
"-search.disableCache=true",
|
||||
@@ -298,15 +290,13 @@ func TestLegacyClusterBackupRestore(t *testing.T) {
|
||||
|
||||
opts := testLegacyBackupRestoreOpts{
|
||||
startLegacySUT: func() at.PrometheusWriteQuerier {
|
||||
return tc.MustStartCluster(&at.ClusterOptions{
|
||||
return tc.MustStartCluster_v1_132_0(&at.ClusterOptions{
|
||||
Vmstorage1Instance: "vmstorage1-legacy",
|
||||
Vmstorage1Binary: legacyVmstoragePath,
|
||||
Vmstorage1Flags: []string{
|
||||
"-storageDataPath=" + storage1DataPath,
|
||||
"-retentionPeriod=100y",
|
||||
},
|
||||
Vmstorage2Instance: "vmstorage2-legacy",
|
||||
Vmstorage2Binary: legacyVmstoragePath,
|
||||
Vmstorage2Flags: []string{
|
||||
"-storageDataPath=" + storage2DataPath,
|
||||
"-retentionPeriod=100y",
|
||||
@@ -583,7 +573,7 @@ func TestLegacySingleDowngrade(t *testing.T) {
|
||||
|
||||
opts := testLegacyDowngradeOpts{
|
||||
startLegacySUT: func() at.PrometheusWriteQuerier {
|
||||
return tc.MustStartVmsingleAt("vmsingle-legacy", legacyVmsinglePath, []string{
|
||||
return tc.MustStartVmsingle_v1_132_0("vmsingle-legacy", []string{
|
||||
"-storageDataPath=" + storageDataPath,
|
||||
"-retentionPeriod=100y",
|
||||
"-search.disableCache=true",
|
||||
@@ -618,15 +608,13 @@ func TestLegacyClusterDowngrade(t *testing.T) {
|
||||
|
||||
opts := testLegacyDowngradeOpts{
|
||||
startLegacySUT: func() at.PrometheusWriteQuerier {
|
||||
return tc.MustStartCluster(&at.ClusterOptions{
|
||||
return tc.MustStartCluster_v1_132_0(&at.ClusterOptions{
|
||||
Vmstorage1Instance: "vmstorage1-legacy",
|
||||
Vmstorage1Binary: legacyVmstoragePath,
|
||||
Vmstorage1Flags: []string{
|
||||
"-storageDataPath=" + storage1DataPath,
|
||||
"-retentionPeriod=100y",
|
||||
},
|
||||
Vmstorage2Instance: "vmstorage2-legacy",
|
||||
Vmstorage2Binary: legacyVmstoragePath,
|
||||
Vmstorage2Flags: []string{
|
||||
"-storageDataPath=" + storage2DataPath,
|
||||
"-retentionPeriod=100y",
|
||||
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user