mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-05-30 07:10:55 +03:00
Compare commits
90 Commits
v1.123.0
...
configwatc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0625bb77d | ||
|
|
4163f18250 | ||
|
|
686289c02b | ||
|
|
9ae10247bb | ||
|
|
06ce3f1496 | ||
|
|
d0690ba15f | ||
|
|
483e00ffb9 | ||
|
|
06f969a4a7 | ||
|
|
9517f5cf1a | ||
|
|
e62e0685dc | ||
|
|
df92e617db | ||
|
|
7c0c8cc702 | ||
|
|
07291c1d62 | ||
|
|
7c0015b836 | ||
|
|
06e52a99fd | ||
|
|
f5840951a4 | ||
|
|
9ca5a8d0f4 | ||
|
|
894b22590d | ||
|
|
f85fd161e4 | ||
|
|
7d552dbd9a | ||
|
|
795c3deaee | ||
|
|
cb44353a36 | ||
|
|
7e05200c60 | ||
|
|
a2f033ce6c | ||
|
|
78b217d70c | ||
|
|
c9b23de9ce | ||
|
|
16a75129be | ||
|
|
68bdb5e4d3 | ||
|
|
4360d10962 | ||
|
|
ce9c868f59 | ||
|
|
212ce1baf0 | ||
|
|
1a091e5831 | ||
|
|
bac186fc65 | ||
|
|
15ce9e5e49 | ||
|
|
2c1596ea84 | ||
|
|
21d4f844ab | ||
|
|
da0002ce66 | ||
|
|
f35b9ed36d | ||
|
|
b4dc67cba6 | ||
|
|
70afdd0285 | ||
|
|
51efd2c32b | ||
|
|
1e208a8c79 | ||
|
|
e49027df8f | ||
|
|
a518a4a904 | ||
|
|
ad46fce7d4 | ||
|
|
7cc13ee1cc | ||
|
|
74fcd10d2e | ||
|
|
59007cda51 | ||
|
|
5869a39e7b | ||
|
|
c3c802a61c | ||
|
|
8b92af9d45 | ||
|
|
e313874d01 | ||
|
|
58a4e48901 | ||
|
|
16d75ab0bd | ||
|
|
fe0afc3fea | ||
|
|
f99e49c15d | ||
|
|
1ba994970b | ||
|
|
25cd5637bc | ||
|
|
00c7533095 | ||
|
|
f668e5d9c6 | ||
|
|
f4548a46a7 | ||
|
|
7048de8d20 | ||
|
|
cba4b2f0df | ||
|
|
068d5a4b07 | ||
|
|
e392cbbba3 | ||
|
|
06f590ee63 | ||
|
|
5eef1d66e0 | ||
|
|
5a572387cf | ||
|
|
c6b165ecba | ||
|
|
fd928a0f5b | ||
|
|
c207c32c44 | ||
|
|
1f2c14260c | ||
|
|
87604e6df6 | ||
|
|
1beb1f69d5 | ||
|
|
5266bf1f3b | ||
|
|
d4aefcecc4 | ||
|
|
93c373d55a | ||
|
|
58bc05ce56 | ||
|
|
516a454f0a | ||
|
|
9fd9de7ab4 | ||
|
|
5a75b93535 | ||
|
|
787bf8ffed | ||
|
|
f4bbb83b6a | ||
|
|
b0409910dc | ||
|
|
b421f43532 | ||
|
|
847398b356 | ||
|
|
53d8e99987 | ||
|
|
7da45924e2 | ||
|
|
ddadfd6d58 | ||
|
|
c025993e8a |
23
.github/copilot-instructions.md
vendored
Normal file
23
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# Project Overview
|
||||
|
||||
VictoriaMetrics is a fast, cost-saving, and scalable solution for monitoring and managing time series data. It delivers high performance and reliability, making it an ideal choice for businesses of all sizes.
|
||||
|
||||
## Folder Structure
|
||||
|
||||
- `/app`: Contains the compilable binaries.
|
||||
- `/lib`: Contains the golang reusable libraries
|
||||
- `/docs/victoriametrics`: Contains documentation for the project.
|
||||
- `/apptest/tests`: Contains integration tests.
|
||||
|
||||
## Libraries and Frameworks
|
||||
|
||||
- Backend: Golang, no framework. Use third-party libraries sparingly.
|
||||
- Frontend: React.
|
||||
|
||||
## Code review guidelines
|
||||
|
||||
Ensure the feature or bugfix includes a changelog entry in /docs/victoriametrics/changelog/CHANGELOG.md.
|
||||
Verify the entry is under the ## tip section and matches the structure and style of existing entries.
|
||||
Chore-only changes may be omitted from the changelog.
|
||||
|
||||
|
||||
63
.github/workflows/build.yml
vendored
63
.github/workflows/build.yml
vendored
@@ -31,43 +31,48 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
name: ${{ matrix.os }}-${{ matrix.arch }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
arch: 386
|
||||
- os: linux
|
||||
arch: amd64
|
||||
- os: linux
|
||||
arch: arm64
|
||||
- os: linux
|
||||
arch: arm
|
||||
- os: linux
|
||||
arch: ppc64le
|
||||
- os: darwin
|
||||
arch: amd64
|
||||
- os: darwin
|
||||
arch: arm64
|
||||
- os: freebsd
|
||||
arch: amd64
|
||||
- os: openbsd
|
||||
arch: amd64
|
||||
- os: windows
|
||||
arch: amd64
|
||||
steps:
|
||||
- name: Free space
|
||||
run: |
|
||||
# cleanup up space to free additional ~20GiB of memory
|
||||
# which are lacking for multiplaform images build
|
||||
formatByteCount() { echo $(numfmt --to=iec-i --suffix=B --padding=7 $1'000'); }
|
||||
getAvailableSpace() { echo $(df -a $1 | awk 'NR > 1 {avail+=$4} END {print avail}'); }
|
||||
BEFORE=$(getAvailableSpace)
|
||||
sudo rm -rf /usr/local/lib/android || true
|
||||
sudo rm -rf /usr/share/dotnet || true
|
||||
sudo rm -rf /opt/ghc || true
|
||||
sudo rm -rf /usr/local/.ghcup || true
|
||||
AFTER=$(getAvailableSpace)
|
||||
SAVED=$((AFTER-BEFORE))
|
||||
echo "Saved $(formatByteCount $SAVED)"
|
||||
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Go
|
||||
id: go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
cache-dependency-path: |
|
||||
go.sum
|
||||
Makefile
|
||||
app/**/Makefile
|
||||
go-version: stable
|
||||
cache: false
|
||||
|
||||
- name: Cache Go artifacts
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/bin
|
||||
~/go/pkg/mod
|
||||
key: go-artifacts-${{ runner.os }}-crossbuild-${{ steps.go.outputs.go-version }}-${{ hashFiles('go.sum', 'Makefile', 'app/**/Makefile') }}
|
||||
restore-keys: go-artifacts-${{ runner.os }}-crossbuild-
|
||||
- name: Build victoria-metrics for ${{ matrix.os }}-${{ matrix.arch }}
|
||||
run: make victoria-metrics-${{ matrix.os }}-${{ matrix.arch }}
|
||||
|
||||
- name: Run crossbuild
|
||||
run: make crossbuild
|
||||
- name: Build vmutils for ${{ matrix.os }}-${{ matrix.arch }}
|
||||
run: make vmutils-${{ matrix.os }}-${{ matrix.arch }}
|
||||
|
||||
2
.github/workflows/codeql-analysis-go.yml
vendored
2
.github/workflows/codeql-analysis-go.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Go
|
||||
id: go
|
||||
|
||||
4
.github/workflows/docs.yaml
vendored
4
.github/workflows/docs.yaml
vendored
@@ -16,12 +16,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
path: __vm
|
||||
|
||||
- name: Checkout private code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: VictoriaMetrics/vmdocs
|
||||
token: ${{ secrets.VM_BOT_GH_TOKEN }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: main
|
||||
name: test
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -25,39 +25,41 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Go
|
||||
id: go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
cache: false
|
||||
cache-dependency-path: |
|
||||
go.sum
|
||||
Makefile
|
||||
app/**/Makefile
|
||||
go-version: stable
|
||||
|
||||
- name: Cache Go artifacts
|
||||
|
||||
- name: Cache golangci-lint
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/.cache/golangci-lint
|
||||
~/go/bin
|
||||
~/go/pkg/mod
|
||||
key: go-artifacts-${{ runner.os }}-check-all-${{ steps.go.outputs.go-version }}-${{ hashFiles('go.sum', 'Makefile', 'app/**/Makefile') }}
|
||||
restore-keys: go-artifacts-${{ runner.os }}-check-all-
|
||||
key: golangci-lint-${{ runner.os }}-${{ hashFiles('.golangci.yml') }}
|
||||
|
||||
- name: Run check-all
|
||||
run: |
|
||||
make check-all
|
||||
git diff --exit-code
|
||||
|
||||
test:
|
||||
name: test
|
||||
needs: lint
|
||||
unit:
|
||||
name: unit
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
@@ -69,25 +71,18 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Go
|
||||
id: go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
cache: false
|
||||
cache-dependency-path: |
|
||||
go.sum
|
||||
Makefile
|
||||
app/**/Makefile
|
||||
go-version: stable
|
||||
|
||||
- name: Cache Go artifacts
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/bin
|
||||
~/go/pkg/mod
|
||||
key: go-artifacts-${{ runner.os }}-${{ matrix.scenario }}-${{ steps.go.outputs.go-version }}-${{ hashFiles('go.sum', 'Makefile', 'app/**/Makefile') }}
|
||||
restore-keys: go-artifacts-${{ runner.os }}-${{ matrix.scenario }}-
|
||||
|
||||
- name: Run tests
|
||||
run: GOGC=10 make ${{ matrix.scenario}}
|
||||
|
||||
@@ -96,31 +91,23 @@ jobs:
|
||||
with:
|
||||
files: ./coverage.txt
|
||||
|
||||
integration-test:
|
||||
name: integration-test
|
||||
needs: [lint, test]
|
||||
integration:
|
||||
name: integration
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Go
|
||||
id: go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
cache: false
|
||||
cache-dependency-path: |
|
||||
go.sum
|
||||
Makefile
|
||||
app/**/Makefile
|
||||
go-version: stable
|
||||
|
||||
- name: Cache Go artifacts
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/bin
|
||||
~/go/pkg/mod
|
||||
key: go-artifacts-${{ runner.os }}-${{ matrix.scenario }}-${{ steps.go.outputs.go-version }}-${{ hashFiles('go.sum', 'Makefile', 'app/**/Makefile') }}
|
||||
restore-keys: go-artifacts-${{ runner.os }}-${{ matrix.scenario }}-
|
||||
|
||||
- name: Run integration tests
|
||||
run: make integration-test
|
||||
2
.github/workflows/vmui.yml
vendored
2
.github/workflows/vmui.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
14
Makefile
14
Makefile
@@ -12,11 +12,12 @@ PKG_TAG := $(BUILDINFO_TAG)
|
||||
endif
|
||||
|
||||
EXTRA_DOCKER_TAG_SUFFIX ?=
|
||||
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.2.1
|
||||
GOLANGCI_LINT_VERSION := 2.4.0
|
||||
|
||||
.PHONY: $(MAKECMDGOALS)
|
||||
|
||||
@@ -169,9 +170,11 @@ vmutils-windows-amd64: \
|
||||
vmrestore-windows-amd64 \
|
||||
vmctl-windows-amd64
|
||||
|
||||
# When adding a new crossbuild target, please also add it to the .github/workflows/build.yml
|
||||
crossbuild:
|
||||
$(MAKE_PARALLEL) victoria-metrics-crossbuild vmutils-crossbuild
|
||||
|
||||
# When adding a new crossbuild target, please also add it to the .github/workflows/build.yml
|
||||
victoria-metrics-crossbuild: \
|
||||
victoria-metrics-linux-386 \
|
||||
victoria-metrics-linux-amd64 \
|
||||
@@ -184,6 +187,7 @@ victoria-metrics-crossbuild: \
|
||||
victoria-metrics-openbsd-amd64 \
|
||||
victoria-metrics-windows-amd64
|
||||
|
||||
# When adding a new crossbuild target, please also add it to the .github/workflows/build.yml
|
||||
vmutils-crossbuild: \
|
||||
vmutils-linux-386 \
|
||||
vmutils-linux-amd64 \
|
||||
@@ -467,16 +471,16 @@ vendor-update:
|
||||
go mod vendor
|
||||
|
||||
app-local:
|
||||
CGO_ENABLED=1 go build $(RACE) -ldflags "$(GO_BUILDINFO)" -o bin/$(APP_NAME)$(RACE) $(PKG_PREFIX)/app/$(APP_NAME)
|
||||
CGO_ENABLED=1 go build $(RACE) -ldflags "$(GO_BUILDINFO)" -tags "$(EXTRA_GO_BUILD_TAGS)" -o bin/$(APP_NAME)$(RACE) $(PKG_PREFIX)/app/$(APP_NAME)
|
||||
|
||||
app-local-pure:
|
||||
CGO_ENABLED=0 go build $(RACE) -ldflags "$(GO_BUILDINFO)" -o bin/$(APP_NAME)-pure$(RACE) $(PKG_PREFIX)/app/$(APP_NAME)
|
||||
CGO_ENABLED=0 go build $(RACE) -ldflags "$(GO_BUILDINFO)" -tags "$(EXTRA_GO_BUILD_TAGS)" -o bin/$(APP_NAME)-pure$(RACE) $(PKG_PREFIX)/app/$(APP_NAME)
|
||||
|
||||
app-local-goos-goarch:
|
||||
CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH) go build $(RACE) -ldflags "$(GO_BUILDINFO)" -o bin/$(APP_NAME)-$(GOOS)-$(GOARCH)$(RACE) $(PKG_PREFIX)/app/$(APP_NAME)
|
||||
CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH) go build $(RACE) -ldflags "$(GO_BUILDINFO)" -tags "$(EXTRA_GO_BUILD_TAGS)" -o bin/$(APP_NAME)-$(GOOS)-$(GOARCH)$(RACE) $(PKG_PREFIX)/app/$(APP_NAME)
|
||||
|
||||
app-local-windows-goarch:
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=$(GOARCH) go build $(RACE) -ldflags "$(GO_BUILDINFO)" -o bin/$(APP_NAME)-windows-$(GOARCH)$(RACE).exe $(PKG_PREFIX)/app/$(APP_NAME)
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=$(GOARCH) go build $(RACE) -ldflags "$(GO_BUILDINFO)" -tags "$(EXTRA_GO_BUILD_TAGS)" -o bin/$(APP_NAME)-windows-$(GOARCH)$(RACE).exe $(PKG_PREFIX)/app/$(APP_NAME)
|
||||
|
||||
quicktemplate-gen: install-qtc
|
||||
qtc
|
||||
|
||||
@@ -57,7 +57,8 @@ func selfScraper(scrapeInterval time.Duration) {
|
||||
appmetrics.WritePrometheusMetrics(&bb)
|
||||
s := bytesutil.ToUnsafeString(bb.B)
|
||||
rows.Reset()
|
||||
rows.Unmarshal(s)
|
||||
// VictoriaMetrics components don't expose metadata yet, only need to parse samples
|
||||
rows.UnmarshalWithErrLogger(s, nil)
|
||||
mrs = mrs[:0]
|
||||
for i := range rows.Rows {
|
||||
r := &rows.Rows[i]
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/configwatcher"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/csvimport"
|
||||
@@ -112,6 +113,7 @@ func main() {
|
||||
flag.Usage = usage
|
||||
envflag.Parse()
|
||||
remotewrite.InitSecretFlags()
|
||||
configwatcher.Init()
|
||||
buildinfo.Init()
|
||||
logger.Init()
|
||||
timeserieslimits.Init(*maxLabelsPerTimeseries, *maxLabelNameLen, *maxLabelValueLen)
|
||||
@@ -199,6 +201,7 @@ func main() {
|
||||
}
|
||||
protoparserutil.StopUnmarshalWorkers()
|
||||
remotewrite.Stop()
|
||||
configwatcher.Stop()
|
||||
|
||||
logger.Infof("successfully stopped vmagent in %.3f seconds", time.Since(startTime).Seconds())
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/remotewrite"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/opentelemetry/firehose"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/opentelemetry/stream"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
|
||||
@@ -16,9 +17,11 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
rowsInserted = metrics.NewCounter(`vmagent_rows_inserted_total{type="opentelemetry"}`)
|
||||
rowsTenantInserted = tenantmetrics.NewCounterMap(`vmagent_tenant_inserted_rows_total{type="opentelemetry"}`)
|
||||
rowsPerInsert = metrics.NewHistogram(`vmagent_rows_per_insert{type="opentelemetry"}`)
|
||||
rowsInserted = metrics.NewCounter(`vmagent_rows_inserted_total{type="opentelemetry"}`)
|
||||
metadataInserted = metrics.NewCounter(`vmagent_metadata_inserted_total{type="opentelemetry"}`)
|
||||
rowsTenantInserted = tenantmetrics.NewCounterMap(`vmagent_tenant_inserted_rows_total{type="opentelemetry"}`)
|
||||
metadataTenantInserted = tenantmetrics.NewCounterMap(`vmagent_tenant_inserted_metadata_total{type="opentelemetry"}`)
|
||||
rowsPerInsert = metrics.NewHistogram(`vmagent_rows_per_insert{type="opentelemetry"}`)
|
||||
)
|
||||
|
||||
// InsertHandler processes opentelemetry metrics.
|
||||
@@ -36,12 +39,12 @@ func InsertHandler(at *auth.Token, req *http.Request) error {
|
||||
return fmt.Errorf("json encoding isn't supported for opentelemetry format. Use protobuf encoding")
|
||||
}
|
||||
}
|
||||
return stream.ParseStream(req.Body, encoding, processBody, func(tss []prompb.TimeSeries) error {
|
||||
return insertRows(at, tss, extraLabels)
|
||||
return stream.ParseStream(req.Body, encoding, processBody, func(tss []prompb.TimeSeries, mms []prompb.MetricMetadata) error {
|
||||
return insertRows(at, tss, mms, extraLabels)
|
||||
})
|
||||
}
|
||||
|
||||
func insertRows(at *auth.Token, tss []prompb.TimeSeries, extraLabels []prompb.Label) error {
|
||||
func insertRows(at *auth.Token, tss []prompb.TimeSeries, mms []prompb.MetricMetadata, extraLabels []prompb.Label) error {
|
||||
ctx := common.GetPushCtx()
|
||||
defer common.PutPushCtx(ctx)
|
||||
|
||||
@@ -63,14 +66,33 @@ func insertRows(at *auth.Token, tss []prompb.TimeSeries, extraLabels []prompb.La
|
||||
})
|
||||
}
|
||||
ctx.WriteRequest.Timeseries = tssDst
|
||||
|
||||
var metadataTotal int
|
||||
if promscrape.IsMetadataEnabled() {
|
||||
var accountID, projectID uint32
|
||||
if at != nil {
|
||||
accountID = at.AccountID
|
||||
projectID = at.ProjectID
|
||||
for i := range mms {
|
||||
mm := &mms[i]
|
||||
mm.AccountID = accountID
|
||||
mm.ProjectID = projectID
|
||||
}
|
||||
}
|
||||
ctx.WriteRequest.Metadata = mms
|
||||
metadataTotal = len(mms)
|
||||
}
|
||||
|
||||
ctx.Labels = labels
|
||||
ctx.Samples = samples
|
||||
if !remotewrite.TryPush(at, &ctx.WriteRequest) {
|
||||
return remotewrite.ErrQueueFullHTTPRetry
|
||||
}
|
||||
rowsInserted.Add(rowsTotal)
|
||||
metadataInserted.Add(metadataTotal)
|
||||
if at != nil {
|
||||
rowsTenantInserted.Get(at).Add(rowsTotal)
|
||||
metadataTenantInserted.Get(at).Add(metadataTotal)
|
||||
}
|
||||
rowsPerInsert.Update(float64(rowsTotal))
|
||||
return nil
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus/stream"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
|
||||
@@ -16,9 +17,12 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
rowsInserted = metrics.NewCounter(`vmagent_rows_inserted_total{type="prometheus"}`)
|
||||
rowsTenantInserted = tenantmetrics.NewCounterMap(`vmagent_tenant_inserted_rows_total{type="prometheus"}`)
|
||||
rowsPerInsert = metrics.NewHistogram(`vmagent_rows_per_insert{type="prometheus"}`)
|
||||
rowsInserted = metrics.NewCounter(`vmagent_rows_inserted_total{type="prometheus"}`)
|
||||
metadataInserted = metrics.NewCounter(`vmagent_metadata_inserted_total{type="prometheus"}`)
|
||||
rowsTenantInserted = tenantmetrics.NewCounterMap(`vmagent_tenant_inserted_rows_total{type="prometheus"}`)
|
||||
metadataTenantInserted = tenantmetrics.NewCounterMap(`vmagent_tenant_inserted_metadata_total{type="prometheus"}`)
|
||||
|
||||
rowsPerInsert = metrics.NewHistogram(`vmagent_rows_per_insert{type="prometheus"}`)
|
||||
)
|
||||
|
||||
// InsertHandler processes `/api/v1/import/prometheus` request.
|
||||
@@ -32,18 +36,19 @@ func InsertHandler(at *auth.Token, req *http.Request) error {
|
||||
return err
|
||||
}
|
||||
encoding := req.Header.Get("Content-Encoding")
|
||||
return stream.Parse(req.Body, defaultTimestamp, encoding, true, func(rows []prometheus.Row) error {
|
||||
return insertRows(at, rows, extraLabels)
|
||||
return stream.Parse(req.Body, defaultTimestamp, encoding, true, promscrape.IsMetadataEnabled(), func(rows []prometheus.Row, mms []prometheus.Metadata) error {
|
||||
return insertRows(at, rows, mms, extraLabels)
|
||||
}, func(s string) {
|
||||
httpserver.LogError(req, s)
|
||||
})
|
||||
}
|
||||
|
||||
func insertRows(at *auth.Token, rows []prometheus.Row, extraLabels []prompb.Label) error {
|
||||
func insertRows(at *auth.Token, rows []prometheus.Row, mms []prometheus.Metadata, extraLabels []prompb.Label) error {
|
||||
ctx := common.GetPushCtx()
|
||||
defer common.PutPushCtx(ctx)
|
||||
|
||||
tssDst := ctx.WriteRequest.Timeseries[:0]
|
||||
mmsDst := ctx.WriteRequest.Metadata[:0]
|
||||
labels := ctx.Labels[:0]
|
||||
samples := ctx.Samples[:0]
|
||||
for i := range rows {
|
||||
@@ -70,15 +75,35 @@ func insertRows(at *auth.Token, rows []prometheus.Row, extraLabels []prompb.Labe
|
||||
Samples: samples[len(samples)-1:],
|
||||
})
|
||||
}
|
||||
var accountID, projectID uint32
|
||||
if at != nil {
|
||||
accountID = at.AccountID
|
||||
projectID = at.ProjectID
|
||||
}
|
||||
for i := range mms {
|
||||
mm := &mms[i]
|
||||
mmsDst = append(mmsDst, prompb.MetricMetadata{
|
||||
MetricFamilyName: mm.Metric,
|
||||
Help: mm.Help,
|
||||
Type: mm.Type,
|
||||
// there is no unit in Prometheus exposition formats
|
||||
|
||||
AccountID: accountID,
|
||||
ProjectID: projectID,
|
||||
})
|
||||
}
|
||||
ctx.WriteRequest.Timeseries = tssDst
|
||||
ctx.WriteRequest.Metadata = mmsDst
|
||||
ctx.Labels = labels
|
||||
ctx.Samples = samples
|
||||
if !remotewrite.TryPush(at, &ctx.WriteRequest) {
|
||||
return remotewrite.ErrQueueFullHTTPRetry
|
||||
}
|
||||
rowsInserted.Add(len(rows))
|
||||
metadataInserted.Add(len(mms))
|
||||
if at != nil {
|
||||
rowsTenantInserted.Get(at).Add(len(rows))
|
||||
metadataTenantInserted.Get(at).Add(len(mms))
|
||||
}
|
||||
rowsPerInsert.Update(float64(len(rows)))
|
||||
return nil
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/remotewrite"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/promremotewrite/stream"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/tenantmetrics"
|
||||
@@ -14,9 +15,11 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
rowsInserted = metrics.NewCounter(`vmagent_rows_inserted_total{type="promremotewrite"}`)
|
||||
rowsTenantInserted = tenantmetrics.NewCounterMap(`vmagent_tenant_inserted_rows_total{type="promremotewrite"}`)
|
||||
rowsPerInsert = metrics.NewHistogram(`vmagent_rows_per_insert{type="promremotewrite"}`)
|
||||
rowsInserted = metrics.NewCounter(`vmagent_rows_inserted_total{type="promremotewrite"}`)
|
||||
metadataInserted = metrics.NewCounter(`vmagent_metadata_inserted_total{type="promremotewrite"}`)
|
||||
rowsTenantInserted = tenantmetrics.NewCounterMap(`vmagent_tenant_inserted_rows_total{type="promremotewrite"}`)
|
||||
metadataTenantInserted = tenantmetrics.NewCounterMap(`vmagent_tenant_inserted_metadata_total{type="promremotewrite"}`)
|
||||
rowsPerInsert = metrics.NewHistogram(`vmagent_rows_per_insert{type="promremotewrite"}`)
|
||||
)
|
||||
|
||||
// InsertHandler processes remote write for prometheus.
|
||||
@@ -26,17 +29,18 @@ func InsertHandler(at *auth.Token, req *http.Request) error {
|
||||
return err
|
||||
}
|
||||
isVMRemoteWrite := req.Header.Get("Content-Encoding") == "zstd"
|
||||
return stream.Parse(req.Body, isVMRemoteWrite, func(tss []prompb.TimeSeries) error {
|
||||
return insertRows(at, tss, extraLabels)
|
||||
return stream.Parse(req.Body, isVMRemoteWrite, func(tss []prompb.TimeSeries, mms []prompb.MetricMetadata) error {
|
||||
return insertRows(at, tss, mms, extraLabels)
|
||||
})
|
||||
}
|
||||
|
||||
func insertRows(at *auth.Token, timeseries []prompb.TimeSeries, extraLabels []prompb.Label) error {
|
||||
func insertRows(at *auth.Token, timeseries []prompb.TimeSeries, mms []prompb.MetricMetadata, extraLabels []prompb.Label) error {
|
||||
ctx := common.GetPushCtx()
|
||||
defer common.PutPushCtx(ctx)
|
||||
|
||||
rowsTotal := 0
|
||||
tssDst := ctx.WriteRequest.Timeseries[:0]
|
||||
mmsDst := ctx.WriteRequest.Metadata[:0]
|
||||
labels := ctx.Labels[:0]
|
||||
samples := ctx.Samples[:0]
|
||||
for i := range timeseries {
|
||||
@@ -65,6 +69,30 @@ func insertRows(at *auth.Token, timeseries []prompb.TimeSeries, extraLabels []pr
|
||||
})
|
||||
}
|
||||
ctx.WriteRequest.Timeseries = tssDst
|
||||
|
||||
var metadataTotal int
|
||||
if promscrape.IsMetadataEnabled() {
|
||||
var accountID, projectID uint32
|
||||
if at != nil {
|
||||
accountID = at.AccountID
|
||||
projectID = at.ProjectID
|
||||
}
|
||||
for i := range mms {
|
||||
mm := &mms[i]
|
||||
mmsDst = append(mmsDst, prompb.MetricMetadata{
|
||||
MetricFamilyName: mm.MetricFamilyName,
|
||||
Help: mm.Help,
|
||||
Type: mm.Type,
|
||||
Unit: mm.Unit,
|
||||
|
||||
AccountID: accountID,
|
||||
ProjectID: projectID,
|
||||
})
|
||||
}
|
||||
ctx.WriteRequest.Metadata = mmsDst
|
||||
metadataTotal = len(mms)
|
||||
}
|
||||
|
||||
ctx.Labels = labels
|
||||
ctx.Samples = samples
|
||||
if !remotewrite.TryPush(at, &ctx.WriteRequest) {
|
||||
@@ -73,7 +101,9 @@ func insertRows(at *auth.Token, timeseries []prompb.TimeSeries, extraLabels []pr
|
||||
rowsInserted.Add(rowsTotal)
|
||||
if at != nil {
|
||||
rowsTenantInserted.Get(at).Add(rowsTotal)
|
||||
metadataTenantInserted.Get(at).Add(metadataTotal)
|
||||
}
|
||||
metadataInserted.Add(metadataTotal)
|
||||
rowsPerInsert.Update(float64(rowsTotal))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -463,12 +463,6 @@ again:
|
||||
// - Real-world implementations of v1 use both 400 and 415 status codes.
|
||||
// See more in research: https://github.com/VictoriaMetrics/VictoriaMetrics/pull/8462#issuecomment-2786918054
|
||||
case 415, 400:
|
||||
if c.canDowngradeVMProto.Swap(false) {
|
||||
logger.Infof("received unsupported media type or bad request from remote storage at %q. Downgrading protocol from VictoriaMetrics to Prometheus remote write for all future requests. "+
|
||||
"See https://docs.victoriametrics.com/victoriametrics/vmagent/#victoriametrics-remote-write-protocol", c.sanitizedURL)
|
||||
c.useVMProto.Store(false)
|
||||
}
|
||||
|
||||
if encoding.IsZstd(block) {
|
||||
logger.Infof("received unsupported media type or bad request from remote storage at %q. Re-packing the block to Prometheus remote write and retrying."+
|
||||
"See https://docs.victoriametrics.com/victoriametrics/vmagent/#victoriametrics-remote-write-protocol", c.sanitizedURL)
|
||||
|
||||
@@ -24,9 +24,10 @@ import (
|
||||
|
||||
var (
|
||||
flushInterval = flag.Duration("remoteWrite.flushInterval", time.Second, "Interval for flushing the data to remote storage. "+
|
||||
"This option takes effect only when less than 10K data points per second are pushed to -remoteWrite.url")
|
||||
"This option takes effect only when less than -remoteWrite.maxRowsPerBlock data points per -remoteWrite.flushInterval are pushed to -remoteWrite.url")
|
||||
maxUnpackedBlockSize = flagutil.NewBytes("remoteWrite.maxBlockSize", 8*1024*1024, "The maximum block size to send to remote storage. Bigger blocks may improve performance at the cost of the increased memory usage. See also -remoteWrite.maxRowsPerBlock")
|
||||
maxRowsPerBlock = flag.Int("remoteWrite.maxRowsPerBlock", 10000, "The maximum number of samples to send in each block to remote storage. Higher number may improve performance at the cost of the increased memory usage. See also -remoteWrite.maxBlockSize")
|
||||
maxMetadataPerBlock = flag.Int("remoteWrite.maxMetadataPerBlock", 5000, "The maximum number of metadata to send in each block to remote storage. Higher number may improve performance at the cost of the increased memory usage. See also -remoteWrite.maxBlockSize")
|
||||
vmProtoCompressLevel = flag.Int("remoteWrite.vmProtoCompressLevel", 0, "The compression level for VictoriaMetrics remote write protocol. "+
|
||||
"Higher values reduce network traffic at the cost of higher CPU usage. Negative values reduce CPU usage at the cost of increased network traffic. "+
|
||||
"See https://docs.victoriametrics.com/victoriametrics/vmagent/#victoriametrics-remote-write-protocol")
|
||||
@@ -60,9 +61,16 @@ func (ps *pendingSeries) MustStop() {
|
||||
ps.periodicFlusherWG.Wait()
|
||||
}
|
||||
|
||||
func (ps *pendingSeries) TryPush(tss []prompb.TimeSeries) bool {
|
||||
func (ps *pendingSeries) TryPushTimeSeries(tss []prompb.TimeSeries) bool {
|
||||
ps.mu.Lock()
|
||||
ok := ps.wr.tryPush(tss)
|
||||
ok := ps.wr.tryPushTimeSeries(tss)
|
||||
ps.mu.Unlock()
|
||||
return ok
|
||||
}
|
||||
|
||||
func (ps *pendingSeries) TryPushMetadata(mms []prompb.MetricMetadata) bool {
|
||||
ps.mu.Lock()
|
||||
ok := ps.wr.tryPushMetadata(mms)
|
||||
ps.mu.Unlock()
|
||||
return ok
|
||||
}
|
||||
@@ -111,26 +119,34 @@ type writeRequest struct {
|
||||
wr prompb.WriteRequest
|
||||
|
||||
tss []prompb.TimeSeries
|
||||
mms []prompb.MetricMetadata
|
||||
labels []prompb.Label
|
||||
samples []prompb.Sample
|
||||
|
||||
// buf holds labels data
|
||||
buf []byte
|
||||
// metadatabuf holds metadata data
|
||||
metadatabuf []byte
|
||||
}
|
||||
|
||||
func (wr *writeRequest) reset() {
|
||||
// Do not reset lastFlushTime, fq, isVMRemoteWrite, significantFigures and roundDigits, since they are reused.
|
||||
|
||||
wr.wr.Timeseries = nil
|
||||
wr.wr.Metadata = nil
|
||||
|
||||
clear(wr.tss)
|
||||
wr.tss = wr.tss[:0]
|
||||
|
||||
clear(wr.mms)
|
||||
wr.mms = wr.mms[:0]
|
||||
|
||||
promrelabel.CleanLabels(wr.labels)
|
||||
wr.labels = wr.labels[:0]
|
||||
|
||||
wr.samples = wr.samples[:0]
|
||||
wr.buf = wr.buf[:0]
|
||||
wr.metadatabuf = wr.metadatabuf[:0]
|
||||
}
|
||||
|
||||
// mustFlushOnStop force pushes wr data into wr.fq
|
||||
@@ -138,6 +154,7 @@ func (wr *writeRequest) reset() {
|
||||
// This is needed in order to properly save in-memory data to persistent queue on graceful shutdown.
|
||||
func (wr *writeRequest) mustFlushOnStop() {
|
||||
wr.wr.Timeseries = wr.tss
|
||||
wr.wr.Metadata = wr.mms
|
||||
if !tryPushWriteRequest(&wr.wr, wr.mustWriteBlock, wr.isVMRemoteWrite.Load()) {
|
||||
logger.Panicf("BUG: final flush must always return true")
|
||||
}
|
||||
@@ -151,6 +168,7 @@ func (wr *writeRequest) mustWriteBlock(block []byte) bool {
|
||||
|
||||
func (wr *writeRequest) tryFlush() bool {
|
||||
wr.wr.Timeseries = wr.tss
|
||||
wr.wr.Metadata = wr.mms
|
||||
wr.lastFlushTime.Store(fasttime.UnixTimestamp())
|
||||
if !tryPushWriteRequest(&wr.wr, wr.fq.TryWriteBlock, wr.isVMRemoteWrite.Load()) {
|
||||
return false
|
||||
@@ -174,7 +192,49 @@ func adjustSampleValues(samples []prompb.Sample, significantFigures, roundDigits
|
||||
}
|
||||
}
|
||||
|
||||
func (wr *writeRequest) tryPush(src []prompb.TimeSeries) bool {
|
||||
func (wr *writeRequest) tryPushMetadata(mms []prompb.MetricMetadata) bool {
|
||||
mmdDst := wr.mms
|
||||
maxMetadataPerBlock := *maxMetadataPerBlock
|
||||
for i := range mms {
|
||||
if len(wr.mms) >= maxMetadataPerBlock {
|
||||
if !wr.tryFlush() {
|
||||
return false
|
||||
}
|
||||
mmdDst = wr.mms
|
||||
}
|
||||
mmSrc := &mms[i]
|
||||
mmdDst = append(mmdDst, prompb.MetricMetadata{})
|
||||
wr.copyMetadata(&mmdDst[len(mmdDst)-1], mmSrc)
|
||||
}
|
||||
wr.mms = mmdDst
|
||||
return true
|
||||
}
|
||||
|
||||
func (wr *writeRequest) copyMetadata(dst, src *prompb.MetricMetadata) {
|
||||
// Direct copy for non-string fields, which are safe by value.
|
||||
dst.Type = src.Type
|
||||
dst.Unit = src.Unit
|
||||
|
||||
// Pre-allocate memory for all string fields.
|
||||
neededBufLen := len(src.MetricFamilyName) + len(src.Help)
|
||||
bufLen := len(wr.metadatabuf)
|
||||
wr.metadatabuf = slicesutil.SetLength(wr.metadatabuf, bufLen+neededBufLen)
|
||||
buf := wr.metadatabuf[:bufLen]
|
||||
|
||||
// Copy MetricFamilyName
|
||||
bufLen = len(buf)
|
||||
buf = append(buf, src.MetricFamilyName...)
|
||||
dst.MetricFamilyName = bytesutil.ToUnsafeString(buf[bufLen:])
|
||||
|
||||
// Copy Help
|
||||
bufLen = len(buf)
|
||||
buf = append(buf, src.Help...)
|
||||
dst.Help = bytesutil.ToUnsafeString(buf[bufLen:])
|
||||
|
||||
wr.metadatabuf = buf
|
||||
}
|
||||
|
||||
func (wr *writeRequest) tryPushTimeSeries(src []prompb.TimeSeries) bool {
|
||||
tssDst := wr.tss
|
||||
maxSamplesPerBlock := *maxRowsPerBlock
|
||||
// Allow up to 10x of labels per each block on average.
|
||||
@@ -241,7 +301,7 @@ func (wr *writeRequest) copyTimeSeries(dst, src *prompb.TimeSeries) {
|
||||
var marshalConcurrencyCh = make(chan struct{}, cgroup.AvailableCPUs())
|
||||
|
||||
func tryPushWriteRequest(wr *prompb.WriteRequest, tryPushBlock func(block []byte) bool, isVMRemoteWrite bool) bool {
|
||||
if len(wr.Timeseries) == 0 {
|
||||
if wr.IsEmpty() {
|
||||
// Nothing to push
|
||||
return true
|
||||
}
|
||||
@@ -267,6 +327,7 @@ func tryPushWriteRequest(wr *prompb.WriteRequest, tryPushBlock func(block []byte
|
||||
compressBufPool.Put(zb)
|
||||
if ok {
|
||||
blockSizeRows.Update(float64(len(wr.Timeseries)))
|
||||
blockMetadataRows.Update(float64(len(wr.Metadata)))
|
||||
blockSizeBytes.Update(float64(zbLen))
|
||||
}
|
||||
return ok
|
||||
@@ -278,47 +339,86 @@ func tryPushWriteRequest(wr *prompb.WriteRequest, tryPushBlock func(block []byte
|
||||
<-marshalConcurrencyCh
|
||||
}
|
||||
|
||||
// Too big block. Recursively split it into smaller parts if possible.
|
||||
if len(wr.Timeseries) == 1 {
|
||||
// A single time series left. Recursively split its samples into smaller parts if possible.
|
||||
// Split timeseries or metadata into two smaller blocks
|
||||
switch len(wr.Timeseries) {
|
||||
case 0:
|
||||
if len(wr.Metadata) == 1 {
|
||||
logger.Warnf("dropping a metadata exceeding -remoteWrite.maxBlockSize=%d bytes", maxUnpackedBlockSize.N)
|
||||
return true
|
||||
}
|
||||
metadata := wr.Metadata
|
||||
n := len(metadata) / 2
|
||||
wr.Metadata = metadata[:n]
|
||||
if !tryPushWriteRequest(wr, tryPushBlock, isVMRemoteWrite) {
|
||||
wr.Metadata = metadata
|
||||
return false
|
||||
}
|
||||
wr.Metadata = metadata[n:]
|
||||
if !tryPushWriteRequest(wr, tryPushBlock, isVMRemoteWrite) {
|
||||
wr.Metadata = metadata
|
||||
return false
|
||||
}
|
||||
wr.Metadata = metadata
|
||||
return true
|
||||
|
||||
case 1:
|
||||
// A single time series left. Recursively split its samples and metadata into smaller parts if possible.
|
||||
samples := wr.Timeseries[0].Samples
|
||||
if len(samples) == 1 {
|
||||
logger.Warnf("dropping a sample for metric with too long labels exceeding -remoteWrite.maxBlockSize=%d bytes", maxUnpackedBlockSize.N)
|
||||
metaData := wr.Metadata
|
||||
if len(samples) == 1 && len(metaData) <= 1 {
|
||||
logger.Warnf("dropping a sample for metric and %d metadata which are exceeding -remoteWrite.maxBlockSize=%d bytes", len(metaData), maxUnpackedBlockSize.N)
|
||||
return true
|
||||
}
|
||||
n := len(samples) / 2
|
||||
m := len(metaData) / 2
|
||||
wr.Timeseries[0].Samples = samples[:n]
|
||||
wr.Metadata = metaData[:m]
|
||||
if !tryPushWriteRequest(wr, tryPushBlock, isVMRemoteWrite) {
|
||||
wr.Timeseries[0].Samples = samples
|
||||
wr.Metadata = metaData
|
||||
return false
|
||||
}
|
||||
wr.Timeseries[0].Samples = samples[n:]
|
||||
wr.Metadata = metaData[m:]
|
||||
if !tryPushWriteRequest(wr, tryPushBlock, isVMRemoteWrite) {
|
||||
wr.Timeseries[0].Samples = samples
|
||||
wr.Metadata = metaData
|
||||
return false
|
||||
}
|
||||
wr.Timeseries[0].Samples = samples
|
||||
wr.Metadata = metaData
|
||||
return true
|
||||
|
||||
default:
|
||||
// Split both timeseries and metadata.
|
||||
timeseries := wr.Timeseries
|
||||
metaData := wr.Metadata
|
||||
n := len(timeseries) / 2
|
||||
m := len(metaData) / 2
|
||||
wr.Timeseries = timeseries[:n]
|
||||
wr.Metadata = metaData[:m]
|
||||
if !tryPushWriteRequest(wr, tryPushBlock, isVMRemoteWrite) {
|
||||
wr.Timeseries = timeseries
|
||||
wr.Metadata = metaData
|
||||
return false
|
||||
}
|
||||
wr.Timeseries = timeseries[n:]
|
||||
wr.Metadata = metaData[m:]
|
||||
if !tryPushWriteRequest(wr, tryPushBlock, isVMRemoteWrite) {
|
||||
wr.Timeseries = timeseries
|
||||
wr.Metadata = metaData
|
||||
return false
|
||||
}
|
||||
wr.Timeseries = timeseries
|
||||
wr.Metadata = metaData
|
||||
return true
|
||||
}
|
||||
timeseries := wr.Timeseries
|
||||
n := len(timeseries) / 2
|
||||
wr.Timeseries = timeseries[:n]
|
||||
if !tryPushWriteRequest(wr, tryPushBlock, isVMRemoteWrite) {
|
||||
wr.Timeseries = timeseries
|
||||
return false
|
||||
}
|
||||
wr.Timeseries = timeseries[n:]
|
||||
if !tryPushWriteRequest(wr, tryPushBlock, isVMRemoteWrite) {
|
||||
wr.Timeseries = timeseries
|
||||
return false
|
||||
}
|
||||
wr.Timeseries = timeseries
|
||||
return true
|
||||
}
|
||||
|
||||
var (
|
||||
blockSizeBytes = metrics.NewHistogram(`vmagent_remotewrite_block_size_bytes`)
|
||||
blockSizeRows = metrics.NewHistogram(`vmagent_remotewrite_block_size_rows`)
|
||||
blockSizeBytes = metrics.NewHistogram(`vmagent_remotewrite_block_size_bytes`)
|
||||
blockSizeRows = metrics.NewHistogram(`vmagent_remotewrite_block_size_rows`)
|
||||
blockMetadataRows = metrics.NewHistogram(`vmagent_remotewrite_block_metadata_rows`)
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -209,7 +209,7 @@ func Init() {
|
||||
// In this case it is impossible to prevent from sending many duplicates of samples passed to TryPush() to all the configured -remoteWrite.url
|
||||
// if these samples couldn't be sent to the -remoteWrite.url with the disabled persistent queue. So it is better sending samples
|
||||
// to the remaining -remoteWrite.url and dropping them on the blocked queue.
|
||||
dropSamplesOnFailureGlobal = *dropSamplesOnOverload || disableOnDiskQueueAny && len(disableOnDiskQueues) > 1
|
||||
dropSamplesOnFailureGlobal = *dropSamplesOnOverload || disableOnDiskQueueAny && len(*remoteWriteURLs) > 1
|
||||
|
||||
dropDanglingQueues()
|
||||
|
||||
@@ -388,13 +388,7 @@ func TryPush(at *auth.Token, wr *prompb.WriteRequest) bool {
|
||||
|
||||
func tryPush(at *auth.Token, wr *prompb.WriteRequest, forceDropSamplesOnFailure bool) bool {
|
||||
tss := wr.Timeseries
|
||||
|
||||
var tenantRctx *relabelCtx
|
||||
if at != nil {
|
||||
// Convert at to (vm_account_id, vm_project_id) labels.
|
||||
tenantRctx = getRelabelCtx()
|
||||
defer putRelabelCtx(tenantRctx)
|
||||
}
|
||||
mms := wr.Metadata
|
||||
|
||||
// Quick check whether writes to configured remote storage systems are blocked.
|
||||
// This allows saving CPU time spent on relabeling and block compression
|
||||
@@ -411,6 +405,23 @@ func tryPush(at *auth.Token, wr *prompb.WriteRequest, forceDropSamplesOnFailure
|
||||
return true
|
||||
}
|
||||
|
||||
// Push metadata separately from time series, since it doesn't need sharding,
|
||||
// relabeling, stream aggregation, deduplication, etc.
|
||||
if !tryPushMetadataToRemoteStorages(rwctxs, mms, forceDropSamplesOnFailure) {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(tss) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
var tenantRctx *relabelCtx
|
||||
if at != nil {
|
||||
// Convert at to (vm_account_id, vm_project_id) labels.
|
||||
tenantRctx = getRelabelCtx()
|
||||
defer putRelabelCtx(tenantRctx)
|
||||
}
|
||||
|
||||
var rctx *relabelCtx
|
||||
rcs := allRelabelConfigs.Load()
|
||||
pcsGlobal := rcs.global
|
||||
@@ -481,7 +492,7 @@ func tryPush(at *auth.Token, wr *prompb.WriteRequest, forceDropSamplesOnFailure
|
||||
deduplicatorGlobal.Push(tssBlock)
|
||||
tssBlock = tssBlock[:0]
|
||||
}
|
||||
if !tryPushBlockToRemoteStorages(rwctxs, tssBlock, forceDropSamplesOnFailure) {
|
||||
if !tryPushTimeSeriesToRemoteStorages(rwctxs, tssBlock, forceDropSamplesOnFailure) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -520,18 +531,49 @@ func getEligibleRemoteWriteCtxs(tss []prompb.TimeSeries, forceDropSamplesOnFailu
|
||||
return rwctxs, true
|
||||
}
|
||||
|
||||
func pushToRemoteStoragesTrackDropped(tss []prompb.TimeSeries) {
|
||||
func pushTimeSeriesToRemoteStoragesTrackDropped(tss []prompb.TimeSeries) {
|
||||
rwctxs, _ := getEligibleRemoteWriteCtxs(tss, true)
|
||||
if len(rwctxs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if !tryPushBlockToRemoteStorages(rwctxs, tss, true) {
|
||||
logger.Panicf("BUG: tryPushBlockToRemoteStorages() must return true when forceDropSamplesOnFailure=true")
|
||||
if !tryPushTimeSeriesToRemoteStorages(rwctxs, tss, true) {
|
||||
logger.Panicf("BUG: tryPushTimeSeriesToRemoteStorages() must return true when forceDropSamplesOnFailure=true")
|
||||
}
|
||||
}
|
||||
|
||||
func tryPushBlockToRemoteStorages(rwctxs []*remoteWriteCtx, tssBlock []prompb.TimeSeries, forceDropSamplesOnFailure bool) bool {
|
||||
func tryPushMetadataToRemoteStorages(rwctxs []*remoteWriteCtx, mms []prompb.MetricMetadata, forceDropSamplesOnFailure bool) bool {
|
||||
if len(mms) == 0 {
|
||||
// Nothing to push
|
||||
return true
|
||||
}
|
||||
// Do not shard metadata even if -remoteWrite.shardByURL is set, just replicate it among rwctxs.
|
||||
// Since metadata is usually small and there is no guarantee that metadata can be sent to
|
||||
// the same remote storage with the corresponding metrics.
|
||||
//
|
||||
// Push metadata to remote storage systems in parallel to reduce
|
||||
// the time needed for sending the data to multiple remote storage systems.
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(rwctxs))
|
||||
var anyPushFailed atomic.Bool
|
||||
for _, rwctx := range rwctxs {
|
||||
go func(rwctx *remoteWriteCtx) {
|
||||
defer wg.Done()
|
||||
if !rwctx.tryPushMetadataInternal(mms) {
|
||||
rwctx.pushFailures.Inc()
|
||||
if forceDropSamplesOnFailure {
|
||||
rwctx.metadataDroppedOnPushFailure.Add(len(mms))
|
||||
return
|
||||
}
|
||||
anyPushFailed.Store(true)
|
||||
}
|
||||
}(rwctx)
|
||||
}
|
||||
wg.Wait()
|
||||
return !anyPushFailed.Load()
|
||||
}
|
||||
|
||||
func tryPushTimeSeriesToRemoteStorages(rwctxs []*remoteWriteCtx, tssBlock []prompb.TimeSeries, forceDropSamplesOnFailure bool) bool {
|
||||
if len(tssBlock) == 0 {
|
||||
// Nothing to push
|
||||
return true
|
||||
@@ -539,7 +581,7 @@ func tryPushBlockToRemoteStorages(rwctxs []*remoteWriteCtx, tssBlock []prompb.Ti
|
||||
|
||||
if len(rwctxs) == 1 {
|
||||
// Fast path - just push data to the configured single remote storage
|
||||
return rwctxs[0].TryPush(tssBlock, forceDropSamplesOnFailure)
|
||||
return rwctxs[0].TryPushTimeSeries(tssBlock, forceDropSamplesOnFailure)
|
||||
}
|
||||
|
||||
// We need to push tssBlock to multiple remote storages.
|
||||
@@ -550,11 +592,11 @@ func tryPushBlockToRemoteStorages(rwctxs []*remoteWriteCtx, tssBlock []prompb.Ti
|
||||
if replicas <= 0 {
|
||||
replicas = 1
|
||||
}
|
||||
return tryShardingBlockAmongRemoteStorages(rwctxs, tssBlock, replicas, forceDropSamplesOnFailure)
|
||||
return tryShardingTimeSeriesAmongRemoteStorages(rwctxs, tssBlock, replicas, forceDropSamplesOnFailure)
|
||||
}
|
||||
|
||||
// Replicate tssBlock samples among rwctxs.
|
||||
// Push tssBlock to remote storage systems in parallel in order to reduce
|
||||
// Push tssBlock to remote storage systems in parallel to reduce
|
||||
// the time needed for sending the data to multiple remote storage systems.
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(rwctxs))
|
||||
@@ -562,7 +604,7 @@ func tryPushBlockToRemoteStorages(rwctxs []*remoteWriteCtx, tssBlock []prompb.Ti
|
||||
for _, rwctx := range rwctxs {
|
||||
go func(rwctx *remoteWriteCtx) {
|
||||
defer wg.Done()
|
||||
if !rwctx.TryPush(tssBlock, forceDropSamplesOnFailure) {
|
||||
if !rwctx.TryPushTimeSeries(tssBlock, forceDropSamplesOnFailure) {
|
||||
anyPushFailed.Store(true)
|
||||
}
|
||||
}(rwctx)
|
||||
@@ -571,7 +613,7 @@ func tryPushBlockToRemoteStorages(rwctxs []*remoteWriteCtx, tssBlock []prompb.Ti
|
||||
return !anyPushFailed.Load()
|
||||
}
|
||||
|
||||
func tryShardingBlockAmongRemoteStorages(rwctxs []*remoteWriteCtx, tssBlock []prompb.TimeSeries, replicas int, forceDropSamplesOnFailure bool) bool {
|
||||
func tryShardingTimeSeriesAmongRemoteStorages(rwctxs []*remoteWriteCtx, tssBlock []prompb.TimeSeries, replicas int, forceDropSamplesOnFailure bool) bool {
|
||||
x := getTSSShards(len(rwctxs))
|
||||
defer putTSSShards(x)
|
||||
|
||||
@@ -590,7 +632,7 @@ func tryShardingBlockAmongRemoteStorages(rwctxs []*remoteWriteCtx, tssBlock []pr
|
||||
wg.Add(1)
|
||||
go func(rwctx *remoteWriteCtx, tss []prompb.TimeSeries) {
|
||||
defer wg.Done()
|
||||
if !rwctx.TryPush(tss, forceDropSamplesOnFailure) {
|
||||
if !rwctx.TryPushTimeSeries(tss, forceDropSamplesOnFailure) {
|
||||
anyPushFailed.Store(true)
|
||||
}
|
||||
}(rwctx, shard)
|
||||
@@ -797,8 +839,9 @@ type remoteWriteCtx struct {
|
||||
rowsPushedAfterRelabel *metrics.Counter
|
||||
rowsDroppedByRelabel *metrics.Counter
|
||||
|
||||
pushFailures *metrics.Counter
|
||||
rowsDroppedOnPushFailure *metrics.Counter
|
||||
pushFailures *metrics.Counter
|
||||
metadataDroppedOnPushFailure *metrics.Counter
|
||||
rowsDroppedOnPushFailure *metrics.Counter
|
||||
}
|
||||
|
||||
func newRemoteWriteCtx(argIdx int, remoteWriteURL *url.URL, maxInmemoryBlocks int, sanitizedURL string) *remoteWriteCtx {
|
||||
@@ -862,8 +905,9 @@ func newRemoteWriteCtx(argIdx int, remoteWriteURL *url.URL, maxInmemoryBlocks in
|
||||
rowsPushedAfterRelabel: metrics.GetOrCreateCounter(fmt.Sprintf(`vmagent_remotewrite_rows_pushed_after_relabel_total{path=%q,url=%q}`, queuePath, sanitizedURL)),
|
||||
rowsDroppedByRelabel: metrics.GetOrCreateCounter(fmt.Sprintf(`vmagent_remotewrite_relabel_metrics_dropped_total{path=%q,url=%q}`, queuePath, sanitizedURL)),
|
||||
|
||||
pushFailures: metrics.GetOrCreateCounter(fmt.Sprintf(`vmagent_remotewrite_push_failures_total{path=%q,url=%q}`, queuePath, sanitizedURL)),
|
||||
rowsDroppedOnPushFailure: metrics.GetOrCreateCounter(fmt.Sprintf(`vmagent_remotewrite_samples_dropped_total{path=%q,url=%q}`, queuePath, sanitizedURL)),
|
||||
pushFailures: metrics.GetOrCreateCounter(fmt.Sprintf(`vmagent_remotewrite_push_failures_total{path=%q,url=%q}`, queuePath, sanitizedURL)),
|
||||
metadataDroppedOnPushFailure: metrics.GetOrCreateCounter(fmt.Sprintf(`vmagent_remotewrite_metadata_dropped_total{path=%q,url=%q}`, queuePath, sanitizedURL)),
|
||||
rowsDroppedOnPushFailure: metrics.GetOrCreateCounter(fmt.Sprintf(`vmagent_remotewrite_samples_dropped_total{path=%q,url=%q}`, queuePath, sanitizedURL)),
|
||||
}
|
||||
rwctx.initStreamAggrConfig()
|
||||
|
||||
@@ -897,10 +941,10 @@ func (rwctx *remoteWriteCtx) MustStop() {
|
||||
rwctx.rowsDroppedByRelabel = nil
|
||||
}
|
||||
|
||||
// TryPush sends tss series to the configured remote write endpoint
|
||||
// TryPushTimeSeries sends tss series to the configured remote write endpoint
|
||||
//
|
||||
// TryPush doesn't modify tss, so tss can be passed concurrently to TryPush across distinct rwctx instances.
|
||||
func (rwctx *remoteWriteCtx) TryPush(tss []prompb.TimeSeries, forceDropSamplesOnFailure bool) bool {
|
||||
// TryPushTimeSeries doesn't modify tss, so tss can be passed concurrently to TryPush across distinct rwctx instances.
|
||||
func (rwctx *remoteWriteCtx) TryPushTimeSeries(tss []prompb.TimeSeries, forceDropSamplesOnFailure bool) bool {
|
||||
var rctx *relabelCtx
|
||||
var v *[]prompb.TimeSeries
|
||||
defer func() {
|
||||
@@ -953,7 +997,7 @@ func (rwctx *remoteWriteCtx) TryPush(tss []prompb.TimeSeries, forceDropSamplesOn
|
||||
}
|
||||
|
||||
// Try pushing tss to remote storage
|
||||
if rwctx.tryPushInternal(tss) {
|
||||
if rwctx.tryPushTimeSeriesInternal(tss) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -985,7 +1029,7 @@ func dropAggregatedSeries(src []prompb.TimeSeries, matchIdxs []byte, dropInput b
|
||||
}
|
||||
|
||||
func (rwctx *remoteWriteCtx) pushInternalTrackDropped(tss []prompb.TimeSeries) {
|
||||
if rwctx.tryPushInternal(tss) {
|
||||
if rwctx.tryPushTimeSeriesInternal(tss) {
|
||||
return
|
||||
}
|
||||
if !rwctx.fq.IsPersistentQueueDisabled() {
|
||||
@@ -996,7 +1040,14 @@ func (rwctx *remoteWriteCtx) pushInternalTrackDropped(tss []prompb.TimeSeries) {
|
||||
rwctx.rowsDroppedOnPushFailure.Add(rowsCount)
|
||||
}
|
||||
|
||||
func (rwctx *remoteWriteCtx) tryPushInternal(tss []prompb.TimeSeries) bool {
|
||||
func (rwctx *remoteWriteCtx) tryPushMetadataInternal(mms []prompb.MetricMetadata) bool {
|
||||
pss := rwctx.pss
|
||||
idx := rwctx.pssNextIdx.Add(1) % uint64(len(pss))
|
||||
|
||||
return pss[idx].TryPushMetadata(mms)
|
||||
}
|
||||
|
||||
func (rwctx *remoteWriteCtx) tryPushTimeSeriesInternal(tss []prompb.TimeSeries) bool {
|
||||
var rctx *relabelCtx
|
||||
var v *[]prompb.TimeSeries
|
||||
defer func() {
|
||||
@@ -1020,7 +1071,7 @@ func (rwctx *remoteWriteCtx) tryPushInternal(tss []prompb.TimeSeries) bool {
|
||||
pss := rwctx.pss
|
||||
idx := rwctx.pssNextIdx.Add(1) % uint64(len(pss))
|
||||
|
||||
return pss[idx].TryPush(tss)
|
||||
return pss[idx].TryPushTimeSeries(tss)
|
||||
}
|
||||
|
||||
var tssPool = &sync.Pool{
|
||||
|
||||
@@ -106,7 +106,7 @@ func TestRemoteWriteContext_TryPush_ImmutableTimeseries(t *testing.T) {
|
||||
|
||||
// copy inputTss to make sure it is not mutated during TryPush call
|
||||
copy(expectedTss, inputTss)
|
||||
if !rwctx.TryPush(inputTss, false) {
|
||||
if !rwctx.TryPushTimeSeries(inputTss, false) {
|
||||
t.Fatalf("cannot push samples to rwctx")
|
||||
}
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ func initStreamAggrConfigGlobal() {
|
||||
}
|
||||
dedupInterval := *streamAggrGlobalDedupInterval
|
||||
if dedupInterval > 0 {
|
||||
deduplicatorGlobal = streamaggr.NewDeduplicator(pushToRemoteStoragesTrackDropped, *streamAggrGlobalEnableWindows, dedupInterval, *streamAggrGlobalDropInputLabels, "dedup-global")
|
||||
deduplicatorGlobal = streamaggr.NewDeduplicator(pushTimeSeriesToRemoteStoragesTrackDropped, *streamAggrGlobalEnableWindows, dedupInterval, *streamAggrGlobalDropInputLabels, "dedup-global")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ func newStreamAggrConfigGlobal() (*streamaggr.Aggregators, error) {
|
||||
EnableWindows: *streamAggrGlobalEnableWindows,
|
||||
}
|
||||
|
||||
sas, err := streamaggr.LoadFromFile(path, pushToRemoteStoragesTrackDropped, opts, "global")
|
||||
sas, err := streamaggr.LoadFromFile(path, pushTimeSeriesToRemoteStoragesTrackDropped, opts, "global")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot load -streamAggr.config=%q: %w", *streamAggrGlobalConfig, err)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@ groups:
|
||||
- alert: SameAlertNameWithDifferentGroup
|
||||
expr: absent(test)
|
||||
for: 1m
|
||||
- alert: AlertWithTemplate
|
||||
expr: test
|
||||
annotations:
|
||||
queryAnno: '{{ query "foo" | first | value }}'
|
||||
|
||||
- name: group2
|
||||
rules:
|
||||
|
||||
16
app/vmalert-tool/unittest/testdata/test1.yaml
vendored
16
app/vmalert-tool/unittest/testdata/test1.yaml
vendored
@@ -10,7 +10,9 @@ tests:
|
||||
input_series:
|
||||
- series: "test"
|
||||
values: "_x5 1x5 _ stale"
|
||||
|
||||
- series: "foo"
|
||||
values: "1x20"
|
||||
|
||||
alert_rule_test:
|
||||
- eval_time: 1m
|
||||
groupname: group1
|
||||
@@ -32,6 +34,14 @@ tests:
|
||||
groupname: group1
|
||||
alertname: SameAlertNameWithDifferentGroup
|
||||
exp_alerts: []
|
||||
- eval_time: 6m
|
||||
groupname: group1
|
||||
alertname: AlertWithTemplate
|
||||
exp_alerts:
|
||||
- exp_labels:
|
||||
cluster: prod
|
||||
exp_annotations:
|
||||
queryAnno: '1'
|
||||
|
||||
metricsql_expr_test:
|
||||
- expr: test
|
||||
@@ -50,6 +60,8 @@ tests:
|
||||
values: "0+0x1440"
|
||||
- series: "test"
|
||||
values: "0+1x1440"
|
||||
- series: "foo"
|
||||
values: "1x20"
|
||||
|
||||
metricsql_expr_test:
|
||||
- expr: count(ALERTS) by (alertgroup, alertname, alertstate)
|
||||
@@ -59,6 +71,8 @@ tests:
|
||||
value: 1
|
||||
- labels: '{alertgroup="group1", alertname="InstanceDown", alertstate="pending"}'
|
||||
value: 1
|
||||
- labels: '{alertgroup="group1", alertname="AlertWithTemplate", alertstate="firing"}'
|
||||
value: 1
|
||||
- expr: t1
|
||||
eval_time: 4m
|
||||
exp_samples:
|
||||
|
||||
@@ -366,6 +366,7 @@ func (tg *testGroup) test(evalInterval time.Duration, groupOrderMap map[string]i
|
||||
mergedExternalLabels[k] = v
|
||||
}
|
||||
ng := rule.NewGroup(group, q, time.Minute, mergedExternalLabels)
|
||||
ng.Init()
|
||||
groups = append(groups, ng)
|
||||
}
|
||||
|
||||
|
||||
@@ -295,10 +295,7 @@ func parse(files map[string][]byte, validateTplFn ValidateTplFn, validateExpress
|
||||
}
|
||||
|
||||
func parseConfig(data []byte) ([]Group, error) {
|
||||
data, err := envtemplate.ReplaceBytes(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot expand environment vars: %w", err)
|
||||
}
|
||||
data = envtemplate.ReplaceBytes(data)
|
||||
|
||||
var result []Group
|
||||
type cfgFile struct {
|
||||
@@ -310,13 +307,13 @@ func parseConfig(data []byte) ([]Group, error) {
|
||||
decoder := yaml.NewDecoder(bytes.NewReader(data))
|
||||
for {
|
||||
var cf cfgFile
|
||||
if err = decoder.Decode(&cf); err != nil {
|
||||
if err := decoder.Decode(&cf); err != nil {
|
||||
if err == io.EOF { // EOF indicates no more documents to read
|
||||
break
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if err = checkOverflow(cf.XXX, "config"); err != nil {
|
||||
if err := checkOverflow(cf.XXX, "config"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, cf.Groups...)
|
||||
|
||||
@@ -29,6 +29,18 @@ type manager struct {
|
||||
groups map[uint64]*rule.Group
|
||||
}
|
||||
|
||||
// groupAPI generates apiGroup object from group by its ID(hash)
|
||||
func (m *manager) groupAPI(gID uint64) (*apiGroup, error) {
|
||||
m.groupsMu.RLock()
|
||||
defer m.groupsMu.RUnlock()
|
||||
|
||||
g, ok := m.groups[gID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("can't find group with id %d", gID)
|
||||
}
|
||||
return groupToAPI(g), nil
|
||||
}
|
||||
|
||||
// ruleAPI generates apiRule object from alert by its ID(hash)
|
||||
func (m *manager) ruleAPI(gID, rID uint64) (apiRule, error) {
|
||||
m.groupsMu.RLock()
|
||||
|
||||
@@ -22,10 +22,11 @@ import (
|
||||
// AlertManager represents integration provider with Prometheus alert manager
|
||||
// https://github.com/prometheus/alertmanager
|
||||
type AlertManager struct {
|
||||
addr *url.URL
|
||||
argFunc AlertURLGenerator
|
||||
client *http.Client
|
||||
timeout time.Duration
|
||||
addr *url.URL
|
||||
argFunc AlertURLGenerator
|
||||
client *http.Client
|
||||
timeout time.Duration
|
||||
lastError string
|
||||
|
||||
authCfg *promauth.Config
|
||||
// stores already parsed RelabelConfigs object
|
||||
@@ -71,6 +72,10 @@ func (am AlertManager) Addr() string {
|
||||
return am.addr.Redacted()
|
||||
}
|
||||
|
||||
func (am *AlertManager) LastError() string {
|
||||
return am.lastError
|
||||
}
|
||||
|
||||
// Send an alert or resolve message
|
||||
func (am *AlertManager) Send(ctx context.Context, alerts []Alert, headers map[string]string) error {
|
||||
am.metrics.alertsSent.Add(len(alerts))
|
||||
@@ -79,6 +84,9 @@ func (am *AlertManager) Send(ctx context.Context, alerts []Alert, headers map[st
|
||||
am.metrics.alertsSendDuration.UpdateDuration(startTime)
|
||||
if err != nil {
|
||||
am.metrics.alertsSendErrors.Add(len(alerts))
|
||||
am.lastError = err.Error()
|
||||
} else {
|
||||
am.lastError = ""
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -18,6 +18,11 @@ type FakeNotifier struct {
|
||||
// Close does nothing
|
||||
func (*FakeNotifier) Close() {}
|
||||
|
||||
// LastError returns last error message
|
||||
func (*FakeNotifier) LastError() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Addr returns ""
|
||||
func (*FakeNotifier) Addr() string { return "" }
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ type Notifier interface {
|
||||
Send(ctx context.Context, alerts []Alert, notifierHeaders map[string]string) error
|
||||
// Addr returns address where alerts are sent.
|
||||
Addr() string
|
||||
// LastError returns error, that occured during last attempt to send data
|
||||
LastError() string
|
||||
// Close is a destructor for the Notifier
|
||||
Close()
|
||||
}
|
||||
|
||||
@@ -25,6 +25,11 @@ func (bh *blackHoleNotifier) Close() {
|
||||
bh.metrics.close()
|
||||
}
|
||||
|
||||
// LastError return last notifier's error
|
||||
func (bh *blackHoleNotifier) LastError() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// newBlackHoleNotifier creates a new blackHoleNotifier
|
||||
func newBlackHoleNotifier() *blackHoleNotifier {
|
||||
address := "blackhole"
|
||||
|
||||
@@ -182,7 +182,7 @@ func (rw *rwServer) handler(w http.ResponseWriter, r *http.Request) {
|
||||
rw.err(w, fmt.Errorf("decode err: %w", err))
|
||||
return
|
||||
}
|
||||
wru := &prompb.WriteRequestUnmarshaller{}
|
||||
wru := &prompb.WriteRequestUnmarshaler{}
|
||||
wr, err := wru.UnmarshalProtobuf(b)
|
||||
if err != nil {
|
||||
rw.err(w, fmt.Errorf("unmarhsal err: %w", err))
|
||||
|
||||
@@ -28,8 +28,8 @@ var (
|
||||
"Defines how many retries to make before giving up on rule if request for it returns an error.")
|
||||
disableProgressBar = flag.Bool("replay.disableProgressBar", false, "Whether to disable rendering progress bars during the replay. "+
|
||||
"Progress bar rendering might be verbose or break the logs parsing, so it is recommended to be disabled when not used in interactive mode.")
|
||||
ruleEvaluationConcurrency = flag.Int("replay.ruleEvaluationConcurrency", 1, "The maximum number of concurrent `/query_range` requests for a single rule. "+
|
||||
"Increasing this value when replaying for a long time and a single request range is limited by `-replay.maxDatapointsPerQuery`.")
|
||||
ruleEvaluationConcurrency = flag.Int("replay.ruleEvaluationConcurrency", 1, "The maximum number of concurrent '/query_range' requests when replay recording rule or alerting rule with for=0. "+
|
||||
"Increasing this value when replaying for a long time, since each request is limited by -replay.maxDatapointsPerQuery.")
|
||||
)
|
||||
|
||||
func replay(groupsCfg []config.Group, qb datasource.QuerierBuilder, rw remotewrite.RWClient) (totalRows, droppedRows int, err error) {
|
||||
|
||||
@@ -246,24 +246,33 @@ func TestReplay(t *testing.T) {
|
||||
|
||||
// multiple rules + rule concurrency + group concurrency
|
||||
f("2021-01-01T12:00:00.000Z", "2021-01-01T12:02:30.000Z", 1, 3, 0, []config.Group{
|
||||
{Rules: []config.Rule{{Alert: "foo-group-single-concurrent", Expr: "sum(up) > 1"}, {Alert: "bar-group-single-concurrent", Expr: "max(up) < 1"}}, Concurrency: 2}}, &fakeReplayQuerier{
|
||||
{Rules: []config.Rule{{Alert: "foo-group-single-concurrent", For: promutil.NewDuration(30 * time.Second), Expr: "sum(up) > 1"}, {Alert: "bar-group-single-concurrent", Expr: "max(up) < 1"}}, Concurrency: 2}}, &fakeReplayQuerier{
|
||||
registry: map[string]map[string][]datasource.Metric{
|
||||
"sum(up) > 1": {
|
||||
"12:00:00+12:01:00": {},
|
||||
"12:01:00+12:02:00": {{
|
||||
Timestamps: []int64{1},
|
||||
"12:00:00+12:01:00": {{
|
||||
Timestamps: []int64{1609502460},
|
||||
Values: []float64{1},
|
||||
}},
|
||||
"12:01:00+12:02:00": {{
|
||||
Timestamps: []int64{1609502520},
|
||||
Values: []float64{1},
|
||||
}},
|
||||
"12:02:00+12:02:30": {{
|
||||
Timestamps: []int64{1609502580},
|
||||
Values: []float64{1},
|
||||
}},
|
||||
"12:02:00+12:02:30": {},
|
||||
},
|
||||
"max(up) < 1": {
|
||||
"12:00:00+12:01:00": {},
|
||||
"12:00:00+12:01:00": {{
|
||||
Timestamps: []int64{1609502460},
|
||||
Values: []float64{1},
|
||||
}},
|
||||
"12:01:00+12:02:00": {{
|
||||
Timestamps: []int64{1},
|
||||
Timestamps: []int64{1609502520},
|
||||
Values: []float64{1},
|
||||
}},
|
||||
"12:02:00+12:02:30": {},
|
||||
},
|
||||
},
|
||||
}, 4)
|
||||
}, 10)
|
||||
}
|
||||
|
||||
@@ -341,11 +341,15 @@ func (ar *AlertingRule) execRange(ctx context.Context, start, end time.Time) ([]
|
||||
return []datasource.Metric{{Timestamps: []int64{0}, Values: []float64{math.NaN()}}}, nil
|
||||
}
|
||||
for _, s := range res.Data {
|
||||
ls, as, err := ar.expandTemplates(s, qFn, time.Time{})
|
||||
ls, err := ar.expandLabelTemplates(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to expand templates: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
alertID := hash(ls.processed)
|
||||
as, err := ar.expandAnnotationTemplates(s, qFn, time.Time{}, ls)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a := ar.newAlert(s, time.Time{}, ls.processed, as) // initial alert
|
||||
|
||||
prevT := time.Time{}
|
||||
@@ -363,7 +367,7 @@ func (ar *AlertingRule) execRange(ctx context.Context, start, end time.Time) ([]
|
||||
a.State = notifier.StatePending
|
||||
a.ActiveAt = at
|
||||
// re-template the annotations as active timestamp is changed
|
||||
_, a.Annotations, _ = ar.expandTemplates(s, qFn, at)
|
||||
a.Annotations, _ = ar.expandAnnotationTemplates(s, qFn, at, ls)
|
||||
a.Start = time.Time{}
|
||||
} else if at.Sub(a.ActiveAt) >= ar.For && a.State != notifier.StateFiring {
|
||||
a.State = notifier.StateFiring
|
||||
@@ -376,13 +380,15 @@ func (ar *AlertingRule) execRange(ctx context.Context, start, end time.Time) ([]
|
||||
}
|
||||
result = append(result, ar.alertToTimeSeries(a, s.Timestamps[i])...)
|
||||
|
||||
// save alert's state on last iteration, so it can be used on the next execRange call
|
||||
if at.Equal(end) {
|
||||
// if for>0, save alert's state on last iteration, so it can be used on the next execRange call
|
||||
if ar.For > 0 && at.Equal(end) {
|
||||
holdAlertState[alertID] = a
|
||||
}
|
||||
}
|
||||
}
|
||||
ar.alerts = holdAlertState
|
||||
if len(holdAlertState) > 0 {
|
||||
ar.alerts = holdAlertState
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -428,9 +434,22 @@ func (ar *AlertingRule) exec(ctx context.Context, ts time.Time, limit int) ([]pr
|
||||
expandedLabels := make([]*labelSet, len(res.Data))
|
||||
expandedAnnotations := make([]map[string]string, len(res.Data))
|
||||
for i, m := range res.Data {
|
||||
ls, as, err := ar.expandTemplates(m, qFn, ts)
|
||||
ls, err := ar.expandLabelTemplates(m)
|
||||
if err != nil {
|
||||
curState.Err = fmt.Errorf("failed to expand templates: %w", err)
|
||||
curState.Err = err
|
||||
return nil, curState.Err
|
||||
}
|
||||
at := ts
|
||||
alertID := hash(ls.processed)
|
||||
if a, ok := ar.alerts[alertID]; ok {
|
||||
// modify activeAt for annotation templating if the alert has already triggered(in state Pending or Firing)
|
||||
if a.State != notifier.StateInactive {
|
||||
at = a.ActiveAt
|
||||
}
|
||||
}
|
||||
as, err := ar.expandAnnotationTemplates(m, qFn, at, ls)
|
||||
if err != nil {
|
||||
curState.Err = err
|
||||
return nil, curState.Err
|
||||
}
|
||||
expandedLabels[i] = ls
|
||||
@@ -473,6 +492,7 @@ func (ar *AlertingRule) exec(ctx context.Context, ts time.Time, limit int) ([]pr
|
||||
a.KeepFiringSince = time.Time{}
|
||||
continue
|
||||
}
|
||||
|
||||
a := ar.newAlert(m, ts, labels.processed, annotations)
|
||||
a.ID = alertID
|
||||
a.State = notifier.StatePending
|
||||
@@ -536,12 +556,18 @@ func (ar *AlertingRule) exec(ctx context.Context, ts time.Time, limit int) ([]pr
|
||||
return append(tss, ar.toTimeSeries(ts.Unix())...), nil
|
||||
}
|
||||
|
||||
func (ar *AlertingRule) expandTemplates(m datasource.Metric, qFn templates.QueryFn, ts time.Time) (*labelSet, map[string]string, error) {
|
||||
func (ar *AlertingRule) expandLabelTemplates(m datasource.Metric) (*labelSet, error) {
|
||||
qFn := func(_ string) ([]datasource.Metric, error) {
|
||||
return nil, fmt.Errorf("`query` template isn't supported in rule label")
|
||||
}
|
||||
ls, err := ar.toLabels(m, qFn)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to expand labels: %w", err)
|
||||
return nil, fmt.Errorf("failed to expand label templates: %s", err)
|
||||
}
|
||||
return ls, nil
|
||||
}
|
||||
|
||||
func (ar *AlertingRule) expandAnnotationTemplates(m datasource.Metric, qFn templates.QueryFn, activeAt time.Time, ls *labelSet) (map[string]string, error) {
|
||||
tplData := notifier.AlertTplData{
|
||||
Value: m.Values[0],
|
||||
Type: ar.Type.String(),
|
||||
@@ -549,14 +575,14 @@ func (ar *AlertingRule) expandTemplates(m datasource.Metric, qFn templates.Query
|
||||
Expr: ar.Expr,
|
||||
AlertID: hash(ls.processed),
|
||||
GroupID: ar.GroupID,
|
||||
ActiveAt: ts,
|
||||
ActiveAt: activeAt,
|
||||
For: ar.For,
|
||||
}
|
||||
as, err := notifier.ExecTemplate(qFn, ar.Annotations, tplData)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to template annotations: %w", err)
|
||||
return nil, fmt.Errorf("failed to expand annotation templates: %s", err)
|
||||
}
|
||||
return ls, as, nil
|
||||
return as, nil
|
||||
}
|
||||
|
||||
// toTimeSeries creates `ALERTS` and `ALERTS_FOR_STATE` for active alerts
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -267,8 +268,15 @@ func TestAlertingRule_Exec(t *testing.T) {
|
||||
if got.State != exp.State {
|
||||
t.Fatalf("evalIndex %d: expected state %d; got %d", i, exp.State, got.State)
|
||||
}
|
||||
if rule.Annotations != nil && exp.Annotations != nil {
|
||||
if !reflect.DeepEqual(got.Annotations, exp.Annotations) {
|
||||
t.Fatalf("evalIndex %d: expected annotations %v; got %v", i, exp.Annotations, got.Annotations)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// reset ts for next test
|
||||
ts, _ = time.Parse(time.RFC3339, "2024-10-29T00:00:00Z")
|
||||
}
|
||||
|
||||
f(newTestAlertingRule("empty", 0), [][]datasource.Metric{}, nil, nil)
|
||||
@@ -522,7 +530,7 @@ func TestAlertingRule_Exec(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
f(newTestAlertingRule("for-pending=>firing=>inactive=>pending=>firing", defaultStep), [][]datasource.Metric{
|
||||
f(newTestAlertingRuleWithCustomFields("for-pending=>firing=>inactive=>pending=>firing", defaultStep, 0, 0, map[string]string{"activeAt": "{{ $activeAt.UnixMilli }}"}), [][]datasource.Metric{
|
||||
{metricWithLabels(t, "name", "foo")},
|
||||
{metricWithLabels(t, "name", "foo")},
|
||||
// empty step to set alert inactive
|
||||
@@ -530,11 +538,11 @@ func TestAlertingRule_Exec(t *testing.T) {
|
||||
{metricWithLabels(t, "name", "foo")},
|
||||
{metricWithLabels(t, "name", "foo")},
|
||||
}, map[int][]testAlert{
|
||||
0: {{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StatePending}}},
|
||||
1: {{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StateFiring}}},
|
||||
2: {{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StateInactive}}},
|
||||
3: {{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StatePending}}},
|
||||
4: {{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StateFiring}}},
|
||||
0: {{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StatePending, Annotations: map[string]string{"activeAt": strconv.FormatInt(ts.UnixMilli(), 10)}}}},
|
||||
1: {{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StateFiring, Annotations: map[string]string{"activeAt": strconv.FormatInt(ts.UnixMilli(), 10)}}}},
|
||||
2: {{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StateInactive, Annotations: map[string]string{"activeAt": strconv.FormatInt(ts.UnixMilli(), 10)}}}},
|
||||
3: {{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StatePending, Annotations: map[string]string{"activeAt": strconv.FormatInt(ts.Add(defaultStep*3).UnixMilli(), 10)}}}},
|
||||
4: {{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StateFiring, Annotations: map[string]string{"activeAt": strconv.FormatInt(ts.Add(defaultStep*3).UnixMilli(), 10)}}}},
|
||||
}, nil)
|
||||
|
||||
f(newTestAlertingRuleWithCustomFields("for-pending=>firing=>keepfiring=>firing", defaultStep, 0, defaultStep, nil), [][]datasource.Metric{
|
||||
|
||||
@@ -587,6 +587,11 @@ func (g *Group) Replay(start, end time.Time, rw remotewrite.RWClient, maxDataPoi
|
||||
|
||||
func replayRuleRange(r Rule, ri rangeIterator, bar *pb.ProgressBar, rw remotewrite.RWClient, replayRuleRetryAttempts, ruleEvaluationConcurrency int) int {
|
||||
fmt.Printf("> Rule %q (ID: %d)\n", r, r.ID())
|
||||
// alerting rule with for>0 can't be replayed concurrently, since the status change might depend on the previous evaluation
|
||||
// see https://github.com/VictoriaMetrics/VictoriaMetrics/commit/abcb21aa5ee918ba9a4e9cde495dba06e1e9564c
|
||||
if r, ok := r.(*AlertingRule); ok && r.For > 0 {
|
||||
ruleEvaluationConcurrency = 1
|
||||
}
|
||||
sem := make(chan struct{}, ruleEvaluationConcurrency)
|
||||
wg := sync.WaitGroup{}
|
||||
res := make(chan int, int(ri.end.Sub(ri.start)/ri.step)+1)
|
||||
|
||||
@@ -437,7 +437,7 @@ func TestRecordingRuleExec_Negative(t *testing.T) {
|
||||
|
||||
_, err = rr.exec(context.TODO(), time.Now(), 0)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot execute recroding rule: %s", err)
|
||||
t.Fatalf("cannot execute recording rule: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ var (
|
||||
{"api/v1/alerts", "list all active alerts"},
|
||||
{"api/v1/notifiers", "list all notifiers"},
|
||||
{fmt.Sprintf("api/v1/alert?%s=<int>&%s=<int>", paramGroupID, paramAlertID), "get alert status by group and alert ID"},
|
||||
{fmt.Sprintf("api/v1/rule?%s=<int>&%s=<int>", paramGroupID, paramRuleID), "get rule status by group and rule ID"},
|
||||
{fmt.Sprintf("api/v1/group?%s=<int>", paramGroupID), "get group status by group ID"},
|
||||
}
|
||||
systemLinks = [][2]string{
|
||||
{"vmalert/groups", "UI"},
|
||||
@@ -195,6 +197,20 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(data)
|
||||
return true
|
||||
case "/vmalert/api/v1/group", "/api/v1/group":
|
||||
group, err := rh.getGroup(r)
|
||||
if err != nil {
|
||||
httpserver.Errorf(w, r, "%s", err)
|
||||
return true
|
||||
}
|
||||
data, err := json.Marshal(group)
|
||||
if err != nil {
|
||||
httpserver.Errorf(w, r, "failed to marshal group: %s", err)
|
||||
return true
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(data)
|
||||
return true
|
||||
case "/-/reload":
|
||||
if !httpserver.CheckAuthFlag(w, r, reloadAuthKey) {
|
||||
return true
|
||||
@@ -209,6 +225,18 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func (rh *requestHandler) getGroup(r *http.Request) (*apiGroup, error) {
|
||||
groupID, err := strconv.ParseUint(r.FormValue(paramGroupID), 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read %q param: %w", paramGroupID, err)
|
||||
}
|
||||
obj, err := rh.m.groupAPI(groupID)
|
||||
if err != nil {
|
||||
return nil, errResponse(err, http.StatusNotFound)
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func (rh *requestHandler) getRule(r *http.Request) (apiRule, error) {
|
||||
groupID, err := strconv.ParseUint(r.FormValue(paramGroupID), 10, 64)
|
||||
if err != nil {
|
||||
@@ -337,12 +365,12 @@ func (rh *requestHandler) groups(rf *rulesFilter) []*apiGroup {
|
||||
rule.Alerts = nil
|
||||
}
|
||||
if rule.LastError != "" {
|
||||
g.Unhealthy++
|
||||
g.unhealthy++
|
||||
} else {
|
||||
g.Healthy++
|
||||
g.healthy++
|
||||
}
|
||||
if isNoMatch(rule) {
|
||||
g.NoMatch++
|
||||
g.noMatch++
|
||||
}
|
||||
filteredRules = append(filteredRules, rule)
|
||||
}
|
||||
@@ -459,8 +487,9 @@ func (rh *requestHandler) listNotifiers() ([]byte, error) {
|
||||
}
|
||||
for _, target := range protoTargets {
|
||||
notifier.Targets = append(notifier.Targets, &apiTarget{
|
||||
Address: target.Addr(),
|
||||
Labels: target.Labels.ToMap(),
|
||||
Address: target.Addr(),
|
||||
Labels: target.Labels.ToMap(),
|
||||
LastError: target.LastError(),
|
||||
})
|
||||
}
|
||||
lr.Data.Notifiers = append(lr.Data.Notifiers, notifier)
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
{%= Controls(prefix, currentIcon, currentText, icons, filters, true) %}
|
||||
{% if len(groups) > 0 %}
|
||||
{% for _, g := range groups %}
|
||||
<div id="group-{%s g.ID %}" class="d-flex w-100 border-0 flex-column group-items{% if g.Unhealthy > 0 %} alert-danger{% endif %}">
|
||||
<div id="group-{%s g.ID %}" class="d-flex w-100 border-0 flex-column group-items{% if g.unhealthy > 0 %} alert-danger{% endif %}">
|
||||
<span class="d-flex justify-content-between">
|
||||
<a href="#group-{%s g.ID %}">{%s g.Name %}{% if g.Type != "prometheus" %} ({%s g.Type %}){% endif %} (every {%f.0 g.Interval %}s) #</a>
|
||||
<span
|
||||
@@ -123,9 +123,9 @@
|
||||
data-bs-target="#sub-{%s g.ID %}"
|
||||
>
|
||||
<span class="d-flex gap-2">
|
||||
{% if g.Unhealthy > 0 %}<span class="badge bg-danger" title="Number of rules with status Error">{%d g.Unhealthy %}</span> {% endif %}
|
||||
{% if g.NoMatch > 0 %}<span class="badge bg-warning" title="Number of rules with status NoMatch">{%d g.NoMatch %}</span> {% endif %}
|
||||
<span class="badge bg-success" title="Number of rules with status Ok">{%d g.Healthy %}</span>
|
||||
{% if g.unhealthy > 0 %}<span class="badge bg-danger" title="Number of rules with status Error">{%d g.unhealthy %}</span> {% endif %}
|
||||
{% if g.noMatch > 0 %}<span class="badge bg-warning" title="Number of rules with status NoMatch">{%d g.noMatch %}</span> {% endif %}
|
||||
<span class="badge bg-success" title="Number of rules with status Ok">{%d g.healthy %}</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -363,7 +363,7 @@ func StreamListGroups(qw422016 *qt422016.Writer, r *http.Request, groups []*apiG
|
||||
//line app/vmalert/web.qtpl:116
|
||||
qw422016.N().S(`" class="d-flex w-100 border-0 flex-column group-items`)
|
||||
//line app/vmalert/web.qtpl:116
|
||||
if g.Unhealthy > 0 {
|
||||
if g.unhealthy > 0 {
|
||||
//line app/vmalert/web.qtpl:116
|
||||
qw422016.N().S(` alert-danger`)
|
||||
//line app/vmalert/web.qtpl:116
|
||||
@@ -407,11 +407,11 @@ func StreamListGroups(qw422016 *qt422016.Writer, r *http.Request, groups []*apiG
|
||||
<span class="d-flex gap-2">
|
||||
`)
|
||||
//line app/vmalert/web.qtpl:126
|
||||
if g.Unhealthy > 0 {
|
||||
if g.unhealthy > 0 {
|
||||
//line app/vmalert/web.qtpl:126
|
||||
qw422016.N().S(`<span class="badge bg-danger" title="Number of rules with status Error">`)
|
||||
//line app/vmalert/web.qtpl:126
|
||||
qw422016.N().D(g.Unhealthy)
|
||||
qw422016.N().D(g.unhealthy)
|
||||
//line app/vmalert/web.qtpl:126
|
||||
qw422016.N().S(`</span> `)
|
||||
//line app/vmalert/web.qtpl:126
|
||||
@@ -420,11 +420,11 @@ func StreamListGroups(qw422016 *qt422016.Writer, r *http.Request, groups []*apiG
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line app/vmalert/web.qtpl:127
|
||||
if g.NoMatch > 0 {
|
||||
if g.noMatch > 0 {
|
||||
//line app/vmalert/web.qtpl:127
|
||||
qw422016.N().S(`<span class="badge bg-warning" title="Number of rules with status NoMatch">`)
|
||||
//line app/vmalert/web.qtpl:127
|
||||
qw422016.N().D(g.NoMatch)
|
||||
qw422016.N().D(g.noMatch)
|
||||
//line app/vmalert/web.qtpl:127
|
||||
qw422016.N().S(`</span> `)
|
||||
//line app/vmalert/web.qtpl:127
|
||||
@@ -433,7 +433,7 @@ func StreamListGroups(qw422016 *qt422016.Writer, r *http.Request, groups []*apiG
|
||||
qw422016.N().S(`
|
||||
<span class="badge bg-success" title="Number of rules with status Ok">`)
|
||||
//line app/vmalert/web.qtpl:128
|
||||
qw422016.N().D(g.Healthy)
|
||||
qw422016.N().D(g.healthy)
|
||||
//line app/vmalert/web.qtpl:128
|
||||
qw422016.N().S(`</span>
|
||||
</span>
|
||||
|
||||
@@ -25,6 +25,7 @@ func TestHandler(t *testing.T) {
|
||||
m := &manager{groups: map[uint64]*rule.Group{}}
|
||||
var ar *rule.AlertingRule
|
||||
var rr *rule.RecordingRule
|
||||
var groupIDs []uint64
|
||||
for _, dsType := range []string{"prometheus", "", "graphite"} {
|
||||
g := rule.NewGroup(config.Group{
|
||||
Name: "group",
|
||||
@@ -45,7 +46,9 @@ func TestHandler(t *testing.T) {
|
||||
ar = g.Rules[0].(*rule.AlertingRule)
|
||||
rr = g.Rules[1].(*rule.RecordingRule)
|
||||
g.ExecOnce(context.Background(), func() []notifier.Notifier { return nil }, nil, time.Time{})
|
||||
m.groups[g.CreateID()] = g
|
||||
id := g.CreateID()
|
||||
m.groups[id] = g
|
||||
groupIDs = append(groupIDs, id)
|
||||
}
|
||||
rh := &requestHandler{m: m}
|
||||
|
||||
@@ -188,6 +191,21 @@ func TestHandler(t *testing.T) {
|
||||
t.Fatalf("expected %+v to have state updates field not empty", gotRuleWithUpdates.StateUpdates)
|
||||
}
|
||||
})
|
||||
t.Run("/api/v1/group?groupID", func(t *testing.T) {
|
||||
id := groupIDs[0]
|
||||
g := m.groups[id]
|
||||
expGroup := groupToAPI(g)
|
||||
gotGroup := apiGroup{}
|
||||
getResp(t, ts.URL+"/"+expGroup.APILink(), &gotGroup, 200)
|
||||
if expGroup.ID != gotGroup.ID {
|
||||
t.Fatalf("expected to get Group %q; got %q instead", expGroup.ID, gotGroup.ID)
|
||||
}
|
||||
gotGroup = apiGroup{}
|
||||
getResp(t, ts.URL+"/vmalert/"+expGroup.APILink(), &gotGroup, 200)
|
||||
if expGroup.ID != gotGroup.ID {
|
||||
t.Fatalf("expected to get Group %q; got %q instead", expGroup.ID, gotGroup.ID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("/api/v1/rules&filters", func(t *testing.T) {
|
||||
check := func(url string, statusCode, expGroups, expRules int) {
|
||||
|
||||
@@ -28,6 +28,8 @@ type apiNotifier struct {
|
||||
type apiTarget struct {
|
||||
Address string `json:"address"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
// LastError contains the error faced while sending to notifier.
|
||||
LastError string `json:"lastError"`
|
||||
}
|
||||
|
||||
// apiAlert represents a notifier.AlertingRule state
|
||||
@@ -109,11 +111,16 @@ type apiGroup struct {
|
||||
// EvalDelay will adjust the `time` parameter of rule evaluation requests to compensate intentional query delay from datasource.
|
||||
EvalDelay float64 `json:"eval_delay,omitempty"`
|
||||
// Unhealthy unhealthy rules count
|
||||
Unhealthy int
|
||||
unhealthy int
|
||||
// Healthy passing rules count
|
||||
Healthy int
|
||||
healthy int
|
||||
// NoMatch not matching rules count
|
||||
NoMatch int
|
||||
noMatch int
|
||||
}
|
||||
|
||||
// APILink returns a link to the group's JSON representation.
|
||||
func (ag *apiGroup) APILink() string {
|
||||
return fmt.Sprintf("api/v1/group?%s=%s", paramGroupID, ag.ID)
|
||||
}
|
||||
|
||||
// groupAlerts represents a group of alerts for WEB view
|
||||
|
||||
@@ -723,14 +723,11 @@ func reloadAuthConfigData(data []byte) (bool, error) {
|
||||
}
|
||||
|
||||
func parseAuthConfig(data []byte) (*AuthConfig, error) {
|
||||
data, err := envtemplate.ReplaceBytes(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot expand environment vars: %w", err)
|
||||
}
|
||||
data = envtemplate.ReplaceBytes(data)
|
||||
ac := &AuthConfig{
|
||||
ms: metrics.NewSet(),
|
||||
}
|
||||
if err = yaml.UnmarshalStrict(data, ac); err != nil {
|
||||
if err := yaml.UnmarshalStrict(data, ac); err != nil {
|
||||
return nil, fmt.Errorf("cannot unmarshal AuthConfig data: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,106 +1,110 @@
|
||||
# All these commands must run from repository root.
|
||||
|
||||
# special tag to reduce resulting binary size
|
||||
# See this issue https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8008
|
||||
VMBACKUP_GO_BUILD_TAGS=disable_grpc_modules
|
||||
|
||||
vmbackup:
|
||||
APP_NAME=vmbackup $(MAKE) app-local
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-local
|
||||
|
||||
vmbackup-race:
|
||||
APP_NAME=vmbackup RACE=-race $(MAKE) app-local
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) RACE=-race $(MAKE) app-local
|
||||
|
||||
vmbackup-prod:
|
||||
APP_NAME=vmbackup $(MAKE) app-via-docker
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-via-docker
|
||||
|
||||
vmbackup-pure-prod:
|
||||
APP_NAME=vmbackup $(MAKE) app-via-docker-pure
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-via-docker-pure
|
||||
|
||||
vmbackup-linux-amd64-prod:
|
||||
APP_NAME=vmbackup $(MAKE) app-via-docker-linux-amd64
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-via-docker-linux-amd64
|
||||
|
||||
vmbackup-linux-arm-prod:
|
||||
APP_NAME=vmbackup $(MAKE) app-via-docker-linux-arm
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-via-docker-linux-arm
|
||||
|
||||
vmbackup-linux-arm64-prod:
|
||||
APP_NAME=vmbackup $(MAKE) app-via-docker-linux-arm64
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-via-docker-linux-arm64
|
||||
|
||||
vmbackup-linux-ppc64le-prod:
|
||||
APP_NAME=vmbackup $(MAKE) app-via-docker-linux-ppc64le
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-via-docker-linux-ppc64le
|
||||
|
||||
vmbackup-linux-386-prod:
|
||||
APP_NAME=vmbackup $(MAKE) app-via-docker-linux-386
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-via-docker-linux-386
|
||||
|
||||
vmbackup-darwin-amd64-prod:
|
||||
APP_NAME=vmbackup $(MAKE) app-via-docker-darwin-amd64
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-via-docker-darwin-amd64
|
||||
|
||||
vmbackup-darwin-arm64-prod:
|
||||
APP_NAME=vmbackup $(MAKE) app-via-docker-darwin-arm64
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-via-docker-darwin-arm64
|
||||
|
||||
vmbackup-freebsd-amd64-prod:
|
||||
APP_NAME=vmbackup $(MAKE) app-via-docker-freebsd-amd64
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-via-docker-freebsd-amd64
|
||||
|
||||
vmbackup-openbsd-amd64-prod:
|
||||
APP_NAME=vmbackup $(MAKE) app-via-docker-openbsd-amd64
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-via-docker-openbsd-amd64
|
||||
|
||||
vmbackup-windows-amd64-prod:
|
||||
APP_NAME=vmbackup $(MAKE) app-via-docker-windows-amd64
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-via-docker-windows-amd64
|
||||
|
||||
package-vmbackup:
|
||||
APP_NAME=vmbackup $(MAKE) package-via-docker
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) package-via-docker
|
||||
|
||||
package-vmbackup-pure:
|
||||
APP_NAME=vmbackup $(MAKE) package-via-docker-pure
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) package-via-docker-pure
|
||||
|
||||
package-vmbackup-amd64:
|
||||
APP_NAME=vmbackup $(MAKE) package-via-docker-amd64
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) package-via-docker-amd64
|
||||
|
||||
package-vmbackup-arm:
|
||||
APP_NAME=vmbackup $(MAKE) package-via-docker-arm
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) package-via-docker-arm
|
||||
|
||||
package-vmbackup-arm64:
|
||||
APP_NAME=vmbackup $(MAKE) package-via-docker-arm64
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) package-via-docker-arm64
|
||||
|
||||
package-vmbackup-ppc64le:
|
||||
APP_NAME=vmbackup $(MAKE) package-via-docker-ppc64le
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) package-via-docker-ppc64le
|
||||
|
||||
package-vmbackup-386:
|
||||
APP_NAME=vmbackup $(MAKE) package-via-docker-386
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) package-via-docker-386
|
||||
|
||||
publish-vmbackup:
|
||||
APP_NAME=vmbackup $(MAKE) publish-via-docker
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) publish-via-docker
|
||||
|
||||
vmbackup-linux-amd64:
|
||||
APP_NAME=vmbackup CGO_ENABLED=1 GOOS=linux GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) CGO_ENABLED=1 GOOS=linux GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmbackup-linux-arm:
|
||||
APP_NAME=vmbackup CGO_ENABLED=0 GOOS=linux GOARCH=arm $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=linux GOARCH=arm $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmbackup-linux-arm64:
|
||||
APP_NAME=vmbackup CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmbackup-linux-ppc64le:
|
||||
APP_NAME=vmbackup CGO_ENABLED=0 GOOS=linux GOARCH=ppc64le $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=linux GOARCH=ppc64le $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmbackup-linux-s390x:
|
||||
APP_NAME=vmbackup CGO_ENABLED=0 GOOS=linux GOARCH=s390x $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=linux GOARCH=s390x $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmbackup-linux-loong64:
|
||||
APP_NAME=vmbackup CGO_ENABLED=0 GOOS=linux GOARCH=loong64 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=linux GOARCH=loong64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmbackup-linux-386:
|
||||
APP_NAME=vmbackup CGO_ENABLED=0 GOOS=linux GOARCH=386 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=linux GOARCH=386 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmbackup-darwin-amd64:
|
||||
APP_NAME=vmbackup CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmbackup-darwin-arm64:
|
||||
APP_NAME=vmbackup CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmbackup-freebsd-amd64:
|
||||
APP_NAME=vmbackup CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmbackup-openbsd-amd64:
|
||||
APP_NAME=vmbackup CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmbackup-windows-amd64:
|
||||
GOARCH=amd64 APP_NAME=vmbackup $(MAKE) app-local-windows-goarch
|
||||
GOARCH=amd64 APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-local-windows-goarch
|
||||
|
||||
vmbackup-pure:
|
||||
APP_NAME=vmbackup $(MAKE) app-local-pure
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-local-pure
|
||||
|
||||
@@ -121,7 +121,7 @@ func (p *vmNativeProcessor) runSingle(ctx context.Context, f native.Filter, srcU
|
||||
pr := bar.NewProxyReader(reader)
|
||||
if pr != nil {
|
||||
reader = pr
|
||||
fmt.Printf("Continue import process with filter %s:\n", f.String())
|
||||
fmt.Fprintf(log.Writer(), "Continue import process with filter %s:\n", f.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ func (p *vmNativeProcessor) runBackfilling(ctx context.Context, tenantID string,
|
||||
initParams = []any{srcURL, dstURL, p.filter.String(), tenantID}
|
||||
}
|
||||
|
||||
fmt.Println("") // extra line for better output formatting
|
||||
fmt.Fprintln(log.Writer(), "") // extra line for better output formatting
|
||||
log.Printf(initMessage, initParams...)
|
||||
if len(ranges) > 1 {
|
||||
log.Printf("Selected time range will be split into %d ranges according to %q step", len(ranges), p.filter.Chunk)
|
||||
|
||||
@@ -33,7 +33,7 @@ func InsertHandler(req *http.Request) error {
|
||||
return fmt.Errorf("json encoding isn't supported for opentelemetry format. Use protobuf encoding")
|
||||
}
|
||||
}
|
||||
return stream.ParseStream(req.Body, encoding, processBody, func(tss []prompb.TimeSeries) error {
|
||||
return stream.ParseStream(req.Body, encoding, processBody, func(tss []prompb.TimeSeries, _ []prompb.MetricMetadata) error {
|
||||
return insertRows(tss, extraLabels)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/relabel"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus/stream"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
|
||||
@@ -29,7 +30,7 @@ func InsertHandler(req *http.Request) error {
|
||||
return err
|
||||
}
|
||||
encoding := req.Header.Get("Content-Encoding")
|
||||
return stream.Parse(req.Body, defaultTimestamp, encoding, true, func(rows []prometheus.Row) error {
|
||||
return stream.Parse(req.Body, defaultTimestamp, encoding, true, promscrape.IsMetadataEnabled(), func(rows []prometheus.Row, _ []prometheus.Metadata) error {
|
||||
return insertRows(rows, extraLabels)
|
||||
}, func(s string) {
|
||||
httpserver.LogError(req, s)
|
||||
|
||||
@@ -23,7 +23,7 @@ func InsertHandler(req *http.Request) error {
|
||||
return err
|
||||
}
|
||||
isVMRemoteWrite := req.Header.Get("Content-Encoding") == "zstd"
|
||||
return stream.Parse(req.Body, isVMRemoteWrite, func(tss []prompb.TimeSeries) error {
|
||||
return stream.Parse(req.Body, isVMRemoteWrite, func(tss []prompb.TimeSeries, _ []prompb.MetricMetadata) error {
|
||||
return insertRows(tss, extraLabels)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,106 +1,110 @@
|
||||
# All these commands must run from repository root.
|
||||
|
||||
# special tag to reduce resulting binary size
|
||||
# See this issue https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8008
|
||||
VMRESTORE_GO_BUILD_TAGS=disable_grpc_modules
|
||||
|
||||
vmrestore:
|
||||
APP_NAME=vmrestore $(MAKE) app-local
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-local
|
||||
|
||||
vmrestore-race:
|
||||
APP_NAME=vmrestore RACE=-race $(MAKE) app-local
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) RACE=-race $(MAKE) app-local
|
||||
|
||||
vmrestore-prod:
|
||||
APP_NAME=vmrestore $(MAKE) app-via-docker
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-via-docker
|
||||
|
||||
vmrestore-pure-prod:
|
||||
APP_NAME=vmrestore $(MAKE) app-via-docker-pure
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-via-docker-pure
|
||||
|
||||
vmrestore-linux-amd64-prod:
|
||||
APP_NAME=vmrestore $(MAKE) app-via-docker-linux-amd64
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-via-docker-linux-amd64
|
||||
|
||||
vmrestore-linux-arm-prod:
|
||||
APP_NAME=vmrestore $(MAKE) app-via-docker-linux-arm
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-via-docker-linux-arm
|
||||
|
||||
vmrestore-linux-arm64-prod:
|
||||
APP_NAME=vmrestore $(MAKE) app-via-docker-linux-arm64
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-via-docker-linux-arm64
|
||||
|
||||
vmrestore-linux-ppc64le-prod:
|
||||
APP_NAME=vmrestore $(MAKE) app-via-docker-linux-ppc64le
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-via-docker-linux-ppc64le
|
||||
|
||||
vmrestore-linux-386-prod:
|
||||
APP_NAME=vmrestore $(MAKE) app-via-docker-linux-386
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-via-docker-linux-386
|
||||
|
||||
vmrestore-darwin-amd64-prod:
|
||||
APP_NAME=vmrestore $(MAKE) app-via-docker-darwin-amd64
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-via-docker-darwin-amd64
|
||||
|
||||
vmrestore-darwin-arm64-prod:
|
||||
APP_NAME=vmrestore $(MAKE) app-via-docker-darwin-arm64
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-via-docker-darwin-arm64
|
||||
|
||||
vmrestore-freebsd-amd64-prod:
|
||||
APP_NAME=vmrestore $(MAKE) app-via-docker-freebsd-amd64
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-via-docker-freebsd-amd64
|
||||
|
||||
vmrestore-openbsd-amd64-prod:
|
||||
APP_NAME=vmrestore $(MAKE) app-via-docker-openbsd-amd64
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-via-docker-openbsd-amd64
|
||||
|
||||
vmrestore-windows-amd64-prod:
|
||||
APP_NAME=vmrestore $(MAKE) app-via-docker-windows-amd64
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-via-docker-windows-amd64
|
||||
|
||||
package-vmrestore:
|
||||
APP_NAME=vmrestore $(MAKE) package-via-docker
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) package-via-docker
|
||||
|
||||
package-vmrestore-pure:
|
||||
APP_NAME=vmrestore $(MAKE) package-via-docker-pure
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) package-via-docker-pure
|
||||
|
||||
package-vmrestore-amd64:
|
||||
APP_NAME=vmrestore $(MAKE) package-via-docker-amd64
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) package-via-docker-amd64
|
||||
|
||||
package-vmrestore-arm:
|
||||
APP_NAME=vmrestore $(MAKE) package-via-docker-arm
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) package-via-docker-arm
|
||||
|
||||
package-vmrestore-arm64:
|
||||
APP_NAME=vmrestore $(MAKE) package-via-docker-arm64
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) package-via-docker-arm64
|
||||
|
||||
package-vmrestore-ppc64le:
|
||||
APP_NAME=vmrestore $(MAKE) package-via-docker-ppc64le
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) package-via-docker-ppc64le
|
||||
|
||||
package-vmrestore-386:
|
||||
APP_NAME=vmrestore $(MAKE) package-via-docker-386
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) package-via-docker-386
|
||||
|
||||
publish-vmrestore:
|
||||
APP_NAME=vmrestore $(MAKE) publish-via-docker
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) publish-via-docker
|
||||
|
||||
vmrestore-linux-amd64:
|
||||
APP_NAME=vmrestore CGO_ENABLED=1 GOOS=linux GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) CGO_ENABLED=1 GOOS=linux GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmrestore-linux-arm:
|
||||
APP_NAME=vmrestore CGO_ENABLED=0 GOOS=linux GOARCH=arm $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=linux GOARCH=arm $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmrestore-linux-arm64:
|
||||
APP_NAME=vmrestore CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmrestore-linux-ppc64le:
|
||||
APP_NAME=vmrestore CGO_ENABLED=0 GOOS=linux GOARCH=ppc64le $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=linux GOARCH=ppc64le $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmrestore-linux-s390x:
|
||||
APP_NAME=vmrestore CGO_ENABLED=0 GOOS=linux GOARCH=s390x $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=linux GOARCH=s390x $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmrestore-linux-loong64:
|
||||
APP_NAME=vmrestore CGO_ENABLED=0 GOOS=linux GOARCH=loong64 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=linux GOARCH=loong64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmrestore-linux-386:
|
||||
APP_NAME=vmrestore CGO_ENABLED=0 GOOS=linux GOARCH=386 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=linux GOARCH=386 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmrestore-darwin-amd64:
|
||||
APP_NAME=vmrestore CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmrestore-darwin-arm64:
|
||||
APP_NAME=vmrestore CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmrestore-freebsd-amd64:
|
||||
APP_NAME=vmrestore CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmrestore-openbsd-amd64:
|
||||
APP_NAME=vmrestore CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmrestore-windows-amd64:
|
||||
GOARCH=amd64 APP_NAME=vmrestore $(MAKE) app-local-windows-goarch
|
||||
GOARCH=amd64 APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-local-windows-goarch
|
||||
|
||||
vmrestore-pure:
|
||||
APP_NAME=vmrestore $(MAKE) app-local-pure
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-local-pure
|
||||
|
||||
@@ -17,6 +17,9 @@ import (
|
||||
var maxGraphiteSeries = flag.Int("search.maxGraphiteSeries", 300e3, "The maximum number of time series, which can be scanned during queries to Graphite Render API. "+
|
||||
"See https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#render-api")
|
||||
|
||||
var maxGraphitePathExpressionLen = flag.Int("search.maxGraphitePathExpressionLen", 1024, "The maximum length of pathExpression field in Graphite series. "+
|
||||
"Longer expressions are truncated to prevent memory exhaustion on complex nested queries. Set to 0 to disable truncation.")
|
||||
|
||||
type evalConfig struct {
|
||||
startTime int64
|
||||
endTime int64
|
||||
@@ -53,6 +56,21 @@ func (ec *evalConfig) newTimestamps(step int64) []int64 {
|
||||
return timestamps
|
||||
}
|
||||
|
||||
// safePathExpression creates a pathExpression string from the given expression,
|
||||
// truncating it if it exceeds the maximum allowed length to prevent memory exhaustion.
|
||||
func safePathExpression(expr graphiteql.Expr) string {
|
||||
if expr == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
pathExpr := string(expr.AppendString(nil))
|
||||
maxLen := *maxGraphitePathExpressionLen
|
||||
if maxLen > 0 && len(pathExpr) > maxLen {
|
||||
return pathExpr[:maxLen] + "..."
|
||||
}
|
||||
return pathExpr
|
||||
}
|
||||
|
||||
type series struct {
|
||||
Name string
|
||||
Tags map[string]string
|
||||
@@ -169,7 +187,7 @@ func newNextSeriesForSearchQuery(ec *evalConfig, sq *storage.SearchQuery, expr g
|
||||
Timestamps: append([]int64{}, rs.Timestamps...),
|
||||
Values: append([]float64{}, rs.Values...),
|
||||
expr: expr,
|
||||
pathExpression: string(expr.AppendString(nil)),
|
||||
pathExpression: safePathExpression(expr),
|
||||
}
|
||||
s.summarize(aggrAvg, ec.startTime, ec.endTime, ec.storageStep, 0)
|
||||
t := timerpool.Get(30 * time.Second)
|
||||
|
||||
@@ -4178,3 +4178,170 @@ func formatTimestamps(tss []int64) string {
|
||||
fmt.Fprintf(&sb, " ]")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func TestSafePathExpression(t *testing.T) {
|
||||
// Save original value and restore after test
|
||||
originalMaxLen := *maxGraphitePathExpressionLen
|
||||
defer func() {
|
||||
*maxGraphitePathExpressionLen = originalMaxLen
|
||||
}()
|
||||
|
||||
t.Run("nil expression", func(t *testing.T) {
|
||||
result := safePathExpression(nil)
|
||||
if result != "" {
|
||||
t.Fatalf("expected empty string for nil expression, got: %q", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("short expression - no truncation", func(t *testing.T) {
|
||||
*maxGraphitePathExpressionLen = 50
|
||||
expr := &graphiteql.MetricExpr{Query: "metric.cpu.usage"}
|
||||
result := safePathExpression(expr)
|
||||
expected := "metric.cpu.usage"
|
||||
if result != expected {
|
||||
t.Fatalf("expected %q, got %q", expected, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("long expression - with truncation", func(t *testing.T) {
|
||||
*maxGraphitePathExpressionLen = 20
|
||||
longQuery := "vertica.metrics.fr4.verticamultitenant-eon.request_resource_consumption.very_long_metric_name"
|
||||
expr := &graphiteql.MetricExpr{Query: longQuery}
|
||||
result := safePathExpression(expr)
|
||||
expectedPrefix := longQuery[:20]
|
||||
expectedSuffix := "..."
|
||||
expected := expectedPrefix + expectedSuffix
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf("expected %q, got %q", expected, result)
|
||||
}
|
||||
if len(result) != 23 { // 20 + 3 for "..."
|
||||
t.Fatalf("expected result length 23, got %d", len(result))
|
||||
}
|
||||
if !strings.HasSuffix(result, "...") {
|
||||
t.Fatalf("expected result to end with '...', got %q", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("truncation disabled", func(t *testing.T) {
|
||||
*maxGraphitePathExpressionLen = 0 // Disable truncation
|
||||
longQuery := "very.long.metric.name.that.would.normally.be.truncated.but.should.not.be"
|
||||
expr := &graphiteql.MetricExpr{Query: longQuery}
|
||||
result := safePathExpression(expr)
|
||||
|
||||
if result != longQuery {
|
||||
t.Fatalf("expected full string %q when truncation disabled, got %q", longQuery, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("function expression", func(t *testing.T) {
|
||||
*maxGraphitePathExpressionLen = 30
|
||||
// Create a function expression: sum(metric.cpu.usage)
|
||||
args := []*graphiteql.ArgExpr{
|
||||
{Expr: &graphiteql.MetricExpr{Query: "metric.cpu.usage"}},
|
||||
}
|
||||
funcExpr := &graphiteql.FuncExpr{
|
||||
FuncName: "sum",
|
||||
Args: args,
|
||||
}
|
||||
|
||||
result := safePathExpression(funcExpr)
|
||||
expected := "sum(metric.cpu.usage)"
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf("expected %q, got %q", expected, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("complex nested function - truncated", func(t *testing.T) {
|
||||
*maxGraphitePathExpressionLen = 15
|
||||
// Create nested functions: sum(avg(metric.cpu.usage))
|
||||
innerArgs := []*graphiteql.ArgExpr{
|
||||
{Expr: &graphiteql.MetricExpr{Query: "metric.cpu.usage"}},
|
||||
}
|
||||
innerFunc := &graphiteql.FuncExpr{
|
||||
FuncName: "avg",
|
||||
Args: innerArgs,
|
||||
}
|
||||
outerArgs := []*graphiteql.ArgExpr{
|
||||
{Expr: innerFunc},
|
||||
}
|
||||
outerFunc := &graphiteql.FuncExpr{
|
||||
FuncName: "sum",
|
||||
Args: outerArgs,
|
||||
}
|
||||
|
||||
result := safePathExpression(outerFunc)
|
||||
|
||||
if len(result) != 18 { // 15 + 3 for "..."
|
||||
t.Fatalf("expected result length 18, got %d", len(result))
|
||||
}
|
||||
if !strings.HasSuffix(result, "...") {
|
||||
t.Fatalf("expected result to end with '...', got %q", result)
|
||||
}
|
||||
if !strings.HasPrefix(result, "sum(avg(metric") {
|
||||
t.Fatalf("expected result to start with 'sum(avg(metric', got %q", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("boundary case - exact length", func(t *testing.T) {
|
||||
*maxGraphitePathExpressionLen = 10
|
||||
expr := &graphiteql.MetricExpr{Query: "metric.cpu"} // Exactly 10 characters
|
||||
result := safePathExpression(expr)
|
||||
expected := "metric.cpu"
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf("expected %q, got %q", expected, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("boundary case - one character over", func(t *testing.T) {
|
||||
*maxGraphitePathExpressionLen = 10
|
||||
expr := &graphiteql.MetricExpr{Query: "metric.cpu.x"} // 11 characters
|
||||
result := safePathExpression(expr)
|
||||
expected := "metric.cpu..."
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf("expected %q, got %q", expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSafePathExpressionFromString(t *testing.T) {
|
||||
// Save original value and restore after test
|
||||
originalMaxLen := *maxGraphitePathExpressionLen
|
||||
defer func() {
|
||||
*maxGraphitePathExpressionLen = originalMaxLen
|
||||
}()
|
||||
|
||||
t.Run("short string - no truncation", func(t *testing.T) {
|
||||
*maxGraphitePathExpressionLen = 50
|
||||
input := "sumSeries(metric1,metric2)"
|
||||
result := safePathExpressionFromString(input)
|
||||
|
||||
if result != input {
|
||||
t.Fatalf("expected %q, got %q", input, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("long string - with truncation", func(t *testing.T) {
|
||||
*maxGraphitePathExpressionLen = 20
|
||||
input := "sumSeries(very.long.metric.name.that.exceeds.limit,another.metric)"
|
||||
result := safePathExpressionFromString(input)
|
||||
expected := "sumSeries(very.long...."
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf("expected %q, got %q", expected, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("truncation disabled", func(t *testing.T) {
|
||||
*maxGraphitePathExpressionLen = 0
|
||||
input := "very.long.string.that.would.normally.be.truncated"
|
||||
result := safePathExpressionFromString(input)
|
||||
|
||||
if result != input {
|
||||
t.Fatalf("expected full string when truncation disabled, got %q", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ func TestParseIntervalSuccess(t *testing.T) {
|
||||
t.Helper()
|
||||
interval, err := parseInterval(s)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error in parseInterva(%q): %s", s, err)
|
||||
t.Fatalf("unexpected error in parseInterval(%q): %s", s, err)
|
||||
}
|
||||
if interval != intervalExpected {
|
||||
t.Fatalf("unexpected result for parseInterval(%q); got %d; want %d", s, interval, intervalExpected)
|
||||
|
||||
@@ -17,6 +17,16 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup"
|
||||
)
|
||||
|
||||
// safePathExpressionFromString truncates a pathExpression string if it exceeds
|
||||
// the maximum allowed length to prevent memory exhaustion.
|
||||
func safePathExpressionFromString(pathExpr string) string {
|
||||
maxLen := *maxGraphitePathExpressionLen
|
||||
if maxLen > 0 && len(pathExpr) > maxLen {
|
||||
return pathExpr[:maxLen] + "..."
|
||||
}
|
||||
return pathExpr
|
||||
}
|
||||
|
||||
// nextSeriesFunc must return the next series to process.
|
||||
//
|
||||
// nextSeriesFunc must release all the occupied resources before returning non-nil error.
|
||||
@@ -319,7 +329,7 @@ func aggregateSeries(ec *evalConfig, expr graphiteql.Expr, nextSeries nextSeries
|
||||
Tags: tags,
|
||||
Timestamps: ec.newTimestamps(step),
|
||||
Values: as.Finalize(xFilesFactor),
|
||||
pathExpression: name,
|
||||
pathExpression: safePathExpressionFromString(name),
|
||||
expr: expr,
|
||||
step: step,
|
||||
}
|
||||
@@ -1124,7 +1134,7 @@ func constantLine(ec *evalConfig, expr graphiteql.Expr, n float64) nextSeriesFun
|
||||
Timestamps: []int64{ec.startTime, ec.startTime + step, ec.startTime + 2*step},
|
||||
Values: []float64{n, n, n},
|
||||
expr: expr,
|
||||
pathExpression: string(expr.AppendString(nil)),
|
||||
pathExpression: safePathExpression(expr),
|
||||
step: step,
|
||||
}
|
||||
return singleSeriesFunc(s)
|
||||
|
||||
@@ -17,7 +17,7 @@ func TestScanStringSuccess(t *testing.T) {
|
||||
t.Fatalf("unexpected string scanned from %s; got %s; want %s", s, result, sExpected)
|
||||
}
|
||||
if !strings.HasPrefix(s, result) {
|
||||
t.Fatalf("invalid prefix for scanne string %s: %s", s, result)
|
||||
t.Fatalf("invalid prefix for scanned string %s: %s", s, result)
|
||||
}
|
||||
}
|
||||
f(`""`, `""`)
|
||||
|
||||
@@ -210,7 +210,7 @@ func (p *parser) parseMetricExprOrFuncCall() (Expr, error) {
|
||||
}
|
||||
return fe, nil
|
||||
default:
|
||||
// Metric epxression or bool expression or None.
|
||||
// Metric expression or bool expression or None.
|
||||
if isBool(ident) {
|
||||
be := &BoolExpr{
|
||||
B: strings.EqualFold(ident, "true"),
|
||||
|
||||
@@ -269,7 +269,7 @@ func (rss *Results) runParallel(qt *querytracer.Tracer, f func(rs *Result, worke
|
||||
}
|
||||
|
||||
// Slow path - spin up multiple local workers for parallel data processing.
|
||||
// Do not use global workers pool, since it increases inter-CPU memory ping-poing,
|
||||
// Do not use global workers pool, since it increases inter-CPU memory ping-pong,
|
||||
// which reduces the scalability on systems with many CPU cores.
|
||||
|
||||
// Prepare the work for workers.
|
||||
@@ -485,7 +485,7 @@ func (pts *packedTimeseries) unpackTo(dst []*sortBlock, tbf *tmpBlocksFile, tr s
|
||||
}
|
||||
|
||||
// Slow path - spin up multiple local workers for parallel data unpacking.
|
||||
// Do not use global workers pool, since it increases inter-CPU memory ping-poing,
|
||||
// Do not use global workers pool, since it increases inter-CPU memory ping-pong,
|
||||
// which reduces the scalability on systems with many CPU cores.
|
||||
|
||||
// Prepare the work for workers.
|
||||
|
||||
@@ -135,7 +135,7 @@ func (tbf *tmpBlocksFile) WriteBlockRefData(b []byte) (tmpBlockAddr, error) {
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// Len() returnt tbf size in bytes.
|
||||
// Len() return tbf size in bytes.
|
||||
func (tbf *tmpBlocksFile) Len() uint64 {
|
||||
return tbf.offset
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ func newBinaryOpFunc(bf func(left, right float64, isBool bool) float64) binaryOp
|
||||
rightValues := right[i].Values
|
||||
dstValues := dst[i].Values
|
||||
if len(leftValues) != len(rightValues) || len(leftValues) != len(dstValues) {
|
||||
logger.Panicf("BUG: len(leftVaues) must match len(rightValues) and len(dstValues); got %d vs %d vs %d",
|
||||
logger.Panicf("BUG: len(leftValues) must match len(rightValues) and len(dstValues); got %d vs %d vs %d",
|
||||
len(leftValues), len(rightValues), len(dstValues))
|
||||
}
|
||||
for j, a := range leftValues {
|
||||
|
||||
@@ -55,7 +55,7 @@ func TestValidateMaxPointsPerSeriesFailure(t *testing.T) {
|
||||
f := func(start, end, step int64, maxPoints int) {
|
||||
t.Helper()
|
||||
if err := ValidateMaxPointsPerSeries(start, end, step, maxPoints); err == nil {
|
||||
t.Fatalf("expecint non-nil error for ValidateMaxPointsPerSeries(start=%d, end=%d, step=%d, maxPoints=%d)", start, end, step, maxPoints)
|
||||
t.Fatalf("expecting non-nil error for ValidateMaxPointsPerSeries(start=%d, end=%d, step=%d, maxPoints=%d)", start, end, step, maxPoints)
|
||||
}
|
||||
}
|
||||
// zero step
|
||||
|
||||
@@ -2443,13 +2443,14 @@ func rollupFake(_ *rollupFuncArg) float64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// getScalar expects result from a [scalar](https://prometheus.io/docs/prometheus/latest/querying/basics/#expression-language-data-types).
|
||||
func getScalar(arg any, argNum int) ([]float64, error) {
|
||||
ts, ok := arg.([]*timeseries)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`unexpected type for arg #%d; got %T; want %T`, argNum+1, arg, ts)
|
||||
return nil, fmt.Errorf(`arg #%d must be a scalar`, argNum+1)
|
||||
}
|
||||
if len(ts) != 1 {
|
||||
return nil, fmt.Errorf(`arg #%d must contain a single timeseries; got %d timeseries`, argNum+1, len(ts))
|
||||
return nil, fmt.Errorf(`arg #%d must be a scalar`, argNum+1)
|
||||
}
|
||||
return ts[0].Values, nil
|
||||
}
|
||||
@@ -2466,14 +2467,15 @@ func getIntNumber(arg any, argNum int) (int, error) {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// getString expects result from a string expression, which contains a single timeseries with only NaN values.
|
||||
func getString(tss []*timeseries, argNum int) (string, error) {
|
||||
if len(tss) != 1 {
|
||||
return "", fmt.Errorf(`arg #%d must contain a single timeseries; got %d timeseries`, argNum+1, len(tss))
|
||||
return "", fmt.Errorf(`arg #%d must be a string`, argNum+1)
|
||||
}
|
||||
ts := tss[0]
|
||||
for _, v := range ts.Values {
|
||||
if !math.IsNaN(v) {
|
||||
return "", fmt.Errorf(`arg #%d contains non-string timeseries`, argNum+1)
|
||||
return "", fmt.Errorf(`arg #%d must be a string`, argNum+1)
|
||||
}
|
||||
}
|
||||
return string(ts.MetricName.MetricGroup), nil
|
||||
|
||||
@@ -903,7 +903,6 @@ func transformHistogramQuantile(tfa *transformFuncArg) ([]*timeseries, error) {
|
||||
|
||||
// Convert buckets with `vmrange` labels to buckets with `le` labels.
|
||||
tss := vmrangeBucketsToLE(args[1])
|
||||
|
||||
// Parse boundsLabel. See https://github.com/prometheus/prometheus/issues/5706 for details.
|
||||
var boundsLabel string
|
||||
if len(args) > 2 {
|
||||
@@ -1050,9 +1049,15 @@ func fixBrokenBuckets(i int, xss []leTimeseries) {
|
||||
return
|
||||
}
|
||||
|
||||
vNext := xss[0].ts.Values[i]
|
||||
// Set the lowest bucket to 0 if its value is NaN, so it can be properly
|
||||
// compared with upper buckets in the loop below.
|
||||
if math.IsNaN(vNext) {
|
||||
vNext = 0
|
||||
xss[0].ts.Values[i] = vNext
|
||||
}
|
||||
// Substitute upper bucket values with lower bucket values if the upper values are NaN
|
||||
// or are bigger than the lower bucket values.
|
||||
vNext := xss[0].ts.Values[i]
|
||||
for j := 1; j < len(xss); j++ {
|
||||
v := xss[j].ts.Values[i]
|
||||
if math.IsNaN(v) || vNext > v {
|
||||
|
||||
@@ -37,6 +37,9 @@ func TestFixBrokenBuckets(t *testing.T) {
|
||||
f([]float64{5, 1, 2, 3, nan}, []float64{5, 5, 5, 5, 5})
|
||||
f([]float64{1, 5, 2, nan, 6, 3}, []float64{1, 5, 5, 5, 6, 6})
|
||||
f([]float64{5, 10, 4, 3}, []float64{5, 10, 10, 10})
|
||||
f([]float64{nan, 2, nan, 5}, []float64{0, 2, 2, 5})
|
||||
f([]float64{nan, nan, 4, 5}, []float64{0, 0, 4, 5})
|
||||
f([]float64{nan, nan, nan, 4}, []float64{0, 0, 0, 4})
|
||||
}
|
||||
|
||||
func TestFixBrokenBucketsMultipleValues(t *testing.T) {
|
||||
@@ -44,12 +47,11 @@ func TestFixBrokenBucketsMultipleValues(t *testing.T) {
|
||||
t.Helper()
|
||||
xss := make([]leTimeseries, len(values))
|
||||
for i, v := range values {
|
||||
|
||||
xss[i].ts = ×eries{
|
||||
Values: v,
|
||||
}
|
||||
}
|
||||
for i := range len(values) - 1 {
|
||||
for i := range len(values[0]) {
|
||||
fixBrokenBuckets(i, xss)
|
||||
}
|
||||
result := make([][]float64, len(values))
|
||||
@@ -61,6 +63,8 @@ func TestFixBrokenBucketsMultipleValues(t *testing.T) {
|
||||
}
|
||||
}
|
||||
f([][]float64{{10, 1}, {11, 2}, {13, 3}}, [][]float64{{10, 1}, {11, 2}, {13, 3}})
|
||||
f([][]float64{{nan, nan}, {11, 2}, {13, 3}}, [][]float64{{0, 0}, {11, 2}, {13, 3}})
|
||||
f([][]float64{{nan, nan, nan}, {11, 2, 3}, {13, 3, 4}}, [][]float64{{0, 0, 0}, {11, 2, 3}, {13, 3, 4}})
|
||||
}
|
||||
|
||||
func TestVmrangeBucketsToLE(t *testing.T) {
|
||||
|
||||
1
app/vmselect/vmui/assets/index-B7vIex3g.css
Normal file
1
app/vmselect/vmui/assets/index-B7vIex3g.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
205
app/vmselect/vmui/assets/index-SqjehVXD.js
Normal file
205
app/vmselect/vmui/assets/index-SqjehVXD.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -36,10 +36,10 @@
|
||||
<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-BT5pWGkz.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/vendor-BVRvRxZ2.js">
|
||||
<script type="module" crossorigin src="./assets/index-SqjehVXD.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/vendor-DBOs1yKE.js">
|
||||
<link rel="stylesheet" crossorigin href="./assets/vendor-D1GxaB_c.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-BHg4iVVe.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-B7vIex3g.css">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
@@ -486,6 +486,7 @@ func writeStorageMetrics(w io.Writer, strg *storage.Storage) {
|
||||
|
||||
metrics.WriteGaugeUint64(w, fmt.Sprintf(`vm_free_disk_space_bytes{path=%q}`, *DataPath), fs.MustGetFreeSpace(*DataPath))
|
||||
metrics.WriteGaugeUint64(w, fmt.Sprintf(`vm_free_disk_space_limit_bytes{path=%q}`, *DataPath), uint64(minFreeDiskSpaceBytes.N))
|
||||
metrics.WriteGaugeUint64(w, fmt.Sprintf(`vm_total_disk_space_bytes{path=%q}`, *DataPath), fs.MustGetTotalSpace(*DataPath))
|
||||
|
||||
isReadOnly := 0
|
||||
if strg.IsReadOnly() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:20-alpine3.19
|
||||
FROM node:22-alpine3.22
|
||||
|
||||
# Sets a custom location for the npm cache, preventing access errors in system directories
|
||||
ENV NPM_CONFIG_CACHE=/build/.npm
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.24.5 AS build-web-stage
|
||||
FROM golang:1.25.0 AS build-web-stage
|
||||
COPY build /build
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
489
app/vmui/packages/vmui/package-lock.json
generated
489
app/vmui/packages/vmui/package-lock.json
generated
@@ -1177,7 +1177,7 @@
|
||||
"version": "0.3.12",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
|
||||
"integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
@@ -1188,24 +1188,36 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/source-map": {
|
||||
"version": "0.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.10.tgz",
|
||||
"integrity": "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
|
||||
"integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.29",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
|
||||
"integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
@@ -1250,6 +1262,316 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
|
||||
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^1.0.3",
|
||||
"is-glob": "^4.0.3",
|
||||
"micromatch": "^4.0.5",
|
||||
"node-addon-api": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@parcel/watcher-android-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-x64": "2.5.1",
|
||||
"@parcel/watcher-freebsd-x64": "2.5.1",
|
||||
"@parcel/watcher-linux-arm-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-musl": "2.5.1",
|
||||
"@parcel/watcher-win32-arm64": "2.5.1",
|
||||
"@parcel/watcher-win32-ia32": "2.5.1",
|
||||
"@parcel/watcher-win32-x64": "2.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-android-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-darwin-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-darwin-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-freebsd-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm64-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm64-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-x64-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-x64-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-ia32": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
|
||||
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@preact/preset-vite": {
|
||||
"version": "2.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@preact/preset-vite/-/preset-vite-2.10.2.tgz",
|
||||
@@ -1750,9 +2072,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.12.tgz",
|
||||
"integrity": "sha512-LtOrbvDf5ndC9Xi+4QZjVL0woFymF/xSTKZKPgrrl7H7XoeDvnD+E2IclKVDyaK9UM756W/3BXqSU+JEHopA9g==",
|
||||
"version": "24.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
|
||||
"integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2221,7 +2543,7 @@
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
@@ -2523,7 +2845,7 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
@@ -2572,6 +2894,14 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT/X11"
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/cac": {
|
||||
"version": "6.7.14",
|
||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||
@@ -2702,6 +3032,23 @@
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.16.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/classnames": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
||||
@@ -2750,6 +3097,14 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -3060,6 +3415,20 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"detect-libc": "bin/detect-libc.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/doctrine": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||
@@ -3808,7 +4177,7 @@
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@@ -4522,7 +4891,7 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -4577,7 +4946,7 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
@@ -4616,7 +4985,7 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
@@ -5119,7 +5488,7 @@
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
@@ -5184,6 +5553,14 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/node-html-parser": {
|
||||
"version": "6.1.13",
|
||||
"resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz",
|
||||
@@ -5512,7 +5889,7 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
@@ -5754,6 +6131,21 @@
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/redent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
||||
@@ -6054,6 +6446,28 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.89.2",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.89.2.tgz",
|
||||
"integrity": "sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"immutable": "^5.0.2",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"sass": "sass.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@parcel/watcher": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded": {
|
||||
"version": "1.89.2",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.89.2.tgz",
|
||||
@@ -6583,6 +6997,29 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-support": {
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-support/node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stack-trace": {
|
||||
"version": "1.0.0-pre2",
|
||||
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-1.0.0-pre2.tgz",
|
||||
@@ -6849,6 +7286,26 @@
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.43.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz",
|
||||
"integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==",
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.14.0",
|
||||
"commander": "^2.20.0",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
"bin": {
|
||||
"terser": "bin/terser"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||
@@ -6959,7 +7416,7 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
|
||||
@@ -18,84 +18,96 @@ import QueryAnalyzer from "./pages/QueryAnalyzer";
|
||||
import DownsamplingFilters from "./pages/DownsamplingFilters";
|
||||
import RetentionFilters from "./pages/RetentionFilters";
|
||||
import RawQueryPage from "./pages/RawQueryPage";
|
||||
import ExploreRules from "./pages/ExploreAlerts/ExploreRules";
|
||||
import ExploreNotifiers from "./pages/ExploreAlerts/ExploreNotifiers";
|
||||
|
||||
const App: FC = () => {
|
||||
const [loadedTheme, setLoadedTheme] = useState(false);
|
||||
|
||||
return <>
|
||||
<HashRouter>
|
||||
<AppContextProvider>
|
||||
<>
|
||||
<ThemeProvider onLoaded={setLoadedTheme}/>
|
||||
{loadedTheme && (
|
||||
<Routes>
|
||||
<Route
|
||||
path={"/"}
|
||||
element={<MainLayout/>}
|
||||
>
|
||||
return (
|
||||
<>
|
||||
<HashRouter>
|
||||
<AppContextProvider>
|
||||
<>
|
||||
<ThemeProvider onLoaded={setLoadedTheme} />
|
||||
{loadedTheme && (
|
||||
<Routes>
|
||||
<Route
|
||||
path={router.home}
|
||||
element={<CustomPanel/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.rawQuery}
|
||||
element={<RawQueryPage/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.metrics}
|
||||
element={<ExploreMetrics/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.cardinality}
|
||||
element={<CardinalityPanel/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.topQueries}
|
||||
element={<TopQueries/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.trace}
|
||||
element={<TracePage/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.queryAnalyzer}
|
||||
element={<QueryAnalyzer/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.dashboards}
|
||||
element={<DashboardsLayout/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.withTemplate}
|
||||
element={<WithTemplate/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.relabel}
|
||||
element={<Relabel/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.activeQueries}
|
||||
element={<ActiveQueries/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.icons}
|
||||
element={<PreviewIcons/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.downsamplingDebug}
|
||||
element={<DownsamplingFilters/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.retentionDebug}
|
||||
element={<RetentionFilters/>}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
)}
|
||||
</>
|
||||
</AppContextProvider>
|
||||
</HashRouter>
|
||||
</>;
|
||||
path={"/"}
|
||||
element={<MainLayout />}
|
||||
>
|
||||
<Route
|
||||
path={router.home}
|
||||
element={<CustomPanel />}
|
||||
/>
|
||||
<Route
|
||||
path={router.rawQuery}
|
||||
element={<RawQueryPage />}
|
||||
/>
|
||||
<Route
|
||||
path={router.metrics}
|
||||
element={<ExploreMetrics />}
|
||||
/>
|
||||
<Route
|
||||
path={router.cardinality}
|
||||
element={<CardinalityPanel />}
|
||||
/>
|
||||
<Route
|
||||
path={router.topQueries}
|
||||
element={<TopQueries />}
|
||||
/>
|
||||
<Route
|
||||
path={router.trace}
|
||||
element={<TracePage />}
|
||||
/>
|
||||
<Route
|
||||
path={router.queryAnalyzer}
|
||||
element={<QueryAnalyzer />}
|
||||
/>
|
||||
<Route
|
||||
path={router.dashboards}
|
||||
element={<DashboardsLayout />}
|
||||
/>
|
||||
<Route
|
||||
path={router.withTemplate}
|
||||
element={<WithTemplate />}
|
||||
/>
|
||||
<Route
|
||||
path={router.relabel}
|
||||
element={<Relabel />}
|
||||
/>
|
||||
<Route
|
||||
path={router.activeQueries}
|
||||
element={<ActiveQueries />}
|
||||
/>
|
||||
<Route
|
||||
path={router.icons}
|
||||
element={<PreviewIcons />}
|
||||
/>
|
||||
<Route
|
||||
path={router.downsamplingDebug}
|
||||
element={<DownsamplingFilters />}
|
||||
/>
|
||||
<Route
|
||||
path={router.retentionDebug}
|
||||
element={<RetentionFilters />}
|
||||
/>
|
||||
<Route
|
||||
path={router.rules}
|
||||
element={<ExploreRules />}
|
||||
/>
|
||||
<Route
|
||||
path={router.notifiers}
|
||||
element={<ExploreNotifiers />}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
)}
|
||||
</>
|
||||
</AppContextProvider>
|
||||
</HashRouter>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export const getAccountIds = (server: string) =>
|
||||
`${server.replace(/^(.+)(\/select.+)/, "$1")}/admin/tenants`;
|
||||
import { getUrlWithoutTenant } from "../utils/tenants";
|
||||
export const getAccountIds = (server: string) => `${getUrlWithoutTenant(server)}/admin/tenants`;
|
||||
|
||||
23
app/vmui/packages/vmui/src/api/explore-alerts.ts
Normal file
23
app/vmui/packages/vmui/src/api/explore-alerts.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export const getGroupsUrl = (server: string): string => {
|
||||
return `${server}/vmalert/api/v1/rules?datasource_type=prometheus`;
|
||||
};
|
||||
|
||||
export const getItemUrl = (
|
||||
server: string,
|
||||
groupId: string,
|
||||
id: string,
|
||||
mode: string,
|
||||
): string => {
|
||||
return `${server}/vmalert/api/v1/${mode}?group_id=${groupId}&${mode}_id=${id}`;
|
||||
};
|
||||
|
||||
export const getGroupUrl = (
|
||||
server: string,
|
||||
id: string,
|
||||
): string => {
|
||||
return `${server}/vmalert/api/v1/group?group_id=${id}`;
|
||||
};
|
||||
|
||||
export const getNotifiersUrl = (server: string): string => {
|
||||
return `${server}/vmalert/api/v1/notifiers`;
|
||||
};
|
||||
@@ -15,3 +15,24 @@ export const getExportDataUrl = (server: string, query: string, period: TimePara
|
||||
if (reduceMemUsage) params.set("reduce_mem_usage", "1");
|
||||
return `${server}/api/v1/export?${params}`;
|
||||
};
|
||||
|
||||
export const getExportCSVDataUrl = (server: string, query: string[], period: TimeParams, reduceMemUsage: boolean): string => {
|
||||
const params = new URLSearchParams({
|
||||
start: period.start.toString(),
|
||||
end: period.end.toString(),
|
||||
format: "__name__,__value__,__timestamp__:unix_ms",
|
||||
});
|
||||
query.forEach((q => params.append("match[]", q)));
|
||||
if (reduceMemUsage) params.set("reduce_mem_usage", "1");
|
||||
return `${server}/api/v1/export/csv?${params}`;
|
||||
};
|
||||
|
||||
export const getExportJSONDataUrl = (server: string, query: string[], period: TimeParams, reduceMemUsage: boolean): string => {
|
||||
const params = new URLSearchParams({
|
||||
start: period.start.toString(),
|
||||
end: period.end.toString(),
|
||||
});
|
||||
query.forEach((q => params.append("match[]", q)));
|
||||
if (reduceMemUsage) params.set("reduce_mem_usage", "1");
|
||||
return `${server}/api/v1/export?${params}`;
|
||||
};
|
||||
|
||||
@@ -30,7 +30,13 @@ const delayOptions: AutoRefreshOption[] = [
|
||||
{ seconds: 7200, title: "2h" }
|
||||
];
|
||||
|
||||
export const ExecutionControls: FC = () => {
|
||||
interface ExecutionControlsProps {
|
||||
tooltip: string;
|
||||
useAutorefresh?: boolean;
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
export const ExecutionControls: FC<ExecutionControlsProps> = ({ tooltip, useAutorefresh, closeModal }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const dispatch = useTimeDispatch();
|
||||
@@ -56,6 +62,9 @@ export const ExecutionControls: FC = () => {
|
||||
|
||||
const handleUpdate = () => {
|
||||
dispatch({ type: "RUN_QUERY" });
|
||||
if (!useAutorefresh && isMobile) {
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -77,91 +86,118 @@ export const ExecutionControls: FC = () => {
|
||||
handleChange(d);
|
||||
};
|
||||
|
||||
return <>
|
||||
<div className="vm-execution-controls">
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-execution-controls-buttons": true,
|
||||
"vm-execution-controls-buttons_mobile": isMobile,
|
||||
"vm-header-button": !appModeEnable,
|
||||
})}
|
||||
>
|
||||
{!isMobile && (
|
||||
<Tooltip title="Refresh dashboard">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleUpdate}
|
||||
startIcon={<RefreshIcon/>}
|
||||
ariaLabel="refresh dashboard"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isMobile ? (
|
||||
<div
|
||||
className="vm-mobile-option"
|
||||
onClick={toggleOpenOptions}
|
||||
>
|
||||
<span className="vm-mobile-option__icon"><RestartIcon/></span>
|
||||
<div className="vm-mobile-option-text">
|
||||
<span className="vm-mobile-option-text__label">Auto-refresh</span>
|
||||
<span className="vm-mobile-option-text__value">{selectedDelay.title}</span>
|
||||
</div>
|
||||
<span className="vm-mobile-option__arrow"><ArrowDownIcon/></span>
|
||||
</div>
|
||||
) : (
|
||||
<Tooltip title="Auto-refresh control">
|
||||
<div ref={optionsButtonRef}>
|
||||
return (
|
||||
<>
|
||||
<div className="vm-execution-controls">
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-execution-controls-buttons": true,
|
||||
"vm-execution-controls-buttons_mobile": isMobile,
|
||||
"vm-header-button": !appModeEnable,
|
||||
"vm-autorefresh": useAutorefresh,
|
||||
})}
|
||||
>
|
||||
{useAutorefresh ? (
|
||||
isMobile ? (
|
||||
<div
|
||||
className="vm-mobile-option"
|
||||
onClick={toggleOpenOptions}
|
||||
>
|
||||
<span className="vm-mobile-option__icon"><RestartIcon/></span>
|
||||
<div className="vm-mobile-option-text">
|
||||
<span className="vm-mobile-option-text__label">Auto-refresh</span>
|
||||
<span className="vm-mobile-option-text__value">{selectedDelay.title}</span>
|
||||
</div>
|
||||
<span className="vm-mobile-option__arrow"><ArrowDownIcon/></span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Tooltip title={tooltip}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleUpdate}
|
||||
startIcon={<RefreshIcon/>}
|
||||
ariaLabel={tooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="Auto-refresh control">
|
||||
<div ref={optionsButtonRef}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
fullWidth
|
||||
endIcon={(
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-execution-controls-buttons__arrow": true,
|
||||
"vm-execution-controls-buttons__arrow_open": openOptions,
|
||||
})}
|
||||
>
|
||||
<ArrowDownIcon/>
|
||||
</div>
|
||||
)}
|
||||
onClick={toggleOpenOptions}
|
||||
>
|
||||
{selectedDelay.title}
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
isMobile ? (
|
||||
<div
|
||||
className="vm-mobile-option"
|
||||
onClick={handleUpdate}
|
||||
>
|
||||
<span className="vm-mobile-option__icon"><RestartIcon/></span>
|
||||
<div className="vm-mobile-option-text">
|
||||
<span className="vm-mobile-option-text__label">Refresh</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
fullWidth
|
||||
endIcon={(
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-execution-controls-buttons__arrow": true,
|
||||
"vm-execution-controls-buttons__arrow_open": openOptions,
|
||||
})}
|
||||
>
|
||||
<ArrowDownIcon/>
|
||||
</div>
|
||||
)}
|
||||
onClick={toggleOpenOptions}
|
||||
>
|
||||
{selectedDelay.title}
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
onClick={handleUpdate}
|
||||
startIcon={<RefreshIcon/>}
|
||||
ariaLabel={tooltip}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Popper
|
||||
open={openOptions}
|
||||
placement="bottom-right"
|
||||
onClose={handleCloseOptions}
|
||||
buttonRef={optionsButtonRef}
|
||||
title={isMobile ? "Auto-refresh duration" : undefined}
|
||||
>
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-execution-controls-list": true,
|
||||
"vm-execution-controls-list_mobile": isMobile,
|
||||
})}
|
||||
>
|
||||
{delayOptions.map(d => (
|
||||
{useAutorefresh && (
|
||||
<Popper
|
||||
open={openOptions}
|
||||
placement="bottom-right"
|
||||
onClose={handleCloseOptions}
|
||||
buttonRef={optionsButtonRef}
|
||||
title={isMobile ? "Auto-refresh duration" : undefined}
|
||||
>
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-list-item": true,
|
||||
"vm-list-item_mobile": isMobile,
|
||||
"vm-list-item_active": d.seconds === selectedDelay.seconds
|
||||
"vm-execution-controls-list": true,
|
||||
"vm-execution-controls-list_mobile": isMobile,
|
||||
})}
|
||||
key={d.seconds}
|
||||
onClick={createHandlerChange(d)}
|
||||
>
|
||||
{d.title}
|
||||
{delayOptions.map(d => (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-list-item": true,
|
||||
"vm-list-item_mobile": isMobile,
|
||||
"vm-list-item_active": d.seconds === selectedDelay.seconds
|
||||
})}
|
||||
key={d.seconds}
|
||||
onClick={createHandlerChange(d)}
|
||||
>
|
||||
{d.title}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Popper>
|
||||
</>;
|
||||
</Popper>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-radius: calc($button-radius + 1px);
|
||||
min-width: 107px;
|
||||
|
||||
:is(.vm-autorefresh) {
|
||||
min-width: 107px;
|
||||
}
|
||||
|
||||
&_mobile {
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import { FC, useCallback } from "preact/compat";
|
||||
import { useCallback, useRef } from "preact/compat";
|
||||
import Tooltip from "../Main/Tooltip/Tooltip";
|
||||
import Button from "../Main/Button/Button";
|
||||
import { DownloadIcon } from "../Main/Icons";
|
||||
import Popper from "../Main/Popper/Popper";
|
||||
import { useRef } from "react";
|
||||
import "./style.scss";
|
||||
import useBoolean from "../../hooks/useBoolean";
|
||||
|
||||
interface DownloadButtonProps {
|
||||
interface DownloadButtonProps<T extends string> {
|
||||
title: string;
|
||||
downloadFormatOptions?: string[];
|
||||
onDownload: (format?: string) => void;
|
||||
downloadFormatOptions?: T[];
|
||||
onDownload: (format?: T) => void;
|
||||
}
|
||||
|
||||
/** TODO: Currently unused, later will be added for the exporting metrics */
|
||||
const DownloadButton: FC<DownloadButtonProps> = ({ title, downloadFormatOptions, onDownload }) => {
|
||||
const DownloadButton = <T extends string>({ title, downloadFormatOptions, onDownload }: DownloadButtonProps<T>) => {
|
||||
const {
|
||||
value: isPopupOpen,
|
||||
setTrue: onOpenPopup,
|
||||
@@ -35,9 +33,19 @@ const DownloadButton: FC<DownloadButtonProps> = ({ title, downloadFormatOptions,
|
||||
}
|
||||
}, [onDownload, onClosePopup, isPopupOpen, onOpenPopup]);
|
||||
|
||||
const isDownloadFormat = useCallback((format: string): format is T => {
|
||||
return (downloadFormatOptions as string[])?.includes(format);
|
||||
}, [downloadFormatOptions]);
|
||||
|
||||
const onDownloadFormatClick = useCallback((event: Event) => {
|
||||
const button = event.currentTarget as HTMLButtonElement;
|
||||
onDownload(button.textContent ?? undefined);
|
||||
const format = button.textContent;
|
||||
if (format && isDownloadFormat(format)) {
|
||||
onDownload(format);
|
||||
} else {
|
||||
onDownload();
|
||||
}
|
||||
onClosePopup();
|
||||
}, [onDownload]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import "./style.scss";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export type BadgeColor = "firing" | "inactive" | "pending" | "no-match" | "unhealthy" | "ok" | "passive";
|
||||
|
||||
interface BadgeItem {
|
||||
value?: number | string;
|
||||
color: BadgeColor;
|
||||
}
|
||||
|
||||
interface BadgesProps {
|
||||
items: Record<string, BadgeItem>;
|
||||
align?: "center" | "start" | "end";
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const Badges = ({ items, children, align = "start" }: BadgesProps) => {
|
||||
return (
|
||||
<div
|
||||
className="vm-badges"
|
||||
style={{ "justify-content": align }}
|
||||
>
|
||||
{Object.entries(items).map(([name, props]) => (
|
||||
<span
|
||||
key={name}
|
||||
className={`vm-badge ${props.color}`}
|
||||
>{props.value ? `${name}: ${props.value}` : name}</span>
|
||||
))}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Badges;
|
||||
@@ -0,0 +1,69 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
$badge-colors: (
|
||||
"firing": $color-error,
|
||||
"inactive": $color-success,
|
||||
"pending": $color-warning,
|
||||
"no-match": $color-notice,
|
||||
"unhealthy": $color-broken,
|
||||
"ok": $color-info,
|
||||
"passive": $color-passive,
|
||||
"all": $color-passive,
|
||||
);
|
||||
|
||||
.vm-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $padding-small;
|
||||
&.align-center {
|
||||
justify-content: center;
|
||||
}
|
||||
.vm-badge {
|
||||
padding: 0 $padding-tiny;
|
||||
width: fit-content;
|
||||
@each $class, $color in $badge-colors {
|
||||
&.#{$class} {
|
||||
border: 1px solid $color;
|
||||
color: $color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vm-badge-base {
|
||||
font-weight: 400;
|
||||
border-radius: $border-radius-small;
|
||||
}
|
||||
|
||||
.vm-badge-menu-item {
|
||||
@extend .vm-badge-base;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 22px;
|
||||
@each $class, $color in $badge-colors {
|
||||
&.#{$class} {
|
||||
border-right: $border-radius-small solid $color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vm-badge-item {
|
||||
@extend .vm-badge-base;
|
||||
@each $class, $color in $badge-colors {
|
||||
&.#{$class} {
|
||||
border-left: $border-radius-small solid $color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vm-badge {
|
||||
@extend .vm-badge-base;
|
||||
background-color: transparent;
|
||||
padding: 0 $padding-tiny;
|
||||
line-height: 22px;
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import "./style.scss";
|
||||
import { Alert as APIAlert } from "../../../types";
|
||||
import { createSearchParams } from "react-router-dom";
|
||||
import Button from "../../Main/Button/Button";
|
||||
import Badges from "../Badges";
|
||||
import {
|
||||
SearchIcon,
|
||||
} from "../../Main/Icons";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
interface BaseAlertProps {
|
||||
item: APIAlert;
|
||||
}
|
||||
|
||||
const BaseAlert = ({ item }: BaseAlertProps) => {
|
||||
const query = item?.expression;
|
||||
|
||||
const openQueryLink = () => {
|
||||
const params = {
|
||||
"g0.expr": query,
|
||||
"g0.end_time": ""
|
||||
};
|
||||
window.open(`#/?${createSearchParams(params).toString()}`, "_blank", "noopener noreferrer");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="vm-explore-alerts-alert-item">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style={{ "text-align": "end" }}
|
||||
colSpan={2}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
startIcon={<SearchIcon />}
|
||||
onClick={openQueryLink}
|
||||
>
|
||||
<span className="vm-button-text">Run query</span>
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="vm-col-md">Query</td>
|
||||
<td>
|
||||
<pre>
|
||||
<code className="language-promql">{query}</code>
|
||||
</pre>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="vm-col-md">Active at</td>
|
||||
<td>{dayjs(item.activeAt).format("DD MMM YYYY HH:mm:ss")}</td>
|
||||
</tr>
|
||||
{!!Object.keys(item?.labels || {}).length && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Labels</td>
|
||||
<td>
|
||||
<Badges
|
||||
items={Object.fromEntries(Object.entries(item.labels).map(([name, value]) => [name, {
|
||||
color: "passive",
|
||||
value: value,
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
{!!Object.keys(item.annotations || {}).length && (
|
||||
<>
|
||||
<span className="title">Annotations</span>
|
||||
<table>
|
||||
<tbody>
|
||||
{Object.entries(item.annotations || {}).map(([name, value]) => (
|
||||
<tr key={name}>
|
||||
<td className="vm-col-md">{name}</td>
|
||||
<td>{value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BaseAlert;
|
||||
@@ -0,0 +1,74 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-modal {
|
||||
.vm-explore-alerts-alert-item {
|
||||
table {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vm-explore-alerts-alert-item {
|
||||
row-gap: $padding-global;
|
||||
margin-right: $padding-global;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a:hover > pre {
|
||||
background-color: $color-background-badge;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background-color: $color-background-hover;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: $color-background-badge;
|
||||
padding: 0 $padding-global;
|
||||
border-radius: $border-radius-small;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
.keyword,
|
||||
.function,
|
||||
.attr-name,
|
||||
.range-duration {
|
||||
color: $color-keyword;
|
||||
}
|
||||
}
|
||||
|
||||
.vm-col-sm {
|
||||
width: 10%;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.vm-col-md {
|
||||
width: 15%;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
td, th {
|
||||
line-height: 30px;
|
||||
padding: 4px $padding-small;
|
||||
}
|
||||
th {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 0 $padding-small;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import "./style.scss";
|
||||
import { Group as APIGroup } from "../../../types";
|
||||
import dayjs from "dayjs";
|
||||
import { formatDuration } from "../helpers";
|
||||
import Badges from "../Badges";
|
||||
|
||||
interface BaseGroupProps {
|
||||
group: APIGroup;
|
||||
}
|
||||
|
||||
const BaseGroup = ({ group }: BaseGroupProps) => {
|
||||
return (
|
||||
<div className="vm-explore-alerts-group">
|
||||
<div></div>
|
||||
<table>
|
||||
<tbody>
|
||||
{!!group.interval && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Interval</td>
|
||||
<td>{formatDuration(group.interval)}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!group.lastEvaluation && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Last evaluation</td>
|
||||
<td>{dayjs(group.lastEvaluation).format("DD MMM YYYY HH:mm:ss")}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!group.eval_offset && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Eval offset</td>
|
||||
<td>{formatDuration(group.eval_offset)}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!group.eval_delay && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Eval delay</td>
|
||||
<td>{formatDuration(group.eval_delay)}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!group.file && (
|
||||
<tr>
|
||||
<td className="vm-col-md">File</td>
|
||||
<td>{group.file}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!group.concurrency && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Concurrency</td>
|
||||
<td>{group.concurrency}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!group?.labels?.length && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Labels</td>
|
||||
<td>
|
||||
<Badges
|
||||
items={Object.fromEntries(Object.entries(group.labels).map(([name, value]) => [name, {
|
||||
color: "passive",
|
||||
value: value,
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!group?.params?.length && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Params</td>
|
||||
<td>
|
||||
<Badges
|
||||
items={Object.fromEntries(group.params.map(value => [value, {
|
||||
color: "passive",
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!group?.headers?.length && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Headers</td>
|
||||
<td>
|
||||
<Badges
|
||||
items={Object.fromEntries(group.headers.map(value => [value, {
|
||||
color: "passive",
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!group?.notifier_headers?.length && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Notifier headers</td>
|
||||
<td>
|
||||
<Badges
|
||||
items={Object.fromEntries(group.notifier_headers.map(value => [value, {
|
||||
color: "passive",
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BaseGroup;
|
||||
@@ -0,0 +1,78 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-modal {
|
||||
.vm-explore-alerts-group {
|
||||
table {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vm-explore-alerts-group {
|
||||
row-gap: $padding-global;
|
||||
margin-right: $padding-global;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
pre {
|
||||
position: relative;
|
||||
background-color: $color-background-badge;
|
||||
padding: 0 $padding-global;
|
||||
border-radius: $border-radius-small;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
.keyword,
|
||||
.function,
|
||||
.attr-name,
|
||||
.range-duration {
|
||||
color: $color-keyword;
|
||||
}
|
||||
div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
column-gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.vm-col-sm {
|
||||
width: 10%;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.vm-col-md {
|
||||
width: 15%;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
tr.hoverable {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: $color-background-hover;
|
||||
}
|
||||
}
|
||||
td, th {
|
||||
line-height: 30px;
|
||||
padding: 4px $padding-small;
|
||||
}
|
||||
th {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 0 $padding-small;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import "./style.scss";
|
||||
import { Rule as APIRule } from "../../../types";
|
||||
import { useNavigate, createSearchParams } from "react-router-dom";
|
||||
import { SearchIcon, DetailsIcon } from "../../Main/Icons";
|
||||
import Button from "../../Main/Button/Button";
|
||||
import Alert from "../../Main/Alert/Alert";
|
||||
import Badges, { BadgeColor } from "../Badges";
|
||||
import dayjs from "dayjs";
|
||||
import { formatDuration } from "../helpers";
|
||||
|
||||
interface BaseRuleProps {
|
||||
item: APIRule;
|
||||
}
|
||||
|
||||
const BaseRule = ({ item }: BaseRuleProps) => {
|
||||
const query = item?.query;
|
||||
const navigate = useNavigate();
|
||||
const openAlertLink = (id: string) => {
|
||||
return () => {
|
||||
navigate({
|
||||
pathname: "/rules",
|
||||
search: `group_id=${item.group_id}&alert_id=${id}`,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
const openQueryLink = () => {
|
||||
const params = {
|
||||
"g0.expr": query,
|
||||
"g0.end_time": ""
|
||||
};
|
||||
window.open(`#/?${createSearchParams(params).toString()}`, "_blank", "noopener noreferrer");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="vm-explore-alerts-rule-item">
|
||||
<div></div>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style={{ "text-align": "end" }}
|
||||
colSpan={2}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
startIcon={<SearchIcon />}
|
||||
onClick={openQueryLink}
|
||||
>
|
||||
<span className="vm-button-text">Run query</span>
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="vm-col-md">Query</td>
|
||||
<td>
|
||||
<pre>
|
||||
<code className="language-promql">{query}</code>
|
||||
</pre>
|
||||
</td>
|
||||
</tr>
|
||||
{!!item.duration && (
|
||||
<tr>
|
||||
<td className="vm-col-md">For</td>
|
||||
<td>{formatDuration(item.duration)}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!item.lastEvaluation && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Last evaluation</td>
|
||||
<td>{dayjs(item.lastEvaluation).format("DD MMM YYYY HH:mm:ss")}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!item.lastError && item.health !== "ok" && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Last error</td>
|
||||
<td>
|
||||
<Alert variant="error">{item.lastError}</Alert>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!Object.keys(item?.labels || {}).length && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Labels</td>
|
||||
<td>
|
||||
<Badges
|
||||
items={Object.fromEntries(Object.entries(item.labels).map(([name, value]) => [name, {
|
||||
color: "passive",
|
||||
value: value,
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
{!!Object.keys(item?.annotations || {}).length && (
|
||||
<>
|
||||
<span className="title">Annotations</span>
|
||||
<table className="fixed">
|
||||
<tbody>
|
||||
{Object.entries(item.annotations || {}).map(([name, value]) => (
|
||||
<tr key={name}>
|
||||
<td className="vm-col-md">{name}</td>
|
||||
<td>{value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
{!!item?.updates?.length && (
|
||||
<>
|
||||
<span className="title">{`Last updates ${item.updates.length}/${item.max_updates_entries}`}</span>
|
||||
<table className="fixed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="vm-col-md">Updated at</th>
|
||||
<th className="vm-col-md">Series returned</th>
|
||||
<th className="vm-col-md">Series fetched</th>
|
||||
<th className="vm-col-md">Duration</th>
|
||||
<th className="vm-col-md">Executed at</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{item.updates.map((update) => (
|
||||
<tr
|
||||
key={update.at}
|
||||
>
|
||||
<td className="vm-col-md">{dayjs(update.time).format("DD MMM YYYY HH:mm:ss")}</td>
|
||||
<td className="vm-col-md">{update.samples}</td>
|
||||
<td className="vm-col-md">{update.series_fetched}</td>
|
||||
<td className="vm-col-md">{formatDuration(update.duration / 1e9)}</td>
|
||||
<td className="vm-col-md">{dayjs(update.at).format("DD MMM YYYY HH:mm:ss")}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
{!!item?.alerts?.length && (
|
||||
<>
|
||||
<span className="title">Alerts</span>
|
||||
<table className="fixed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="vm-col-sm">Active since</th>
|
||||
<th className="vm-col-sm">State</th>
|
||||
<th className="vm-col-sm">Value</th>
|
||||
<th>Labels</th>
|
||||
<th className="vm-col-hidden"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{item.alerts.map((alert) => (
|
||||
<tr
|
||||
id={`alert-${alert.id}`}
|
||||
key={alert.id}
|
||||
>
|
||||
<td className="vm-col-sm">
|
||||
{dayjs(alert.activeAt).format("DD MMM YYYY HH:mm:ss")}
|
||||
</td>
|
||||
<td className="vm-col-sm">
|
||||
<Badges
|
||||
items={{ [alert.state]: { color: alert.state as BadgeColor } }}
|
||||
/>
|
||||
</td>
|
||||
<td className="vm-col-sm">
|
||||
<Badges
|
||||
items={{ [alert.value]: { color: "passive" } }}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Badges
|
||||
align="center"
|
||||
items={Object.fromEntries(Object.entries(alert.labels || {}).map(([name, value]) => [name, {
|
||||
color: "passive",
|
||||
value: value,
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
<td className="vm-col-hidden">
|
||||
<Button
|
||||
className="vm-button-borderless"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
startIcon={<DetailsIcon />}
|
||||
onClick={openAlertLink(alert.id)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BaseRule;
|
||||
@@ -0,0 +1,88 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-modal {
|
||||
.vm-explore-alerts-rule-item {
|
||||
table {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vm-explore-alerts-rule-item {
|
||||
row-gap: $padding-global;
|
||||
margin-right: $padding-global;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
pre {
|
||||
position: relative;
|
||||
background-color: $color-background-badge;
|
||||
padding: 0 $padding-global;
|
||||
border-radius: $border-radius-small;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
.keyword,
|
||||
.function,
|
||||
.attr-name,
|
||||
.range-duration {
|
||||
color: $color-keyword;
|
||||
}
|
||||
div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
column-gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.vm-col-hidden {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.vm-button {
|
||||
color: $color-passive;
|
||||
border: 1px solid var(--color-passive);
|
||||
}
|
||||
|
||||
.vm-col-sm {
|
||||
width: 10%;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.vm-col-md {
|
||||
width: 15%;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
table {
|
||||
&.fixed {
|
||||
table-layout: fixed;
|
||||
}
|
||||
width: 100%;
|
||||
td, th {
|
||||
line-height: 30px;
|
||||
padding: 4px $padding-small;
|
||||
vertical-align: middle;
|
||||
}
|
||||
td.align-center {
|
||||
text-align: center
|
||||
}
|
||||
th {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 0 $padding-small;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { FC } from "preact/compat";
|
||||
import "./style.scss";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Group as APIGroup } from "../../../types";
|
||||
import { DetailsIcon } from "../../Main/Icons";
|
||||
import Button from "../../Main/Button/Button";
|
||||
import Badges, { BadgeColor } from "../Badges";
|
||||
import classNames from "classnames";
|
||||
interface GroupHeaderControlsProps {
|
||||
group: APIGroup;
|
||||
}
|
||||
|
||||
const GroupHeaderHeader: FC<GroupHeaderControlsProps> = ({ group }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const openGroupModal = async () => {
|
||||
navigate({
|
||||
pathname: "/rules",
|
||||
search: `group_id=${group.id}`,
|
||||
});
|
||||
};
|
||||
|
||||
const headerClasses = classNames({
|
||||
"vm-explore-alerts-group-header": true,
|
||||
"vm-explore-alerts-group-header_mobile": isMobile,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={headerClasses}>
|
||||
<div className="vm-explore-alerts-group-header__desc">
|
||||
<div className="vm-explore-alerts-group-header__name">{group.name}</div>
|
||||
{!isMobile && (
|
||||
<div className="vm-explore-alerts-group-header__file">{group.file}</div>
|
||||
)}
|
||||
</div>
|
||||
<Badges
|
||||
items={Object.fromEntries(Object.entries(group.states || {}).map(([name, value]) => [name.toLowerCase(), {
|
||||
color: name.toLowerCase().replace(" ", "-") as BadgeColor,
|
||||
value: value,
|
||||
}]))}
|
||||
>
|
||||
<Button
|
||||
className="vm-button-borderless"
|
||||
size="small"
|
||||
color="gray"
|
||||
variant="outlined"
|
||||
startIcon={<DetailsIcon />}
|
||||
onClick={openGroupModal}
|
||||
/>
|
||||
</Badges>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupHeaderHeader;
|
||||
@@ -0,0 +1,60 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-explore-alerts-group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $padding-tiny 0 $padding-tiny $padding-global;
|
||||
justify-content: space-between;
|
||||
|
||||
.vm-button_small {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.vm-button-borderless {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@media(max-width: 768px) {
|
||||
.vm-button-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&_mobile {
|
||||
.vm-button-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__desc {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $padding-tiny;
|
||||
}
|
||||
|
||||
&__index {
|
||||
color: $color-text-secondary;
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
|
||||
&__name {
|
||||
flex-grow: 1;
|
||||
font-weight: bold;
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
line-height: 130%;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
&__file {
|
||||
color: $color-text-disabled;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0.2em 0.4em;
|
||||
font-size: 85%;
|
||||
background-color: $color-hover-black;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { FC } from "preact/compat";
|
||||
import "./style.scss";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import useCopyToClipboard from "../../../hooks/useCopyToClipboard";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
import Tooltip from "../../Main/Tooltip/Tooltip";
|
||||
import classNames from "classnames";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Badges, { BadgeColor } from "../Badges";
|
||||
import {
|
||||
LinkIcon,
|
||||
GroupIcon,
|
||||
AlertIcon,
|
||||
AlertingRuleIcon,
|
||||
RecordingRuleIcon,
|
||||
DetailsIcon,
|
||||
} from "../../Main/Icons";
|
||||
import Button from "../../Main/Button/Button";
|
||||
|
||||
interface ItemHeaderControlsProps {
|
||||
entity: string;
|
||||
type?: string;
|
||||
groupId: string;
|
||||
states?: Record<string, number>;
|
||||
id?: string;
|
||||
name: string;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const ItemHeader: FC<ItemHeaderControlsProps> = ({ name, id, groupId, entity, type, states, onClose }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
const { serverUrl } = useAppState();
|
||||
const navigate = useNavigate();
|
||||
const copyToClipboard = useCopyToClipboard();
|
||||
|
||||
const openItemLink = () => {
|
||||
navigate({
|
||||
pathname: "/rules",
|
||||
search: `group_id=${groupId}&${entity}_id=${id}`,
|
||||
});
|
||||
};
|
||||
|
||||
const copyLink = async () => {
|
||||
let link = `${serverUrl}/vmui/#/rules?group_id=${groupId}`;
|
||||
if (type) link = `${link}&${entity}_id=${id}`;
|
||||
await copyToClipboard(link, `Link to ${entity} has been copied`);
|
||||
};
|
||||
|
||||
const headerClasses = classNames({
|
||||
"vm-explore-alerts-item-header": true,
|
||||
"vm-explore-alerts-item-header_mobile": isMobile,
|
||||
});
|
||||
|
||||
const renderIcon = () => {
|
||||
switch(entity) {
|
||||
case "alert":
|
||||
return (
|
||||
<Tooltip title="Alert">
|
||||
<AlertIcon />
|
||||
</Tooltip>
|
||||
);
|
||||
case "group":
|
||||
return (
|
||||
<Tooltip title="Group">
|
||||
<GroupIcon />
|
||||
</Tooltip>
|
||||
);
|
||||
default:
|
||||
switch(type) {
|
||||
case "alerting":
|
||||
return (
|
||||
<Tooltip title="Alerting rule">
|
||||
<AlertingRuleIcon />
|
||||
</Tooltip>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tooltip title="Recording rule">
|
||||
<RecordingRuleIcon />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={headerClasses}
|
||||
id={`rule-${id}`}
|
||||
>
|
||||
<div className="vm-explore-alerts-item-header__title">
|
||||
{renderIcon()}
|
||||
<div className="vm-explore-alerts-item-header__name">{name}</div>
|
||||
</div>
|
||||
<Badges
|
||||
items={Object.fromEntries(Object.entries(states || {}).map(([name, value]) => [name, {
|
||||
color: name.toLowerCase().replace(" ", "-") as BadgeColor,
|
||||
value: value == 1 ? 0 : value,
|
||||
}]))}
|
||||
>
|
||||
{onClose ? (
|
||||
<Button
|
||||
className="vm-back-button"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
startIcon={<LinkIcon />}
|
||||
onClick={copyLink}
|
||||
>
|
||||
<span className="vm-button-text">Copy Link</span>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="vm-button-borderless"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
startIcon={<DetailsIcon />}
|
||||
onClick={openItemLink}
|
||||
/>
|
||||
)}
|
||||
</Badges>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ItemHeader;
|
||||
@@ -0,0 +1,70 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-explore-alerts-item-header {
|
||||
display: flex;
|
||||
grid-template-columns: auto 1fr auto auto;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $padding-global;
|
||||
|
||||
.vm-button_small {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
@media(max-width: 768px) {
|
||||
.vm-button-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.vm-button-borderless {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.vm-back-button {
|
||||
svg {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
&_mobile {
|
||||
grid-template-columns: 1fr auto;
|
||||
.vm-button-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__index {
|
||||
color: $color-text-secondary;
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-weight: bold;
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
line-height: 130%;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
&__title {
|
||||
display: flex;
|
||||
column-gap: $padding-global;
|
||||
svg {
|
||||
fill: $color-text-disabled;
|
||||
width: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&__file {
|
||||
color: $color-text-disabled;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0.2em 0.4em;
|
||||
font-size: 85%;
|
||||
background-color: $color-hover-black;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { FC } from "preact/compat";
|
||||
import "./style.scss";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import { Notifier } from "../../../types";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface NotifierHeaderControlsProps {
|
||||
notifier: Notifier;
|
||||
}
|
||||
|
||||
const NotifierHeaderHeader: FC<NotifierHeaderControlsProps> = ({
|
||||
notifier,
|
||||
}) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-explore-alerts-notifier-header": true,
|
||||
"vm-explore-alerts-notifier-header_mobile": isMobile,
|
||||
})}
|
||||
>
|
||||
<div className="vm-explore-alerts-notifier-header__name">
|
||||
{notifier.kind}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotifierHeaderHeader;
|
||||
@@ -0,0 +1,40 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-explore-alerts-notifier-header {
|
||||
display: flex;
|
||||
grid-template-columns: auto 1fr auto auto;
|
||||
align-items: center;
|
||||
padding: $padding-global;
|
||||
justify-content: space-between;
|
||||
gap: $padding-global;
|
||||
|
||||
&_mobile {
|
||||
grid-template-columns: 1fr auto;
|
||||
padding: $padding-small $padding-global;
|
||||
}
|
||||
|
||||
&__index {
|
||||
color: $color-text-secondary;
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
|
||||
&__name {
|
||||
flex-grow: 1;
|
||||
font-weight: bold;
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
line-height: 130%;
|
||||
}
|
||||
|
||||
&__file {
|
||||
color: $color-text-disabled;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0.2em 0.4em;
|
||||
font-size: 85%;
|
||||
background-color: $color-hover-black;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { FC } from "preact/compat";
|
||||
import Select from "../../Main/Select/Select";
|
||||
import { SearchIcon } from "../../Main/Icons";
|
||||
import TextField from "../../Main/TextField/TextField";
|
||||
import "./style.scss";
|
||||
import classNames from "classnames";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
|
||||
interface NotifiersHeaderProps {
|
||||
kinds: string[];
|
||||
allKinds: string[];
|
||||
onChangeKinds: (input: string) => void;
|
||||
onChangeSearch: (input: string) => void;
|
||||
}
|
||||
|
||||
const NotifiersHeader: FC<NotifiersHeaderProps> = ({
|
||||
kinds,
|
||||
allKinds,
|
||||
onChangeKinds,
|
||||
onChangeSearch,
|
||||
}) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-explore-alerts-header": true,
|
||||
"vm-explore-alerts-header_mobile": isMobile,
|
||||
"vm-block": true,
|
||||
"vm-block_mobile": isMobile,
|
||||
})}
|
||||
>
|
||||
<div className="vm-explore-alerts-header__rule_type">
|
||||
<Select
|
||||
value={kinds}
|
||||
list={allKinds}
|
||||
label="Notifier type"
|
||||
placeholder="Please select notifier type"
|
||||
onChange={onChangeKinds}
|
||||
autofocus={!!kinds.length && !isMobile}
|
||||
includeAll
|
||||
searchable
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-explore-alerts-header-search">
|
||||
<TextField
|
||||
label="Search"
|
||||
placeholder="Filter by kind, address or labels"
|
||||
startIcon={<SearchIcon />}
|
||||
onChange={onChangeSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotifiersHeader;
|
||||
@@ -0,0 +1,65 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-explore-alerts-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: $padding-global calc($padding-small + 10px);
|
||||
width: 100%;
|
||||
|
||||
&_mobile {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
&__rule_type {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
&__state {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
&-description {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: flex-start;
|
||||
gap: $padding-small;
|
||||
|
||||
ul {
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
button {
|
||||
color: inherit;
|
||||
min-height: 29px;
|
||||
}
|
||||
|
||||
code {
|
||||
margin: 0 3px;
|
||||
}
|
||||
}
|
||||
|
||||
&-search {
|
||||
flex-grow: 1;
|
||||
.vm-text-field__input {
|
||||
padding: 11px 28px;
|
||||
}
|
||||
.vm-text-field__icon-start {
|
||||
height: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
&__clear-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { FC } from "preact/compat";
|
||||
import ItemHeader from "../ItemHeader";
|
||||
import Accordion from "../../Main/Accordion/Accordion";
|
||||
import "./style.scss";
|
||||
import { Rule as APIRule } from "../../../types";
|
||||
import BaseRule from "../BaseRule";
|
||||
|
||||
interface RuleProps {
|
||||
states: Record<string, number>;
|
||||
rule: APIRule;
|
||||
}
|
||||
|
||||
const Rule: FC<RuleProps> = ({ states, rule }) => {
|
||||
const state = Object.keys(states).length > 0 ? Object.keys(states)[0] : "ok";
|
||||
return (
|
||||
<div className={`vm-explore-alerts-rule vm-badge-item ${state.replace(" ", "-")}`}>
|
||||
<Accordion
|
||||
key={`rule-${rule.id}`}
|
||||
title={<ItemHeader
|
||||
entity="rule"
|
||||
type={rule.type}
|
||||
groupId={rule.group_id}
|
||||
states={states}
|
||||
id={rule.id}
|
||||
name={rule.name}
|
||||
/>}
|
||||
>
|
||||
<BaseRule item={rule} />
|
||||
</Accordion>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Rule;
|
||||
@@ -0,0 +1,18 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-explore-alerts-rule {
|
||||
padding: $padding-tiny;
|
||||
padding-right: 0;
|
||||
display: flex;
|
||||
row-gap: $padding-tiny;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
&:has(>details[open]) {
|
||||
background-color: $color-background-item;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $color-background-item;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { FC, useMemo } from "preact/compat";
|
||||
import Select from "../../Main/Select/Select";
|
||||
import { SearchIcon } from "../../Main/Icons";
|
||||
import TextField from "../../Main/TextField/TextField";
|
||||
import "./style.scss";
|
||||
import classNames from "classnames";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
|
||||
interface RulesHeaderProps {
|
||||
types: string[];
|
||||
allTypes: string[];
|
||||
allStates: string[];
|
||||
states: string[];
|
||||
onChangeTypes: (input: string) => void;
|
||||
onChangeStates: (input: string) => void;
|
||||
onChangeSearch: (input: string) => void;
|
||||
}
|
||||
|
||||
const RulesHeader: FC<RulesHeaderProps> = ({
|
||||
types,
|
||||
allTypes,
|
||||
allStates,
|
||||
states,
|
||||
onChangeTypes,
|
||||
onChangeStates,
|
||||
onChangeSearch,
|
||||
}) => {
|
||||
const noStateText = useMemo(
|
||||
() => (types.length ? "" : "No states. Please select rule states"),
|
||||
[types],
|
||||
);
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-explore-alerts-header": true,
|
||||
"vm-explore-alerts-header_mobile": isMobile,
|
||||
"vm-block": true,
|
||||
"vm-block_mobile": isMobile,
|
||||
})}
|
||||
>
|
||||
<div className="vm-explore-alerts-header__rule_type">
|
||||
<Select
|
||||
value={types}
|
||||
list={allTypes}
|
||||
label="Rules type"
|
||||
placeholder="Please select rule type"
|
||||
onChange={onChangeTypes}
|
||||
autofocus={!!types.length && !isMobile}
|
||||
includeAll
|
||||
searchable
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-explore-alerts-header__state">
|
||||
<Select
|
||||
itemClassName="vm-badge-menu-item"
|
||||
value={states}
|
||||
list={allStates}
|
||||
label="State"
|
||||
placeholder="Please rule state"
|
||||
onChange={onChangeStates}
|
||||
noOptionsText={noStateText}
|
||||
includeAll
|
||||
searchable
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-explore-alerts-header-search">
|
||||
<TextField
|
||||
label="Search"
|
||||
placeholder="Filter by rule, name or labels"
|
||||
startIcon={<SearchIcon />}
|
||||
onChange={onChangeSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RulesHeader;
|
||||
@@ -0,0 +1,65 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-explore-alerts-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: $padding-global calc($padding-small + 10px);
|
||||
width: 100%;
|
||||
|
||||
&_mobile {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
&__rule_type {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
&__state {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
&-description {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: flex-start;
|
||||
gap: $padding-small;
|
||||
|
||||
ul {
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
button {
|
||||
color: inherit;
|
||||
min-height: 29px;
|
||||
}
|
||||
|
||||
code {
|
||||
margin: 0 3px;
|
||||
}
|
||||
}
|
||||
|
||||
&-search {
|
||||
flex-grow: 1;
|
||||
.vm-text-field__input {
|
||||
padding: 11px 28px;
|
||||
}
|
||||
.vm-text-field__icon-start {
|
||||
height: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
&__clear-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { FC } from "preact/compat";
|
||||
import "./style.scss";
|
||||
import { Target as APITarget } from "../../../types";
|
||||
import Alert from "../../Main/Alert/Alert";
|
||||
import Accordion from "../../Main/Accordion/Accordion";
|
||||
import Badges from "../Badges";
|
||||
|
||||
interface TargetProps {
|
||||
target: APITarget;
|
||||
}
|
||||
|
||||
const Target: FC<TargetProps> = ({ target }) => {
|
||||
const state = target?.lastError ? "unhealthy" : "ok";
|
||||
return (
|
||||
<div className={`vm-explore-alerts-target vm-badge-item ${state.replace(" ", "-")}`}>
|
||||
{(!!target?.labels?.length || !!target?.lastError) ? (
|
||||
<Accordion
|
||||
key={`target-${target.address}`}
|
||||
title={(
|
||||
<div className="vm-explore-alerts-target-header__name">{target.address}</div>
|
||||
)}
|
||||
>
|
||||
<div className="vm-explore-alerts-target-item">
|
||||
<table>
|
||||
<tbody>
|
||||
{!!target?.labels?.length && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Labels</td>
|
||||
<td>
|
||||
<Badges
|
||||
items={Object.fromEntries(Object.entries(target.labels).map(([name, value]) => [name, {
|
||||
value: value,
|
||||
color: "passive",
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!target.lastError && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Last error</td>
|
||||
<td>
|
||||
<Alert variant="error">{target.lastError}</Alert>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Accordion>
|
||||
) : (
|
||||
<span>{target.address}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Target;
|
||||
@@ -0,0 +1,48 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-explore-alerts-target {
|
||||
row-gap: $padding-global;
|
||||
margin-right: $padding-global;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.vm-col-md {
|
||||
width: 40%;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
td {
|
||||
vertical-align: middle;
|
||||
padding: $padding-global $padding-small;
|
||||
}
|
||||
th {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 0 $padding-small;
|
||||
}
|
||||
}
|
||||
|
||||
padding: $padding-tiny;
|
||||
padding-right: 0;
|
||||
display: flex;
|
||||
row-gap: $padding-tiny;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
border-radius: $border-radius-small;
|
||||
|
||||
&:has(>details[open]) {
|
||||
background-color: $color-background-item;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $color-background-item;
|
||||
}
|
||||
|
||||
.vm-explore-alerts-item-header__name {
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export const formatDuration = (raw: number) => {
|
||||
const duration = dayjs.duration(Math.round(raw * 1000));
|
||||
const fmt = [];
|
||||
if (duration.get("day")) fmt.push("D[d]");
|
||||
if (duration.get("hour")) fmt.push("H[h]");
|
||||
if (duration.get("minute")) fmt.push("m[m]");
|
||||
if (duration.get("millisecond")) {
|
||||
fmt.push("s.SSS[s]");
|
||||
} else if (!fmt.length || duration.get("second")) {
|
||||
fmt.push("s[s]");
|
||||
}
|
||||
return duration.format(fmt.join(" "));
|
||||
};
|
||||
@@ -1,9 +1,11 @@
|
||||
import { FC, useState, useEffect } from "preact/compat";
|
||||
import { JSX } from "preact";
|
||||
import { ArrowDownIcon } from "../Icons";
|
||||
import "./style.scss";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface AccordionProps {
|
||||
id?: string
|
||||
title: ReactNode
|
||||
children: ReactNode
|
||||
defaultExpanded?: boolean
|
||||
@@ -14,21 +16,24 @@ const Accordion: FC<AccordionProps> = ({
|
||||
defaultExpanded = false,
|
||||
onChange,
|
||||
title,
|
||||
children
|
||||
children,
|
||||
id,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(defaultExpanded);
|
||||
|
||||
const toggleOpen = () => {
|
||||
const toggleOpen = (event: JSX.TargetedMouseEvent<HTMLElement>) => {
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.toString()) {
|
||||
if ((event.target as HTMLElement).closest("button")) {
|
||||
event.preventDefault();
|
||||
return; // If the text is selected, cancel the execution of toggle.
|
||||
}
|
||||
|
||||
setIsOpen((prev) => {
|
||||
const newState = !prev;
|
||||
onChange && onChange(newState);
|
||||
return newState;
|
||||
});
|
||||
if (selection && selection.toString()) {
|
||||
event.preventDefault();
|
||||
return; // If the text is selected, cancel the execution of toggle.
|
||||
}
|
||||
const details = event.currentTarget.parentElement as HTMLDetailsElement;
|
||||
onChange && onChange(details.open);
|
||||
setIsOpen(details.open);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -37,23 +42,23 @@ const Accordion: FC<AccordionProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<header
|
||||
className={`vm-accordion-header ${isOpen && "vm-accordion-header_open"}`}
|
||||
onClick={toggleOpen}
|
||||
<details
|
||||
className="vm-accordion-section"
|
||||
key="content"
|
||||
open={isOpen}
|
||||
id={id}
|
||||
>
|
||||
{title}
|
||||
<div className={`vm-accordion-header__arrow ${isOpen && "vm-accordion-header__arrow_open"}`}>
|
||||
<ArrowDownIcon />
|
||||
</div>
|
||||
</header>
|
||||
{isOpen && (
|
||||
<section
|
||||
className="vm-accordion-section"
|
||||
key="content"
|
||||
<summary
|
||||
className="vm-accordion-header"
|
||||
onClick={toggleOpen}
|
||||
>
|
||||
{children}
|
||||
</section>
|
||||
)}
|
||||
{title}
|
||||
<div className="vm-accordion-header__arrow">
|
||||
<ArrowDownIcon />
|
||||
</div>
|
||||
</summary>
|
||||
{children}
|
||||
</details>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,10 +17,6 @@
|
||||
transform: rotate(0);
|
||||
transition: transform 200ms ease-in-out;
|
||||
|
||||
&_open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 14px;
|
||||
height: auto;
|
||||
@@ -28,6 +24,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.vm-accordion-section[open] > summary {
|
||||
& > .vm-accordion-header {
|
||||
&__arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.accordion-section {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user