mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-06-28 04:57:20 +03:00
Compare commits
170 Commits
weakpointe
...
timerpool-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ea70b8f57 | ||
|
|
676a88793a | ||
|
|
8d3e9d1dac | ||
|
|
09251f0a1e | ||
|
|
4ea5f8a84d | ||
|
|
cd52978096 | ||
|
|
f65e24b2ab | ||
|
|
0579e68409 | ||
|
|
f2aea8532f | ||
|
|
94473ed262 | ||
|
|
c646a66b60 | ||
|
|
ccf97a4143 | ||
|
|
66df8a5003 | ||
|
|
cea9505bab | ||
|
|
30ac8cd3fa | ||
|
|
a1f0b792af | ||
|
|
50f75d751f | ||
|
|
27f7bc81e0 | ||
|
|
90d23d7c9f | ||
|
|
f68c028673 | ||
|
|
f24bf391a4 | ||
|
|
bc64ecfa3d | ||
|
|
f0bbf6ec15 | ||
|
|
cff4bde4d6 | ||
|
|
1716f11677 | ||
|
|
b4932ed2da | ||
|
|
77f2ab139f | ||
|
|
5537140074 | ||
|
|
5d766bf7f1 | ||
|
|
5907239181 | ||
|
|
720c2bfa1d | ||
|
|
e971e6102e | ||
|
|
5cd6d7cfba | ||
|
|
907aa1973a | ||
|
|
d6dacd9771 | ||
|
|
5bb67a7f00 | ||
|
|
8c1c92d4c9 | ||
|
|
95ca45d05a | ||
|
|
828a2aaf17 | ||
|
|
007ae5a3f0 | ||
|
|
dcd23da4ba | ||
|
|
e33dbaf3d2 | ||
|
|
c68973a247 | ||
|
|
2c72ef0f38 | ||
|
|
bd0551da3b | ||
|
|
9f52c40b0b | ||
|
|
ba3b50df1d | ||
|
|
3cfeae7f1a | ||
|
|
32da04725b | ||
|
|
8ce4636bc0 | ||
|
|
6167ce655e | ||
|
|
f1e294aa2b | ||
|
|
b72bf6961d | ||
|
|
2b880fe7db | ||
|
|
9898743fbd | ||
|
|
ca372168ae | ||
|
|
323974164b | ||
|
|
d0b948289b | ||
|
|
aa429631a6 | ||
|
|
9e3cf9ab64 | ||
|
|
94601365ca | ||
|
|
02b5849d92 | ||
|
|
933f5b39d6 | ||
|
|
367cdb089f | ||
|
|
2a1b3866e1 | ||
|
|
327a103367 | ||
|
|
5a80d4c552 | ||
|
|
30c2868ff8 | ||
|
|
d9ffef486d | ||
|
|
cf6a1017bd | ||
|
|
84fc71e876 | ||
|
|
26c920e738 | ||
|
|
ebd736a30f | ||
|
|
76fcd96aec | ||
|
|
4f82250845 | ||
|
|
cfff64295d | ||
|
|
89464789dc | ||
|
|
f8859574de | ||
|
|
9d4a8ed799 | ||
|
|
dfcfacd04f | ||
|
|
28da18282b | ||
|
|
db8e40f26c | ||
|
|
e958d488b0 | ||
|
|
569f045728 | ||
|
|
2bb42cfb16 | ||
|
|
e227eb82bd | ||
|
|
3fff181e2c | ||
|
|
5854d9df72 | ||
|
|
85f556f53e | ||
|
|
42984e3413 | ||
|
|
08c835e79f | ||
|
|
4c23f6913e | ||
|
|
8411675d55 | ||
|
|
ba5cacbe60 | ||
|
|
1d2d0c49cc | ||
|
|
a0a33f0ce1 | ||
|
|
9327a426e0 | ||
|
|
272f6b2a46 | ||
|
|
5f559b7307 | ||
|
|
c06d499bf1 | ||
|
|
89fd27c922 | ||
|
|
ec4ec4c2be | ||
|
|
d0993058b1 | ||
|
|
f7ee52c245 | ||
|
|
63a6b9b863 | ||
|
|
fd23f6bfb3 | ||
|
|
1b8dc8a94c | ||
|
|
9109e2e7c3 | ||
|
|
bc75bbfbe7 | ||
|
|
dd19a17ef6 | ||
|
|
03d93cc413 | ||
|
|
46d4635b08 | ||
|
|
ea5bf24676 | ||
|
|
8a7b572ff4 | ||
|
|
611e96d875 | ||
|
|
0278bc5d9a | ||
|
|
a585d95365 | ||
|
|
b5578fcac2 | ||
|
|
86334534f6 | ||
|
|
944af7b049 | ||
|
|
3f98af6a0b | ||
|
|
7967ad661e | ||
|
|
1aa72ecbfd | ||
|
|
3b656147ef | ||
|
|
3c4004673e | ||
|
|
45c9f31987 | ||
|
|
37013d36c0 | ||
|
|
c9b3088c9c | ||
|
|
24aef8ea90 | ||
|
|
e540e5e381 | ||
|
|
51aebcd061 | ||
|
|
df7b752c7a | ||
|
|
6f74b139cc | ||
|
|
e49609cbc2 | ||
|
|
2e655a91bc | ||
|
|
1e927b2e53 | ||
|
|
21963a1cad | ||
|
|
87b291debe | ||
|
|
cce1cdcb6d | ||
|
|
03e003c828 | ||
|
|
ad9d11ba3f | ||
|
|
5c2ed99dab | ||
|
|
eaec80b7f3 | ||
|
|
d6ef8a807b | ||
|
|
c0318a84f0 | ||
|
|
5a056321af | ||
|
|
686289c02b | ||
|
|
9ae10247bb | ||
|
|
06ce3f1496 | ||
|
|
d0690ba15f | ||
|
|
483e00ffb9 | ||
|
|
06f969a4a7 | ||
|
|
9517f5cf1a | ||
|
|
e62e0685dc | ||
|
|
df92e617db | ||
|
|
7c0c8cc702 | ||
|
|
07291c1d62 | ||
|
|
7c0015b836 | ||
|
|
06e52a99fd | ||
|
|
f5840951a4 | ||
|
|
9ca5a8d0f4 | ||
|
|
894b22590d | ||
|
|
f85fd161e4 | ||
|
|
7d552dbd9a | ||
|
|
795c3deaee | ||
|
|
cb44353a36 | ||
|
|
7e05200c60 | ||
|
|
a2f033ce6c | ||
|
|
78b217d70c | ||
|
|
c9b23de9ce |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -63,7 +63,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
id: go
|
id: go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
go.sum
|
go.sum
|
||||||
|
|||||||
37
.github/workflows/check-commit-signed.yml
vendored
Normal file
37
.github/workflows/check-commit-signed.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
name: check-commit-signed
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-commit-signed:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # we need full history for commit verification
|
||||||
|
|
||||||
|
- name: Check commit signatures
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" != "pull_request" ]; then
|
||||||
|
echo "Not a PR event, skipping signature check"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
RANGE="${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
|
||||||
|
echo "Checking commits in PR range: $RANGE"
|
||||||
|
|
||||||
|
if [ -z "$(git rev-list $RANGE)" ]; then
|
||||||
|
echo "No new commits in this PR, skipping signature check"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
unsigned=$(git log --pretty="%H %G?" $RANGE | grep -vE " (G|E)$" || true)
|
||||||
|
if [ -n "$unsigned" ]; then
|
||||||
|
echo "Found unsigned commits:"
|
||||||
|
echo "$unsigned"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "All commits in PR are signed (G or E)"
|
||||||
2
.github/workflows/check-licenses.yml
vendored
2
.github/workflows/check-licenses.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
id: go
|
id: go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: stable
|
go-version: stable
|
||||||
cache: false
|
cache: false
|
||||||
|
|||||||
2
.github/workflows/codeql-analysis-go.yml
vendored
2
.github/workflows/codeql-analysis-go.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
id: go
|
id: go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
cache: false
|
cache: false
|
||||||
go-version: stable
|
go-version: stable
|
||||||
|
|||||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
id: go
|
id: go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
go.sum
|
go.sum
|
||||||
@@ -75,7 +75,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
id: go
|
id: go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
go.sum
|
go.sum
|
||||||
@@ -101,7 +101,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
id: go
|
id: go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
go.sum
|
go.sum
|
||||||
|
|||||||
11
SECURITY.md
11
SECURITY.md
@@ -4,12 +4,11 @@
|
|||||||
|
|
||||||
The following versions of VictoriaMetrics receive regular security fixes:
|
The following versions of VictoriaMetrics receive regular security fixes:
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
|---------|--------------------|
|
|--------------------------------------------------------------------------------|--------------------|
|
||||||
| [latest release](https://docs.victoriametrics.com/victoriametrics/changelog/) | :white_check_mark: |
|
| [Latest release](https://docs.victoriametrics.com/victoriametrics/changelog/) | :white_check_mark: |
|
||||||
| v1.102.x [LTS line](https://docs.victoriametrics.com/victoriametrics/lts-releases/) | :white_check_mark: |
|
| [LTS releases](https://docs.victoriametrics.com/victoriametrics/lts-releases/) | :white_check_mark: |
|
||||||
| v1.110.x [LTS line](https://docs.victoriametrics.com/victoriametrics/lts-releases/) | :white_check_mark: |
|
| other releases | :x: |
|
||||||
| other releases | :x: |
|
|
||||||
|
|
||||||
See [this page](https://victoriametrics.com/security/) for more details.
|
See [this page](https://victoriametrics.com/security/) for more details.
|
||||||
|
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ func usage() {
|
|||||||
const s = `
|
const s = `
|
||||||
victoria-metrics is a time series database and monitoring solution.
|
victoria-metrics is a time series database and monitoring solution.
|
||||||
|
|
||||||
See the docs at https://docs.victoriametrics.com/
|
See the docs at https://docs.victoriametrics.com/victoriametrics/
|
||||||
`
|
`
|
||||||
flagutil.Usage(s)
|
flagutil.Usage(s)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/common"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/common"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/remotewrite"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/remotewrite"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prommetadata"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
|
"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/firehose"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/opentelemetry/stream"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/opentelemetry/stream"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
|
||||||
@@ -68,7 +68,7 @@ func insertRows(at *auth.Token, tss []prompb.TimeSeries, mms []prompb.MetricMeta
|
|||||||
ctx.WriteRequest.Timeseries = tssDst
|
ctx.WriteRequest.Timeseries = tssDst
|
||||||
|
|
||||||
var metadataTotal int
|
var metadataTotal int
|
||||||
if promscrape.IsMetadataEnabled() {
|
if prommetadata.IsEnabled() {
|
||||||
var accountID, projectID uint32
|
var accountID, projectID uint32
|
||||||
if at != nil {
|
if at != nil {
|
||||||
accountID = at.AccountID
|
accountID = at.AccountID
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/remotewrite"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/remotewrite"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prommetadata"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
|
"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"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus/stream"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus/stream"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
|
||||||
@@ -36,7 +36,7 @@ func InsertHandler(at *auth.Token, req *http.Request) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
encoding := req.Header.Get("Content-Encoding")
|
encoding := req.Header.Get("Content-Encoding")
|
||||||
return stream.Parse(req.Body, defaultTimestamp, encoding, true, promscrape.IsMetadataEnabled(), func(rows []prometheus.Row, mms []prometheus.Metadata) error {
|
return stream.Parse(req.Body, defaultTimestamp, encoding, true, prommetadata.IsEnabled(), func(rows []prometheus.Row, mms []prometheus.Metadata) error {
|
||||||
return insertRows(at, rows, mms, extraLabels)
|
return insertRows(at, rows, mms, extraLabels)
|
||||||
}, func(s string) {
|
}, func(s string) {
|
||||||
httpserver.LogError(req, s)
|
httpserver.LogError(req, s)
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/common"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/common"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/remotewrite"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/remotewrite"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prommetadata"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
|
"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/promremotewrite/stream"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/tenantmetrics"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/tenantmetrics"
|
||||||
@@ -71,7 +71,7 @@ func insertRows(at *auth.Token, timeseries []prompb.TimeSeries, mms []prompb.Met
|
|||||||
ctx.WriteRequest.Timeseries = tssDst
|
ctx.WriteRequest.Timeseries = tssDst
|
||||||
|
|
||||||
var metadataTotal int
|
var metadataTotal int
|
||||||
if promscrape.IsMetadataEnabled() {
|
if prommetadata.IsEnabled() {
|
||||||
var accountID, projectID uint32
|
var accountID, projectID uint32
|
||||||
if at != nil {
|
if at != nil {
|
||||||
accountID = at.AccountID
|
accountID = at.AccountID
|
||||||
|
|||||||
@@ -463,12 +463,6 @@ again:
|
|||||||
// - Real-world implementations of v1 use both 400 and 415 status codes.
|
// - 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
|
// See more in research: https://github.com/VictoriaMetrics/VictoriaMetrics/pull/8462#issuecomment-2786918054
|
||||||
case 415, 400:
|
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) {
|
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."+
|
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)
|
"See https://docs.victoriametrics.com/victoriametrics/vmagent/#victoriametrics-remote-write-protocol", c.sanitizedURL)
|
||||||
|
|||||||
@@ -93,10 +93,7 @@ func TestParseRetryAfterHeader(t *testing.T) {
|
|||||||
|
|
||||||
// helper calculate the max possible time duration calculated by timeutil.AddJitterToDuration.
|
// helper calculate the max possible time duration calculated by timeutil.AddJitterToDuration.
|
||||||
func helper(d time.Duration) time.Duration {
|
func helper(d time.Duration) time.Duration {
|
||||||
dv := d / 10
|
dv := min(d/10, 10*time.Second)
|
||||||
if dv > 10*time.Second {
|
|
||||||
dv = 10 * time.Second
|
|
||||||
}
|
|
||||||
|
|
||||||
return d + dv
|
return d + dv
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import (
|
|||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/netutil"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutil"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutil"
|
||||||
"github.com/VictoriaMetrics/metrics"
|
"github.com/VictoriaMetrics/metrics"
|
||||||
)
|
)
|
||||||
@@ -84,7 +85,8 @@ func UnitTest(files []string, disableGroupLabel bool, externalLabels []string, e
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
} else {
|
} else {
|
||||||
httpListenAddr = httpListenPort
|
httpListenAddr = httpListenPort
|
||||||
ln, err := net.Listen("tcp", fmt.Sprintf(":%s", httpListenPort))
|
|
||||||
|
ln, err := net.Listen(netutil.GetTCPNetwork(), fmt.Sprintf(":%s", httpListenPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("cannot listen on port %s: %v", httpListenPort, err)
|
logger.Fatalf("cannot listen on port %s: %v", httpListenPort, err)
|
||||||
}
|
}
|
||||||
@@ -130,7 +132,7 @@ func UnitTest(files []string, disableGroupLabel bool, externalLabels []string, e
|
|||||||
}
|
}
|
||||||
labels[s[:n]] = s[n+1:]
|
labels[s[:n]] = s[n+1:]
|
||||||
}
|
}
|
||||||
_, err = notifier.Init(nil, labels, externalURL)
|
_, err = notifier.Init(labels, externalURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("failed to init notifier: %v", err)
|
logger.Fatalf("failed to init notifier: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ type Group struct {
|
|||||||
// EvalDelay will adjust the `time` parameter of rule evaluation requests to compensate intentional query delay from datasource.
|
// EvalDelay will adjust the `time` parameter of rule evaluation requests to compensate intentional query delay from datasource.
|
||||||
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5155
|
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5155
|
||||||
EvalDelay *promutil.Duration `yaml:"eval_delay,omitempty"`
|
EvalDelay *promutil.Duration `yaml:"eval_delay,omitempty"`
|
||||||
Limit int `yaml:"limit,omitempty"`
|
Limit *int `yaml:"limit,omitempty"`
|
||||||
Rules []Rule `yaml:"rules"`
|
Rules []Rule `yaml:"rules"`
|
||||||
Concurrency int `yaml:"concurrency"`
|
Concurrency int `yaml:"concurrency"`
|
||||||
// Labels is a set of label value pairs, that will be added to every rule.
|
// Labels is a set of label value pairs, that will be added to every rule.
|
||||||
@@ -91,8 +91,8 @@ func (g *Group) Validate(validateTplFn ValidateTplFn, validateExpressions bool)
|
|||||||
if g.EvalOffset != nil && g.EvalDelay != nil {
|
if g.EvalOffset != nil && g.EvalDelay != nil {
|
||||||
return fmt.Errorf("eval_offset cannot be used with eval_delay")
|
return fmt.Errorf("eval_offset cannot be used with eval_delay")
|
||||||
}
|
}
|
||||||
if g.Limit < 0 {
|
if g.Limit != nil && *g.Limit < 0 {
|
||||||
return fmt.Errorf("invalid limit %d, shouldn't be less than 0", g.Limit)
|
return fmt.Errorf("invalid limit %d, shouldn't be less than 0", *g.Limit)
|
||||||
}
|
}
|
||||||
if g.Concurrency < 0 {
|
if g.Concurrency < 0 {
|
||||||
return fmt.Errorf("invalid concurrency %d, shouldn't be less than 0", g.Concurrency)
|
return fmt.Errorf("invalid concurrency %d, shouldn't be less than 0", g.Concurrency)
|
||||||
|
|||||||
@@ -181,9 +181,10 @@ func TestGroupValidate_Failure(t *testing.T) {
|
|||||||
EvalOffset: promutil.NewDuration(2 * time.Minute),
|
EvalOffset: promutil.NewDuration(2 * time.Minute),
|
||||||
}, false, "eval_offset should be smaller than interval")
|
}, false, "eval_offset should be smaller than interval")
|
||||||
|
|
||||||
|
limit := -1
|
||||||
f(&Group{
|
f(&Group{
|
||||||
Name: "wrong limit",
|
Name: "wrong limit",
|
||||||
Limit: -1,
|
Limit: &limit,
|
||||||
}, false, "invalid limit")
|
}, false, "invalid limit")
|
||||||
|
|
||||||
f(&Group{
|
f(&Group{
|
||||||
|
|||||||
@@ -132,10 +132,7 @@ func (ls Labels) String() string {
|
|||||||
// a=[]Label{{Name: "a", Value: "2"}},b=[]Label{{Name: "a", Value: "1"}}, return 1
|
// a=[]Label{{Name: "a", Value: "2"}},b=[]Label{{Name: "a", Value: "1"}}, return 1
|
||||||
// a=[]Label{{Name: "a", Value: "1"}},b=[]Label{{Name: "a", Value: "1"}}, return 0
|
// a=[]Label{{Name: "a", Value: "1"}},b=[]Label{{Name: "a", Value: "1"}}, return 0
|
||||||
func LabelCompare(a, b Labels) int {
|
func LabelCompare(a, b Labels) int {
|
||||||
l := len(a)
|
l := min(len(b), len(a))
|
||||||
if len(b) < l {
|
|
||||||
l = len(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < l; i++ {
|
for i := 0; i < l; i++ {
|
||||||
if a[i].Name != b[i].Name {
|
if a[i].Name != b[i].Name {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -83,8 +82,7 @@ absolute path to all .tpl files in root.
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
alertURLGeneratorFn notifier.AlertURLGenerator
|
extURL *url.URL
|
||||||
extURL *url.URL
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -121,7 +119,7 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
alertURLGeneratorFn, err = getAlertURLGenerator(extURL, *externalAlertSource, *validateTemplates)
|
err = notifier.InitAlertURLGeneratorFn(extURL, *externalAlertSource, *validateTemplates)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("failed to init `external.alert.source`: %s", err)
|
logger.Fatalf("failed to init `external.alert.source`: %s", err)
|
||||||
}
|
}
|
||||||
@@ -228,7 +226,7 @@ func newManager(ctx context.Context) (*manager, error) {
|
|||||||
labels[s[:n]] = s[n+1:]
|
labels[s[:n]] = s[n+1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
nts, err := notifier.Init(alertURLGeneratorFn, labels, *externalURL)
|
nts, err := notifier.Init(labels, *externalURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to init notifier: %w", err)
|
return nil, fmt.Errorf("failed to init notifier: %w", err)
|
||||||
}
|
}
|
||||||
@@ -292,35 +290,6 @@ func getHostnameAsExternalURL(addr string, isSecure bool) (*url.URL, error) {
|
|||||||
return url.Parse(fmt.Sprintf("%s%s%s", schema, hname, port))
|
return url.Parse(fmt.Sprintf("%s%s%s", schema, hname, port))
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAlertURLGenerator(externalURL *url.URL, externalAlertSource string, validateTemplate bool) (notifier.AlertURLGenerator, error) {
|
|
||||||
if externalAlertSource == "" {
|
|
||||||
return func(a notifier.Alert) string {
|
|
||||||
gID, aID := strconv.FormatUint(a.GroupID, 10), strconv.FormatUint(a.ID, 10)
|
|
||||||
return fmt.Sprintf("%s/vmalert/alert?%s=%s&%s=%s", externalURL, paramGroupID, gID, paramAlertID, aID)
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
if validateTemplate {
|
|
||||||
if err := notifier.ValidateTemplates(map[string]string{
|
|
||||||
"tpl": externalAlertSource,
|
|
||||||
}); err != nil {
|
|
||||||
return nil, fmt.Errorf("error validating source template %s: %w", externalAlertSource, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m := map[string]string{
|
|
||||||
"tpl": externalAlertSource,
|
|
||||||
}
|
|
||||||
return func(alert notifier.Alert) string {
|
|
||||||
qFn := func(_ string) ([]datasource.Metric, error) {
|
|
||||||
return nil, fmt.Errorf("`query` template isn't supported for alert source template")
|
|
||||||
}
|
|
||||||
templated, err := alert.ExecTemplate(qFn, alert.Labels, m)
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("cannot template alert source: %s", err)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s/%s", externalURL, templated["tpl"])
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func usage() {
|
func usage() {
|
||||||
const s = `
|
const s = `
|
||||||
vmalert processes alerts and recording rules.
|
vmalert processes alerts and recording rules.
|
||||||
|
|||||||
@@ -49,30 +49,6 @@ func TestGetExternalURL(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetAlertURLGenerator(t *testing.T) {
|
|
||||||
testAlert := notifier.Alert{GroupID: 42, ID: 2, Value: 4, Labels: map[string]string{"tenant": "baz"}}
|
|
||||||
u, _ := url.Parse("https://victoriametrics.com/path")
|
|
||||||
fn, err := getAlertURLGenerator(u, "", false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error %s", err)
|
|
||||||
}
|
|
||||||
exp := fmt.Sprintf("https://victoriametrics.com/path/vmalert/alert?%s=42&%s=2", paramGroupID, paramAlertID)
|
|
||||||
if exp != fn(testAlert) {
|
|
||||||
t.Fatalf("unexpected url want %s, got %s", exp, fn(testAlert))
|
|
||||||
}
|
|
||||||
_, err = getAlertURLGenerator(nil, "foo?{{invalid}}", true)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected template validation error got nil")
|
|
||||||
}
|
|
||||||
fn, err = getAlertURLGenerator(u, "foo?query={{$value}}&ds={{ $labels.tenant }}", true)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error %s", err)
|
|
||||||
}
|
|
||||||
if exp := "https://victoriametrics.com/path/foo?query=4&ds=baz"; exp != fn(testAlert) {
|
|
||||||
t.Fatalf("unexpected url want %s, got %s", exp, fn(testAlert))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfigReload(t *testing.T) {
|
func TestConfigReload(t *testing.T) {
|
||||||
originalRulePath := *rulePath
|
originalRulePath := *rulePath
|
||||||
originalExternalURL := extURL
|
originalExternalURL := extURL
|
||||||
|
|||||||
@@ -29,25 +29,37 @@ type manager struct {
|
|||||||
groups map[uint64]*rule.Group
|
groups map[uint64]*rule.Group
|
||||||
}
|
}
|
||||||
|
|
||||||
// ruleAPI generates apiRule object from alert by its ID(hash)
|
// groupAPI generates apiGroup object from group by its ID(hash)
|
||||||
func (m *manager) ruleAPI(gID, rID uint64) (apiRule, error) {
|
func (m *manager) groupAPI(gID uint64) (*rule.ApiGroup, error) {
|
||||||
m.groupsMu.RLock()
|
m.groupsMu.RLock()
|
||||||
defer m.groupsMu.RUnlock()
|
defer m.groupsMu.RUnlock()
|
||||||
|
|
||||||
g, ok := m.groups[gID]
|
g, ok := m.groups[gID]
|
||||||
if !ok {
|
if !ok {
|
||||||
return apiRule{}, fmt.Errorf("can't find group with id %d", gID)
|
return nil, fmt.Errorf("can't find group with id %d", gID)
|
||||||
}
|
}
|
||||||
for _, rule := range g.Rules {
|
return g.ToAPI(), nil
|
||||||
if rule.ID() == rID {
|
}
|
||||||
return ruleToAPI(rule), nil
|
|
||||||
|
// ruleAPI generates apiRule object from alert by its ID(hash)
|
||||||
|
func (m *manager) ruleAPI(gID, rID uint64) (rule.ApiRule, error) {
|
||||||
|
m.groupsMu.RLock()
|
||||||
|
defer m.groupsMu.RUnlock()
|
||||||
|
|
||||||
|
g, ok := m.groups[gID]
|
||||||
|
if !ok {
|
||||||
|
return rule.ApiRule{}, fmt.Errorf("can't find group with id %d", gID)
|
||||||
|
}
|
||||||
|
for _, r := range g.Rules {
|
||||||
|
if r.ID() == rID {
|
||||||
|
return r.ToAPI(), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return apiRule{}, fmt.Errorf("can't find rule with id %d in group %q", rID, g.Name)
|
return rule.ApiRule{}, fmt.Errorf("can't find rule with id %d in group %q", rID, g.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// alertAPI generates apiAlert object from alert by its ID(hash)
|
// alertAPI generates apiAlert object from alert by its ID(hash)
|
||||||
func (m *manager) alertAPI(gID, aID uint64) (*apiAlert, error) {
|
func (m *manager) alertAPI(gID, aID uint64) (*rule.ApiAlert, error) {
|
||||||
m.groupsMu.RLock()
|
m.groupsMu.RLock()
|
||||||
defer m.groupsMu.RUnlock()
|
defer m.groupsMu.RUnlock()
|
||||||
|
|
||||||
@@ -60,7 +72,7 @@ func (m *manager) alertAPI(gID, aID uint64) (*apiAlert, error) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if apiAlert := alertToAPI(ar, aID); apiAlert != nil {
|
if apiAlert := ar.AlertToAPI(aID); apiAlert != nil {
|
||||||
return apiAlert, nil
|
return apiAlert, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func TestAlertExecTemplate(t *testing.T) {
|
|||||||
)
|
)
|
||||||
extLabels["cluster"] = extCluster
|
extLabels["cluster"] = extCluster
|
||||||
extLabels["dc"] = extDC
|
extLabels["dc"] = extDC
|
||||||
_, err := Init(nil, extLabels, extURL)
|
_, err := Init(extLabels, extURL)
|
||||||
checkErr(t, err)
|
checkErr(t, err)
|
||||||
|
|
||||||
f := func(alert *Alert, annotations map[string]string, tplExpected map[string]string) {
|
f := func(alert *Alert, annotations map[string]string, tplExpected map[string]string) {
|
||||||
|
|||||||
@@ -22,10 +22,11 @@ import (
|
|||||||
// AlertManager represents integration provider with Prometheus alert manager
|
// AlertManager represents integration provider with Prometheus alert manager
|
||||||
// https://github.com/prometheus/alertmanager
|
// https://github.com/prometheus/alertmanager
|
||||||
type AlertManager struct {
|
type AlertManager struct {
|
||||||
addr *url.URL
|
addr *url.URL
|
||||||
argFunc AlertURLGenerator
|
argFunc AlertURLGenerator
|
||||||
client *http.Client
|
client *http.Client
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
|
lastError string
|
||||||
|
|
||||||
authCfg *promauth.Config
|
authCfg *promauth.Config
|
||||||
// stores already parsed RelabelConfigs object
|
// stores already parsed RelabelConfigs object
|
||||||
@@ -71,6 +72,10 @@ func (am AlertManager) Addr() string {
|
|||||||
return am.addr.Redacted()
|
return am.addr.Redacted()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (am *AlertManager) LastError() string {
|
||||||
|
return am.lastError
|
||||||
|
}
|
||||||
|
|
||||||
// Send an alert or resolve message
|
// Send an alert or resolve message
|
||||||
func (am *AlertManager) Send(ctx context.Context, alerts []Alert, headers map[string]string) error {
|
func (am *AlertManager) Send(ctx context.Context, alerts []Alert, headers map[string]string) error {
|
||||||
am.metrics.alertsSent.Add(len(alerts))
|
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)
|
am.metrics.alertsSendDuration.UpdateDuration(startTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
am.metrics.alertsSendErrors.Add(len(alerts))
|
am.metrics.alertsSendErrors.Add(len(alerts))
|
||||||
|
am.lastError = err.Error()
|
||||||
|
} else {
|
||||||
|
am.lastError = ""
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ type FakeNotifier struct {
|
|||||||
// Close does nothing
|
// Close does nothing
|
||||||
func (*FakeNotifier) Close() {}
|
func (*FakeNotifier) Close() {}
|
||||||
|
|
||||||
|
// LastError returns last error message
|
||||||
|
func (*FakeNotifier) LastError() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// Addr returns ""
|
// Addr returns ""
|
||||||
func (*FakeNotifier) Addr() string { return "" }
|
func (*FakeNotifier) Addr() string { return "" }
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutil"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutil"
|
||||||
)
|
)
|
||||||
@@ -57,6 +60,42 @@ var (
|
|||||||
sendTimeout = flagutil.NewArrayDuration("notifier.sendTimeout", 10*time.Second, "Timeout when sending alerts to the corresponding -notifier.url")
|
sendTimeout = flagutil.NewArrayDuration("notifier.sendTimeout", 10*time.Second, "Timeout when sending alerts to the corresponding -notifier.url")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// AlertURLGeneratorFn returns a URL to the passed alert object.
|
||||||
|
// Call InitAlertURLGeneratorFn before using this function.
|
||||||
|
var AlertURLGeneratorFn AlertURLGenerator
|
||||||
|
|
||||||
|
// InitAlertURLGeneratorFn populates AlertURLGeneratorFn
|
||||||
|
func InitAlertURLGeneratorFn(externalURL *url.URL, externalAlertSource string, validateTemplate bool) error {
|
||||||
|
if externalAlertSource == "" {
|
||||||
|
AlertURLGeneratorFn = func(a Alert) string {
|
||||||
|
gID, aID := strconv.FormatUint(a.GroupID, 10), strconv.FormatUint(a.ID, 10)
|
||||||
|
return fmt.Sprintf("%s/vmalert/alert?%s=%s&%s=%s", externalURL, "group_id", gID, "alert_id", aID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if validateTemplate {
|
||||||
|
if err := ValidateTemplates(map[string]string{
|
||||||
|
"tpl": externalAlertSource,
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("error validating source template %s: %w", externalAlertSource, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m := map[string]string{
|
||||||
|
"tpl": externalAlertSource,
|
||||||
|
}
|
||||||
|
AlertURLGeneratorFn = func(alert Alert) string {
|
||||||
|
qFn := func(_ string) ([]datasource.Metric, error) {
|
||||||
|
return nil, fmt.Errorf("`query` template isn't supported for alert source template")
|
||||||
|
}
|
||||||
|
templated, err := alert.ExecTemplate(qFn, alert.Labels, m)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("cannot template alert source: %s", err)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s/%s", externalURL, templated["tpl"])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// cw holds a configWatcher for configPath configuration file
|
// cw holds a configWatcher for configPath configuration file
|
||||||
// configWatcher provides a list of Notifier objects discovered
|
// configWatcher provides a list of Notifier objects discovered
|
||||||
// from static config or via service discovery.
|
// from static config or via service discovery.
|
||||||
@@ -90,7 +129,7 @@ var (
|
|||||||
// - configuration via file. Supports live reloads and service discovery.
|
// - configuration via file. Supports live reloads and service discovery.
|
||||||
//
|
//
|
||||||
// Init returns an error if both mods are used.
|
// Init returns an error if both mods are used.
|
||||||
func Init(gen AlertURLGenerator, extLabels map[string]string, extURL string) (func() []Notifier, error) {
|
func Init(extLabels map[string]string, extURL string) (func() []Notifier, error) {
|
||||||
externalURL = extURL
|
externalURL = extURL
|
||||||
externalLabels = extLabels
|
externalLabels = extLabels
|
||||||
_, err := url.Parse(externalURL)
|
_, err := url.Parse(externalURL)
|
||||||
@@ -117,7 +156,7 @@ func Init(gen AlertURLGenerator, extLabels map[string]string, extURL string) (fu
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(*addrs) > 0 {
|
if len(*addrs) > 0 {
|
||||||
notifiers, err := notifiersFromFlags(gen)
|
notifiers, err := notifiersFromFlags(AlertURLGeneratorFn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create notifier from flag values: %w", err)
|
return nil, fmt.Errorf("failed to create notifier from flag values: %w", err)
|
||||||
}
|
}
|
||||||
@@ -127,7 +166,7 @@ func Init(gen AlertURLGenerator, extLabels map[string]string, extURL string) (fu
|
|||||||
return staticNotifiersFn, nil
|
return staticNotifiersFn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cw, err = newWatcher(*configPath, gen)
|
cw, err = newWatcher(*configPath, AlertURLGeneratorFn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to init config watcher: %w", err)
|
return nil, fmt.Errorf("failed to init config watcher: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package notifier
|
package notifier
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||||
@@ -12,7 +14,7 @@ func TestInit(t *testing.T) {
|
|||||||
|
|
||||||
*addrs = flagutil.ArrayString{"127.0.0.1", "127.0.0.2"}
|
*addrs = flagutil.ArrayString{"127.0.0.1", "127.0.0.2"}
|
||||||
|
|
||||||
fn, err := Init(nil, nil, "")
|
fn, err := Init(nil, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("%s", err)
|
t.Fatalf("%s", err)
|
||||||
}
|
}
|
||||||
@@ -52,7 +54,7 @@ func TestInitNegative(t *testing.T) {
|
|||||||
*configPath = path
|
*configPath = path
|
||||||
*addrs = flagutil.ArrayString{addr}
|
*addrs = flagutil.ArrayString{addr}
|
||||||
*blackHole = bh
|
*blackHole = bh
|
||||||
if _, err := Init(nil, nil, ""); err == nil {
|
if _, err := Init(nil, ""); err == nil {
|
||||||
t.Fatalf("expected to get error; got nil instead")
|
t.Fatalf("expected to get error; got nil instead")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,7 +71,7 @@ func TestBlackHole(t *testing.T) {
|
|||||||
|
|
||||||
*blackHole = true
|
*blackHole = true
|
||||||
|
|
||||||
fn, err := Init(nil, nil, "")
|
fn, err := Init(nil, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("%s", err)
|
t.Fatalf("%s", err)
|
||||||
}
|
}
|
||||||
@@ -91,3 +93,30 @@ func TestBlackHole(t *testing.T) {
|
|||||||
t.Fatalf("expected to get \"blackhole\"; got %q instead", nf1.Addr())
|
t.Fatalf("expected to get \"blackhole\"; got %q instead", nf1.Addr())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetAlertURLGenerator(t *testing.T) {
|
||||||
|
oldAlertURLGeneratorFn := AlertURLGeneratorFn
|
||||||
|
defer func() { AlertURLGeneratorFn = oldAlertURLGeneratorFn }()
|
||||||
|
|
||||||
|
testAlert := Alert{GroupID: 42, ID: 2, Value: 4, Labels: map[string]string{"tenant": "baz"}}
|
||||||
|
u, _ := url.Parse("https://victoriametrics.com/path")
|
||||||
|
err := InitAlertURLGeneratorFn(u, "", false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error %s", err)
|
||||||
|
}
|
||||||
|
exp := fmt.Sprintf("https://victoriametrics.com/path/vmalert/alert?%s=42&%s=2", "group_id", "alert_id")
|
||||||
|
if exp != AlertURLGeneratorFn(testAlert) {
|
||||||
|
t.Fatalf("unexpected url want %s, got %s", exp, AlertURLGeneratorFn(testAlert))
|
||||||
|
}
|
||||||
|
err = InitAlertURLGeneratorFn(nil, "foo?{{invalid}}", true)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected template validation error got nil")
|
||||||
|
}
|
||||||
|
err = InitAlertURLGeneratorFn(u, "foo?query={{$value}}&ds={{ $labels.tenant }}", true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error %s", err)
|
||||||
|
}
|
||||||
|
if exp := "https://victoriametrics.com/path/foo?query=4&ds=baz"; exp != AlertURLGeneratorFn(testAlert) {
|
||||||
|
t.Fatalf("unexpected url want %s, got %s", exp, AlertURLGeneratorFn(testAlert))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ type Notifier interface {
|
|||||||
Send(ctx context.Context, alerts []Alert, notifierHeaders map[string]string) error
|
Send(ctx context.Context, alerts []Alert, notifierHeaders map[string]string) error
|
||||||
// Addr returns address where alerts are sent.
|
// Addr returns address where alerts are sent.
|
||||||
Addr() string
|
Addr() string
|
||||||
|
// LastError returns error, that occured during last attempt to send data
|
||||||
|
LastError() string
|
||||||
// Close is a destructor for the Notifier
|
// Close is a destructor for the Notifier
|
||||||
Close()
|
Close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ func (bh *blackHoleNotifier) Close() {
|
|||||||
bh.metrics.close()
|
bh.metrics.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LastError return last notifier's error
|
||||||
|
func (bh *blackHoleNotifier) LastError() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// newBlackHoleNotifier creates a new blackHoleNotifier
|
// newBlackHoleNotifier creates a new blackHoleNotifier
|
||||||
func newBlackHoleNotifier() *blackHoleNotifier {
|
func newBlackHoleNotifier() *blackHoleNotifier {
|
||||||
address := "blackhole"
|
address := "blackhole"
|
||||||
|
|||||||
19
app/vmalert/notifier/web.go
Normal file
19
app/vmalert/notifier/web.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package notifier
|
||||||
|
|
||||||
|
// ApiNotifier represents a Notifier configuration for WEB view
|
||||||
|
type ApiNotifier struct {
|
||||||
|
// Kind is a Notifier type
|
||||||
|
Kind TargetType `json:"kind"`
|
||||||
|
// Targets is a list of Notifier targets
|
||||||
|
Targets []*ApiTarget `json:"targets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApiTarget represents a specific Notifier target for WEB view
|
||||||
|
type ApiTarget struct {
|
||||||
|
// Address is a URL for sending notifications
|
||||||
|
Address string `json:"address"`
|
||||||
|
// Labels is a list of labels to add to each sent notification
|
||||||
|
Labels map[string]string `json:"labels"`
|
||||||
|
// LastError contains the error faced while sending to notifier.
|
||||||
|
LastError string `json:"lastError"`
|
||||||
|
}
|
||||||
@@ -187,6 +187,54 @@ func (ar *AlertingRule) ID() uint64 {
|
|||||||
return ar.RuleID
|
return ar.RuleID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToAPI returns ApiRule representation of ar
|
||||||
|
func (ar *AlertingRule) ToAPI() ApiRule {
|
||||||
|
state := ar.state
|
||||||
|
lastState := state.getLast()
|
||||||
|
r := ApiRule{
|
||||||
|
Type: TypeAlerting,
|
||||||
|
DatasourceType: ar.Type.String(),
|
||||||
|
Name: ar.Name,
|
||||||
|
Query: ar.Expr,
|
||||||
|
Duration: ar.For.Seconds(),
|
||||||
|
KeepFiringFor: ar.KeepFiringFor.Seconds(),
|
||||||
|
Labels: ar.Labels,
|
||||||
|
Annotations: ar.Annotations,
|
||||||
|
LastEvaluation: lastState.Time,
|
||||||
|
EvaluationTime: lastState.Duration.Seconds(),
|
||||||
|
Health: "ok",
|
||||||
|
State: "inactive",
|
||||||
|
Alerts: ar.AlertsToAPI(),
|
||||||
|
LastSamples: lastState.Samples,
|
||||||
|
LastSeriesFetched: lastState.SeriesFetched,
|
||||||
|
MaxUpdates: state.size(),
|
||||||
|
Updates: state.getAll(),
|
||||||
|
Debug: ar.Debug,
|
||||||
|
|
||||||
|
// encode as strings to avoid rounding in JSON
|
||||||
|
ID: fmt.Sprintf("%d", ar.ID()),
|
||||||
|
GroupID: fmt.Sprintf("%d", ar.GroupID),
|
||||||
|
GroupName: ar.GroupName,
|
||||||
|
File: ar.File,
|
||||||
|
}
|
||||||
|
if lastState.Err != nil {
|
||||||
|
r.LastError = lastState.Err.Error()
|
||||||
|
r.Health = "err"
|
||||||
|
}
|
||||||
|
// satisfy apiRule.State logic
|
||||||
|
if len(r.Alerts) > 0 {
|
||||||
|
r.State = notifier.StatePending.String()
|
||||||
|
stateFiring := notifier.StateFiring.String()
|
||||||
|
for _, a := range r.Alerts {
|
||||||
|
if a.State == stateFiring {
|
||||||
|
r.State = stateFiring
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
// GetAlerts returns active alerts of rule
|
// GetAlerts returns active alerts of rule
|
||||||
func (ar *AlertingRule) GetAlerts() []*notifier.Alert {
|
func (ar *AlertingRule) GetAlerts() []*notifier.Alert {
|
||||||
ar.alertsMu.RLock()
|
ar.alertsMu.RLock()
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package rule
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -25,6 +24,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
ruleResultsLimit = flag.Int("rule.resultsLimit", 0, "Limits the number of alerts or recording results a single rule can produce. "+
|
||||||
|
"Can be overridden by the limit option under group if specified. "+
|
||||||
|
"If exceeded, the rule will be marked with an error and all its results will be discarded. "+
|
||||||
|
"0 means no limit.")
|
||||||
ruleUpdateEntriesLimit = flag.Int("rule.updateEntriesLimit", 20, "Defines the max number of rule's state updates stored in-memory. "+
|
ruleUpdateEntriesLimit = flag.Int("rule.updateEntriesLimit", 20, "Defines the max number of rule's state updates stored in-memory. "+
|
||||||
"Rule's updates are available on rule's Details page and are used for debugging purposes. The number of stored updates can be overridden per rule via update_entries_limit param.")
|
"Rule's updates are available on rule's Details page and are used for debugging purposes. The number of stored updates can be overridden per rule via update_entries_limit param.")
|
||||||
resendDelay = flag.Duration("rule.resendDelay", 0, "MiniMum amount of time to wait before resending an alert to notifier.")
|
resendDelay = flag.Duration("rule.resendDelay", 0, "MiniMum amount of time to wait before resending an alert to notifier.")
|
||||||
@@ -112,7 +115,6 @@ func NewGroup(cfg config.Group, qb datasource.QuerierBuilder, defaultInterval ti
|
|||||||
Name: cfg.Name,
|
Name: cfg.Name,
|
||||||
File: cfg.File,
|
File: cfg.File,
|
||||||
Interval: cfg.Interval.Duration(),
|
Interval: cfg.Interval.Duration(),
|
||||||
Limit: cfg.Limit,
|
|
||||||
Concurrency: cfg.Concurrency,
|
Concurrency: cfg.Concurrency,
|
||||||
checksum: cfg.Checksum,
|
checksum: cfg.Checksum,
|
||||||
Params: cfg.Params,
|
Params: cfg.Params,
|
||||||
@@ -129,6 +131,11 @@ func NewGroup(cfg config.Group, qb datasource.QuerierBuilder, defaultInterval ti
|
|||||||
if g.Interval == 0 {
|
if g.Interval == 0 {
|
||||||
g.Interval = defaultInterval
|
g.Interval = defaultInterval
|
||||||
}
|
}
|
||||||
|
if cfg.Limit != nil {
|
||||||
|
g.Limit = *cfg.Limit
|
||||||
|
} else {
|
||||||
|
g.Limit = *ruleResultsLimit
|
||||||
|
}
|
||||||
if g.Concurrency < 1 {
|
if g.Concurrency < 1 {
|
||||||
g.Concurrency = 1
|
g.Concurrency = 1
|
||||||
}
|
}
|
||||||
@@ -289,7 +296,7 @@ func (g *Group) InterruptEval() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close stops the group and it's rules, unregisters group metrics
|
// Close stops the group and its rules, unregisters group metrics
|
||||||
func (g *Group) Close() {
|
func (g *Group) Close() {
|
||||||
if g.doneCh == nil {
|
if g.doneCh == nil {
|
||||||
return
|
return
|
||||||
@@ -298,10 +305,6 @@ func (g *Group) Close() {
|
|||||||
g.InterruptEval()
|
g.InterruptEval()
|
||||||
<-g.finishedCh
|
<-g.finishedCh
|
||||||
|
|
||||||
g.closeGroupMetrics()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Group) closeGroupMetrics() {
|
|
||||||
metrics.UnregisterSet(g.metrics.set, true)
|
metrics.UnregisterSet(g.metrics.set, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,7 +334,7 @@ func (g *Group) Start(ctx context.Context, nts func() []notifier.Notifier, rw re
|
|||||||
defer func() { close(g.finishedCh) }()
|
defer func() { close(g.finishedCh) }()
|
||||||
evalTS := time.Now()
|
evalTS := time.Now()
|
||||||
// sleep random duration to spread group rules evaluation
|
// sleep random duration to spread group rules evaluation
|
||||||
// over time in order to reduce load on datasource.
|
// over time to reduce the load on datasource.
|
||||||
if !SkipRandSleepOnGroupStart {
|
if !SkipRandSleepOnGroupStart {
|
||||||
sleepBeforeStart := delayBeforeStart(evalTS, g.GetID(), g.Interval, g.EvalOffset)
|
sleepBeforeStart := delayBeforeStart(evalTS, g.GetID(), g.Interval, g.EvalOffset)
|
||||||
g.infof("will start in %v", sleepBeforeStart)
|
g.infof("will start in %v", sleepBeforeStart)
|
||||||
@@ -472,18 +475,6 @@ func (g *Group) UpdateWith(newGroup *Group) {
|
|||||||
g.updateCh <- newGroup
|
g.updateCh <- newGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeepCopy returns a deep copy of group
|
|
||||||
func (g *Group) DeepCopy() *Group {
|
|
||||||
g.mu.RLock()
|
|
||||||
data, _ := json.Marshal(g)
|
|
||||||
g.mu.RUnlock()
|
|
||||||
newG := Group{}
|
|
||||||
_ = json.Unmarshal(data, &newG)
|
|
||||||
newG.Rules = g.Rules
|
|
||||||
newG.id = g.id
|
|
||||||
return &newG
|
|
||||||
}
|
|
||||||
|
|
||||||
// if offset is specified, delayBeforeStart returns a duration to help aligning timestamp with offset;
|
// if offset is specified, delayBeforeStart returns a duration to help aligning timestamp with offset;
|
||||||
// otherwise, it returns a random duration between [0..interval] based on group key.
|
// otherwise, it returns a random duration between [0..interval] based on group key.
|
||||||
func delayBeforeStart(ts time.Time, key uint64, interval time.Duration, offset *time.Duration) time.Duration {
|
func delayBeforeStart(ts time.Time, key uint64, interval time.Duration, offset *time.Duration) time.Duration {
|
||||||
|
|||||||
@@ -81,6 +81,37 @@ func (rr *RecordingRule) ID() uint64 {
|
|||||||
return rr.RuleID
|
return rr.RuleID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToAPI returns ApiRule representation of rr
|
||||||
|
func (rr *RecordingRule) ToAPI() ApiRule {
|
||||||
|
state := rr.state
|
||||||
|
lastState := state.getLast()
|
||||||
|
r := ApiRule{
|
||||||
|
Type: TypeRecording,
|
||||||
|
DatasourceType: rr.Type.String(),
|
||||||
|
Name: rr.Name,
|
||||||
|
Query: rr.Expr,
|
||||||
|
Labels: rr.Labels,
|
||||||
|
LastEvaluation: lastState.Time,
|
||||||
|
EvaluationTime: lastState.Duration.Seconds(),
|
||||||
|
Health: "ok",
|
||||||
|
LastSamples: lastState.Samples,
|
||||||
|
LastSeriesFetched: lastState.SeriesFetched,
|
||||||
|
MaxUpdates: state.size(),
|
||||||
|
Updates: state.getAll(),
|
||||||
|
|
||||||
|
// encode as strings to avoid rounding
|
||||||
|
ID: fmt.Sprintf("%d", rr.ID()),
|
||||||
|
GroupID: fmt.Sprintf("%d", rr.GroupID),
|
||||||
|
GroupName: rr.GroupName,
|
||||||
|
File: rr.File,
|
||||||
|
}
|
||||||
|
if lastState.Err != nil {
|
||||||
|
r.LastError = lastState.Err.Error()
|
||||||
|
r.Health = "err"
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
// NewRecordingRule creates a new RecordingRule
|
// NewRecordingRule creates a new RecordingRule
|
||||||
func NewRecordingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rule) *RecordingRule {
|
func NewRecordingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rule) *RecordingRule {
|
||||||
debug := group.Debug
|
debug := group.Debug
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ type Rule interface {
|
|||||||
// ID returns unique ID that may be used for
|
// ID returns unique ID that may be used for
|
||||||
// identifying this Rule among others.
|
// identifying this Rule among others.
|
||||||
ID() uint64
|
ID() uint64
|
||||||
|
// ToAPI returns ApiRule representation of Rule
|
||||||
|
ToAPI() ApiRule
|
||||||
// exec executes the rule with given context at the given timestamp and limit.
|
// exec executes the rule with given context at the given timestamp and limit.
|
||||||
// returns an err if number of resulting time series exceeds the limit.
|
// returns an err if number of resulting time series exceeds the limit.
|
||||||
exec(ctx context.Context, ts time.Time, limit int) ([]prompb.TimeSeries, error)
|
exec(ctx context.Context, ts time.Time, limit int) ([]prompb.TimeSeries, error)
|
||||||
@@ -68,39 +70,6 @@ type StateEntry struct {
|
|||||||
Curl string `json:"curl"`
|
Curl string `json:"curl"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLastEntry returns latest stateEntry of rule
|
|
||||||
func GetLastEntry(r Rule) StateEntry {
|
|
||||||
if rule, ok := r.(*AlertingRule); ok {
|
|
||||||
return rule.state.getLast()
|
|
||||||
}
|
|
||||||
if rule, ok := r.(*RecordingRule); ok {
|
|
||||||
return rule.state.getLast()
|
|
||||||
}
|
|
||||||
return StateEntry{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRuleStateSize returns size of rule stateEntry
|
|
||||||
func GetRuleStateSize(r Rule) int {
|
|
||||||
if rule, ok := r.(*AlertingRule); ok {
|
|
||||||
return rule.state.size()
|
|
||||||
}
|
|
||||||
if rule, ok := r.(*RecordingRule); ok {
|
|
||||||
return rule.state.size()
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllRuleState returns rule entire stateEntries
|
|
||||||
func GetAllRuleState(r Rule) []StateEntry {
|
|
||||||
if rule, ok := r.(*AlertingRule); ok {
|
|
||||||
return rule.state.getAll()
|
|
||||||
}
|
|
||||||
if rule, ok := r.(*RecordingRule); ok {
|
|
||||||
return rule.state.getAll()
|
|
||||||
}
|
|
||||||
return []StateEntry{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ruleState) size() int {
|
func (s *ruleState) size() int {
|
||||||
s.RLock()
|
s.RLock()
|
||||||
defer s.RUnlock()
|
defer s.RUnlock()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package rule
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -8,79 +8,28 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/rule"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// ParamGroupID is group id key in url parameter
|
// ParamGroupID is group id key in url parameter
|
||||||
paramGroupID = "group_id"
|
ParamGroupID = "group_id"
|
||||||
// ParamAlertID is alert id key in url parameter
|
// ParamAlertID is alert id key in url parameter
|
||||||
paramAlertID = "alert_id"
|
ParamAlertID = "alert_id"
|
||||||
// ParamRuleID is rule id key in url parameter
|
// ParamRuleID is rule id key in url parameter
|
||||||
paramRuleID = "rule_id"
|
ParamRuleID = "rule_id"
|
||||||
|
|
||||||
|
// TypeRecording is a RecordingRule type
|
||||||
|
TypeRecording = "recording"
|
||||||
|
// TypeAlerting is an AlertingRule type
|
||||||
|
TypeAlerting = "alerting"
|
||||||
)
|
)
|
||||||
|
|
||||||
type apiNotifier struct {
|
// ApiGroup represents a Group for web view
|
||||||
Kind string `json:"kind"`
|
type ApiGroup struct {
|
||||||
Targets []*apiTarget `json:"targets"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type apiTarget struct {
|
|
||||||
Address string `json:"address"`
|
|
||||||
Labels map[string]string `json:"labels"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// apiAlert represents a notifier.AlertingRule state
|
|
||||||
// for WEB view
|
|
||||||
// https://github.com/prometheus/compliance/blob/main/alert_generator/specification.md#get-apiv1rules
|
|
||||||
type apiAlert struct {
|
|
||||||
State string `json:"state"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
Labels map[string]string `json:"labels,omitempty"`
|
|
||||||
Annotations map[string]string `json:"annotations"`
|
|
||||||
ActiveAt time.Time `json:"activeAt"`
|
|
||||||
|
|
||||||
// Additional fields
|
|
||||||
|
|
||||||
// ID is an unique Alert's ID within a group
|
|
||||||
ID string `json:"id"`
|
|
||||||
// RuleID is an unique Rule's ID within a group
|
|
||||||
RuleID string `json:"rule_id"`
|
|
||||||
// GroupID is an unique Group's ID
|
|
||||||
GroupID string `json:"group_id"`
|
|
||||||
// Expression contains the PromQL/MetricsQL expression
|
|
||||||
// for Rule's evaluation
|
|
||||||
Expression string `json:"expression"`
|
|
||||||
// SourceLink contains a link to a system which should show
|
|
||||||
// why Alert was generated
|
|
||||||
SourceLink string `json:"source"`
|
|
||||||
// Restored shows whether Alert's state was restored on restart
|
|
||||||
Restored bool `json:"restored"`
|
|
||||||
// Stabilizing shows when firing state is kept because of
|
|
||||||
// `keep_firing_for` instead of real alert
|
|
||||||
Stabilizing bool `json:"stabilizing"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// WebLink returns a link to the alert which can be used in UI.
|
|
||||||
func (aa *apiAlert) WebLink() string {
|
|
||||||
return fmt.Sprintf("alert?%s=%s&%s=%s",
|
|
||||||
paramGroupID, aa.GroupID, paramAlertID, aa.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// APILink returns a link to the alert's JSON representation.
|
|
||||||
func (aa *apiAlert) APILink() string {
|
|
||||||
return fmt.Sprintf("api/v1/alert?%s=%s&%s=%s",
|
|
||||||
paramGroupID, aa.GroupID, paramAlertID, aa.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// apiGroup represents Group for web view
|
|
||||||
// https://github.com/prometheus/compliance/blob/main/alert_generator/specification.md#get-apiv1rules
|
|
||||||
type apiGroup struct {
|
|
||||||
// Name is the group name as present in the config
|
// Name is the group name as present in the config
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
// Rules contains both recording and alerting rules
|
// Rules contains both recording and alerting rules
|
||||||
Rules []apiRule `json:"rules"`
|
Rules []ApiRule `json:"rules"`
|
||||||
// Interval is the Group's evaluation interval in float seconds as present in the file.
|
// Interval is the Group's evaluation interval in float seconds as present in the file.
|
||||||
Interval float64 `json:"interval"`
|
Interval float64 `json:"interval"`
|
||||||
// LastEvaluation is the timestamp of the last time the Group was executed
|
// LastEvaluation is the timestamp of the last time the Group was executed
|
||||||
@@ -116,15 +65,20 @@ type apiGroup struct {
|
|||||||
NoMatch int
|
NoMatch int
|
||||||
}
|
}
|
||||||
|
|
||||||
// groupAlerts represents a group of alerts for WEB view
|
// APILink returns a link to the group's JSON representation.
|
||||||
type groupAlerts struct {
|
func (ag *ApiGroup) APILink() string {
|
||||||
Group *apiGroup
|
return fmt.Sprintf("api/v1/group?%s=%s", ParamGroupID, ag.ID)
|
||||||
Alerts []*apiAlert
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// apiRule represents a Rule for web view
|
// GroupAlerts represents a Group with its Alerts for web view
|
||||||
|
type GroupAlerts struct {
|
||||||
|
Group *ApiGroup
|
||||||
|
Alerts []*ApiAlert
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApiRule represents a Rule for web view
|
||||||
// see https://github.com/prometheus/compliance/blob/main/alert_generator/specification.md#get-apiv1rules
|
// see https://github.com/prometheus/compliance/blob/main/alert_generator/specification.md#get-apiv1rules
|
||||||
type apiRule struct {
|
type ApiRule struct {
|
||||||
// State must be one of these under following scenarios
|
// State must be one of these under following scenarios
|
||||||
// "pending": at least 1 alert in the rule in pending state and no other alert in firing ruleState.
|
// "pending": at least 1 alert in the rule in pending state and no other alert in firing ruleState.
|
||||||
// "firing": at least 1 alert in the rule in firing state.
|
// "firing": at least 1 alert in the rule in firing state.
|
||||||
@@ -146,7 +100,7 @@ type apiRule struct {
|
|||||||
// LastEvaluation is the timestamp of the last time the rule was executed
|
// LastEvaluation is the timestamp of the last time the rule was executed
|
||||||
LastEvaluation time.Time `json:"lastEvaluation"`
|
LastEvaluation time.Time `json:"lastEvaluation"`
|
||||||
// Alerts is the list of all the alerts in this rule that are currently pending or firing
|
// Alerts is the list of all the alerts in this rule that are currently pending or firing
|
||||||
Alerts []*apiAlert `json:"alerts,omitempty"`
|
Alerts []*ApiAlert `json:"alerts,omitempty"`
|
||||||
// Health is the health of rule evaluation.
|
// Health is the health of rule evaluation.
|
||||||
// It MUST be one of "ok", "err", "unknown"
|
// It MUST be one of "ok", "err", "unknown"
|
||||||
Health string `json:"health"`
|
Health string `json:"health"`
|
||||||
@@ -177,143 +131,96 @@ type apiRule struct {
|
|||||||
// MaxUpdates is the max number of recorded ruleStateEntry objects
|
// MaxUpdates is the max number of recorded ruleStateEntry objects
|
||||||
MaxUpdates int `json:"max_updates_entries"`
|
MaxUpdates int `json:"max_updates_entries"`
|
||||||
// Updates contains the ordered list of recorded ruleStateEntry objects
|
// Updates contains the ordered list of recorded ruleStateEntry objects
|
||||||
Updates []rule.StateEntry `json:"-"`
|
Updates []StateEntry `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// apiRuleWithUpdates represents apiRule but with extra fields for marshalling
|
// ApiAlert represents a notifier.AlertingRule state
|
||||||
type apiRuleWithUpdates struct {
|
// for WEB view
|
||||||
apiRule
|
// https://github.com/prometheus/compliance/blob/main/alert_generator/specification.md#get-apiv1rules
|
||||||
// Updates contains the ordered list of recorded ruleStateEntry objects
|
type ApiAlert struct {
|
||||||
StateUpdates []rule.StateEntry `json:"updates,omitempty"`
|
State string `json:"state"`
|
||||||
}
|
Name string `json:"name"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Labels map[string]string `json:"labels,omitempty"`
|
||||||
|
Annotations map[string]string `json:"annotations"`
|
||||||
|
ActiveAt time.Time `json:"activeAt"`
|
||||||
|
|
||||||
// APILink returns a link to the rule's JSON representation.
|
// Additional fields
|
||||||
func (ar apiRule) APILink() string {
|
|
||||||
return fmt.Sprintf("api/v1/rule?%s=%s&%s=%s",
|
// ID is an unique Alert's ID within a group
|
||||||
paramGroupID, ar.GroupID, paramRuleID, ar.ID)
|
ID string `json:"id"`
|
||||||
|
// RuleID is an unique Rule's ID within a group
|
||||||
|
RuleID string `json:"rule_id"`
|
||||||
|
// GroupID is an unique Group's ID
|
||||||
|
GroupID string `json:"group_id"`
|
||||||
|
// Expression contains the PromQL/MetricsQL expression
|
||||||
|
// for Rule's evaluation
|
||||||
|
Expression string `json:"expression"`
|
||||||
|
// SourceLink contains a link to a system which should show
|
||||||
|
// why Alert was generated
|
||||||
|
SourceLink string `json:"source"`
|
||||||
|
// Restored shows whether Alert's state was restored on restart
|
||||||
|
Restored bool `json:"restored"`
|
||||||
|
// Stabilizing shows when firing state is kept because of
|
||||||
|
// `keep_firing_for` instead of real alert
|
||||||
|
Stabilizing bool `json:"stabilizing"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebLink returns a link to the alert which can be used in UI.
|
// WebLink returns a link to the alert which can be used in UI.
|
||||||
func (ar apiRule) WebLink() string {
|
func (aa *ApiAlert) WebLink() string {
|
||||||
|
return fmt.Sprintf("alert?%s=%s&%s=%s",
|
||||||
|
ParamGroupID, aa.GroupID, ParamAlertID, aa.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// APILink returns a link to the alert's JSON representation.
|
||||||
|
func (aa *ApiAlert) APILink() string {
|
||||||
|
return fmt.Sprintf("api/v1/alert?%s=%s&%s=%s",
|
||||||
|
ParamGroupID, aa.GroupID, ParamAlertID, aa.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApiRuleWithUpdates represents ApiRule but with extra fields for marshalling
|
||||||
|
type ApiRuleWithUpdates struct {
|
||||||
|
ApiRule
|
||||||
|
// Updates contains the ordered list of recorded ruleStateEntry objects
|
||||||
|
StateUpdates []StateEntry `json:"updates,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// APILink returns a link to the rule's JSON representation.
|
||||||
|
func (ar ApiRule) APILink() string {
|
||||||
|
return fmt.Sprintf("api/v1/rule?%s=%s&%s=%s",
|
||||||
|
ParamGroupID, ar.GroupID, ParamRuleID, ar.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebLink returns a link to the alert which can be used in UI.
|
||||||
|
func (ar ApiRule) WebLink() string {
|
||||||
return fmt.Sprintf("rule?%s=%s&%s=%s",
|
return fmt.Sprintf("rule?%s=%s&%s=%s",
|
||||||
paramGroupID, ar.GroupID, paramRuleID, ar.ID)
|
ParamGroupID, ar.GroupID, ParamRuleID, ar.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ruleToAPI(r any) apiRule {
|
// AlertsToAPI returns list of ApiAlert objects from existing alerts
|
||||||
if ar, ok := r.(*rule.AlertingRule); ok {
|
func (ar *AlertingRule) AlertsToAPI() []*ApiAlert {
|
||||||
return alertingToAPI(ar)
|
var alerts []*ApiAlert
|
||||||
}
|
|
||||||
if rr, ok := r.(*rule.RecordingRule); ok {
|
|
||||||
return recordingToAPI(rr)
|
|
||||||
}
|
|
||||||
return apiRule{}
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
ruleTypeRecording = "recording"
|
|
||||||
ruleTypeAlerting = "alerting"
|
|
||||||
)
|
|
||||||
|
|
||||||
func recordingToAPI(rr *rule.RecordingRule) apiRule {
|
|
||||||
lastState := rule.GetLastEntry(rr)
|
|
||||||
r := apiRule{
|
|
||||||
Type: ruleTypeRecording,
|
|
||||||
DatasourceType: rr.Type.String(),
|
|
||||||
Name: rr.Name,
|
|
||||||
Query: rr.Expr,
|
|
||||||
Labels: rr.Labels,
|
|
||||||
LastEvaluation: lastState.Time,
|
|
||||||
EvaluationTime: lastState.Duration.Seconds(),
|
|
||||||
Health: "ok",
|
|
||||||
LastSamples: lastState.Samples,
|
|
||||||
LastSeriesFetched: lastState.SeriesFetched,
|
|
||||||
MaxUpdates: rule.GetRuleStateSize(rr),
|
|
||||||
Updates: rule.GetAllRuleState(rr),
|
|
||||||
|
|
||||||
// encode as strings to avoid rounding
|
|
||||||
ID: fmt.Sprintf("%d", rr.ID()),
|
|
||||||
GroupID: fmt.Sprintf("%d", rr.GroupID),
|
|
||||||
GroupName: rr.GroupName,
|
|
||||||
File: rr.File,
|
|
||||||
}
|
|
||||||
if lastState.Err != nil {
|
|
||||||
r.LastError = lastState.Err.Error()
|
|
||||||
r.Health = "err"
|
|
||||||
}
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
// alertingToAPI returns Rule representation in form of apiRule
|
|
||||||
func alertingToAPI(ar *rule.AlertingRule) apiRule {
|
|
||||||
lastState := rule.GetLastEntry(ar)
|
|
||||||
r := apiRule{
|
|
||||||
Type: ruleTypeAlerting,
|
|
||||||
DatasourceType: ar.Type.String(),
|
|
||||||
Name: ar.Name,
|
|
||||||
Query: ar.Expr,
|
|
||||||
Duration: ar.For.Seconds(),
|
|
||||||
KeepFiringFor: ar.KeepFiringFor.Seconds(),
|
|
||||||
Labels: ar.Labels,
|
|
||||||
Annotations: ar.Annotations,
|
|
||||||
LastEvaluation: lastState.Time,
|
|
||||||
EvaluationTime: lastState.Duration.Seconds(),
|
|
||||||
Health: "ok",
|
|
||||||
State: "inactive",
|
|
||||||
Alerts: ruleToAPIAlert(ar),
|
|
||||||
LastSamples: lastState.Samples,
|
|
||||||
LastSeriesFetched: lastState.SeriesFetched,
|
|
||||||
MaxUpdates: rule.GetRuleStateSize(ar),
|
|
||||||
Updates: rule.GetAllRuleState(ar),
|
|
||||||
Debug: ar.Debug,
|
|
||||||
|
|
||||||
// encode as strings to avoid rounding in JSON
|
|
||||||
ID: fmt.Sprintf("%d", ar.ID()),
|
|
||||||
GroupID: fmt.Sprintf("%d", ar.GroupID),
|
|
||||||
GroupName: ar.GroupName,
|
|
||||||
File: ar.File,
|
|
||||||
}
|
|
||||||
if lastState.Err != nil {
|
|
||||||
r.LastError = lastState.Err.Error()
|
|
||||||
r.Health = "err"
|
|
||||||
}
|
|
||||||
// satisfy apiRule.State logic
|
|
||||||
if len(r.Alerts) > 0 {
|
|
||||||
r.State = notifier.StatePending.String()
|
|
||||||
stateFiring := notifier.StateFiring.String()
|
|
||||||
for _, a := range r.Alerts {
|
|
||||||
if a.State == stateFiring {
|
|
||||||
r.State = stateFiring
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
// ruleToAPIAlert generates list of apiAlert objects from existing alerts
|
|
||||||
func ruleToAPIAlert(ar *rule.AlertingRule) []*apiAlert {
|
|
||||||
var alerts []*apiAlert
|
|
||||||
for _, a := range ar.GetAlerts() {
|
for _, a := range ar.GetAlerts() {
|
||||||
if a.State == notifier.StateInactive {
|
if a.State == notifier.StateInactive {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
alerts = append(alerts, newAlertAPI(ar, a))
|
alerts = append(alerts, NewAlertAPI(ar, a))
|
||||||
}
|
}
|
||||||
return alerts
|
return alerts
|
||||||
}
|
}
|
||||||
|
|
||||||
// alertToAPI generates apiAlert object from alert by its id(hash)
|
// AlertToAPI generates apiAlert object from alert by its id(hash)
|
||||||
func alertToAPI(ar *rule.AlertingRule, id uint64) *apiAlert {
|
func (ar *AlertingRule) AlertToAPI(id uint64) *ApiAlert {
|
||||||
a := ar.GetAlert(id)
|
a := ar.GetAlert(id)
|
||||||
if a == nil {
|
if a == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return newAlertAPI(ar, a)
|
return NewAlertAPI(ar, a)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAlertAPI creates apiAlert for notifier.Alert
|
// NewAlertAPI creates apiAlert for notifier.Alert
|
||||||
func newAlertAPI(ar *rule.AlertingRule, a *notifier.Alert) *apiAlert {
|
func NewAlertAPI(ar *AlertingRule, a *notifier.Alert) *ApiAlert {
|
||||||
aa := &apiAlert{
|
aa := &ApiAlert{
|
||||||
// encode as strings to avoid rounding
|
// encode as strings to avoid rounding
|
||||||
ID: fmt.Sprintf("%d", a.ID),
|
ID: fmt.Sprintf("%d", a.ID),
|
||||||
GroupID: fmt.Sprintf("%d", a.GroupID),
|
GroupID: fmt.Sprintf("%d", a.GroupID),
|
||||||
@@ -328,8 +235,8 @@ func newAlertAPI(ar *rule.AlertingRule, a *notifier.Alert) *apiAlert {
|
|||||||
Restored: a.Restored,
|
Restored: a.Restored,
|
||||||
Value: strconv.FormatFloat(a.Value, 'f', -1, 32),
|
Value: strconv.FormatFloat(a.Value, 'f', -1, 32),
|
||||||
}
|
}
|
||||||
if alertURLGeneratorFn != nil {
|
if notifier.AlertURLGeneratorFn != nil {
|
||||||
aa.SourceLink = alertURLGeneratorFn(*a)
|
aa.SourceLink = notifier.AlertURLGeneratorFn(*a)
|
||||||
}
|
}
|
||||||
if a.State == notifier.StateFiring && !a.KeepFiringSince.IsZero() {
|
if a.State == notifier.StateFiring && !a.KeepFiringSince.IsZero() {
|
||||||
aa.Stabilizing = true
|
aa.Stabilizing = true
|
||||||
@@ -337,9 +244,11 @@ func newAlertAPI(ar *rule.AlertingRule, a *notifier.Alert) *apiAlert {
|
|||||||
return aa
|
return aa
|
||||||
}
|
}
|
||||||
|
|
||||||
func groupToAPI(g *rule.Group) *apiGroup {
|
// ToAPI returns ApiGroup representation of g
|
||||||
g = g.DeepCopy()
|
func (g *Group) ToAPI() *ApiGroup {
|
||||||
ag := apiGroup{
|
g.mu.RLock()
|
||||||
|
defer g.mu.RUnlock()
|
||||||
|
ag := ApiGroup{
|
||||||
// encode as string to avoid rounding
|
// encode as string to avoid rounding
|
||||||
ID: strconv.FormatUint(g.GetID(), 10),
|
ID: strconv.FormatUint(g.GetID(), 10),
|
||||||
Name: g.Name,
|
Name: g.Name,
|
||||||
@@ -359,9 +268,9 @@ func groupToAPI(g *rule.Group) *apiGroup {
|
|||||||
if g.EvalDelay != nil {
|
if g.EvalDelay != nil {
|
||||||
ag.EvalDelay = g.EvalDelay.Seconds()
|
ag.EvalDelay = g.EvalDelay.Seconds()
|
||||||
}
|
}
|
||||||
ag.Rules = make([]apiRule, 0)
|
ag.Rules = make([]ApiRule, 0)
|
||||||
for _, r := range g.Rules {
|
for _, r := range g.Rules {
|
||||||
ag.Rules = append(ag.Rules, ruleToAPI(r))
|
ag.Rules = append(ag.Rules, r.ToAPI())
|
||||||
}
|
}
|
||||||
return &ag
|
return &ag
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package rule
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
|
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/rule"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRecordingToApi(t *testing.T) {
|
func TestRecordingToApi(t *testing.T) {
|
||||||
@@ -17,7 +16,7 @@ func TestRecordingToApi(t *testing.T) {
|
|||||||
Values: []float64{1}, Timestamps: []int64{0},
|
Values: []float64{1}, Timestamps: []int64{0},
|
||||||
})
|
})
|
||||||
entriesLimit := 44
|
entriesLimit := 44
|
||||||
g := rule.NewGroup(config.Group{
|
g := NewGroup(config.Group{
|
||||||
Name: "group",
|
Name: "group",
|
||||||
File: "rules.yaml",
|
File: "rules.yaml",
|
||||||
Concurrency: 1,
|
Concurrency: 1,
|
||||||
@@ -31,24 +30,24 @@ func TestRecordingToApi(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, fq, 1*time.Minute, nil)
|
}, fq, 1*time.Minute, nil)
|
||||||
rr := g.Rules[0].(*rule.RecordingRule)
|
rr := g.Rules[0].(*RecordingRule)
|
||||||
|
|
||||||
expectedRes := apiRule{
|
expectedRes := ApiRule{
|
||||||
Name: "record_name",
|
Name: "record_name",
|
||||||
Query: "up",
|
Query: "up",
|
||||||
Labels: map[string]string{"label": "value"},
|
Labels: map[string]string{"label": "value"},
|
||||||
Health: "ok",
|
Health: "ok",
|
||||||
Type: ruleTypeRecording,
|
Type: TypeRecording,
|
||||||
DatasourceType: "prometheus",
|
DatasourceType: "prometheus",
|
||||||
ID: "1248",
|
ID: "1248",
|
||||||
GroupID: fmt.Sprintf("%d", g.CreateID()),
|
GroupID: fmt.Sprintf("%d", g.CreateID()),
|
||||||
GroupName: "group",
|
GroupName: "group",
|
||||||
File: "rules.yaml",
|
File: "rules.yaml",
|
||||||
MaxUpdates: 44,
|
MaxUpdates: 44,
|
||||||
Updates: make([]rule.StateEntry, 0),
|
Updates: make([]StateEntry, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
res := recordingToAPI(rr)
|
res := rr.ToAPI()
|
||||||
|
|
||||||
if !reflect.DeepEqual(res, expectedRes) {
|
if !reflect.DeepEqual(res, expectedRes) {
|
||||||
t.Fatalf("expected to have: \n%v;\ngot: \n%v", expectedRes, res)
|
t.Fatalf("expected to have: \n%v;\ngot: \n%v", expectedRes, res)
|
||||||
@@ -29,7 +29,9 @@ var (
|
|||||||
{"api/v1/rules", "list all loaded groups and rules"},
|
{"api/v1/rules", "list all loaded groups and rules"},
|
||||||
{"api/v1/alerts", "list all active alerts"},
|
{"api/v1/alerts", "list all active alerts"},
|
||||||
{"api/v1/notifiers", "list all notifiers"},
|
{"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/alert?%s=<int>&%s=<int>", rule.ParamGroupID, rule.ParamAlertID), "get alert status by group and alert ID"},
|
||||||
|
{fmt.Sprintf("api/v1/rule?%s=<int>&%s=<int>", rule.ParamGroupID, rule.ParamRuleID), "get rule status by group and rule ID"},
|
||||||
|
{fmt.Sprintf("api/v1/group?%s=<int>", rule.ParamGroupID), "get group status by group ID"},
|
||||||
}
|
}
|
||||||
systemLinks = [][2]string{
|
systemLinks = [][2]string{
|
||||||
{"vmalert/groups", "UI"},
|
{"vmalert/groups", "UI"},
|
||||||
@@ -45,8 +47,8 @@ var (
|
|||||||
{Name: "Docs", URL: "https://docs.victoriametrics.com/victoriametrics/vmalert/"},
|
{Name: "Docs", URL: "https://docs.victoriametrics.com/victoriametrics/vmalert/"},
|
||||||
}
|
}
|
||||||
ruleTypeMap = map[string]string{
|
ruleTypeMap = map[string]string{
|
||||||
"alert": ruleTypeAlerting,
|
"alert": rule.TypeAlerting,
|
||||||
"record": ruleTypeRecording,
|
"record": rule.TypeRecording,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -112,7 +114,7 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
|
|||||||
case "/rules":
|
case "/rules":
|
||||||
// Grafana makes an extra request to `/rules`
|
// Grafana makes an extra request to `/rules`
|
||||||
// handler in addition to `/api/v1/rules` calls in alerts UI
|
// handler in addition to `/api/v1/rules` calls in alerts UI
|
||||||
var data []*apiGroup
|
var data []*rule.ApiGroup
|
||||||
rf, err := newRulesFilter(r)
|
rf, err := newRulesFilter(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpserver.Errorf(w, r, "%s", err)
|
httpserver.Errorf(w, r, "%s", err)
|
||||||
@@ -178,14 +180,14 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
|
|||||||
w.Write(data)
|
w.Write(data)
|
||||||
return true
|
return true
|
||||||
case "/vmalert/api/v1/rule", "/api/v1/rule":
|
case "/vmalert/api/v1/rule", "/api/v1/rule":
|
||||||
rule, err := rh.getRule(r)
|
apiRule, err := rh.getRule(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpserver.Errorf(w, r, "%s", err)
|
httpserver.Errorf(w, r, "%s", err)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
rwu := apiRuleWithUpdates{
|
rwu := rule.ApiRuleWithUpdates{
|
||||||
apiRule: rule,
|
ApiRule: apiRule,
|
||||||
StateUpdates: rule.Updates,
|
StateUpdates: apiRule.Updates,
|
||||||
}
|
}
|
||||||
data, err := json.Marshal(rwu)
|
data, err := json.Marshal(rwu)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -195,6 +197,20 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
|
|||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.Write(data)
|
w.Write(data)
|
||||||
return true
|
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":
|
case "/-/reload":
|
||||||
if !httpserver.CheckAuthFlag(w, r, reloadAuthKey) {
|
if !httpserver.CheckAuthFlag(w, r, reloadAuthKey) {
|
||||||
return true
|
return true
|
||||||
@@ -209,30 +225,42 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rh *requestHandler) getRule(r *http.Request) (apiRule, error) {
|
func (rh *requestHandler) getGroup(r *http.Request) (*rule.ApiGroup, error) {
|
||||||
groupID, err := strconv.ParseUint(r.FormValue(paramGroupID), 10, 64)
|
groupID, err := strconv.ParseUint(r.FormValue(rule.ParamGroupID), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return apiRule{}, fmt.Errorf("failed to read %q param: %w", paramGroupID, err)
|
return nil, fmt.Errorf("failed to read %q param: %w", rule.ParamGroupID, err)
|
||||||
}
|
}
|
||||||
ruleID, err := strconv.ParseUint(r.FormValue(paramRuleID), 10, 64)
|
obj, err := rh.m.groupAPI(groupID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return apiRule{}, fmt.Errorf("failed to read %q param: %w", paramRuleID, err)
|
return nil, errResponse(err, http.StatusNotFound)
|
||||||
}
|
|
||||||
obj, err := rh.m.ruleAPI(groupID, ruleID)
|
|
||||||
if err != nil {
|
|
||||||
return apiRule{}, errResponse(err, http.StatusNotFound)
|
|
||||||
}
|
}
|
||||||
return obj, nil
|
return obj, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rh *requestHandler) getAlert(r *http.Request) (*apiAlert, error) {
|
func (rh *requestHandler) getRule(r *http.Request) (rule.ApiRule, error) {
|
||||||
groupID, err := strconv.ParseUint(r.FormValue(paramGroupID), 10, 64)
|
groupID, err := strconv.ParseUint(r.FormValue(rule.ParamGroupID), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read %q param: %w", paramGroupID, err)
|
return rule.ApiRule{}, fmt.Errorf("failed to read %q param: %w", rule.ParamGroupID, err)
|
||||||
}
|
}
|
||||||
alertID, err := strconv.ParseUint(r.FormValue(paramAlertID), 10, 64)
|
ruleID, err := strconv.ParseUint(r.FormValue(rule.ParamRuleID), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read %q param: %w", paramAlertID, err)
|
return rule.ApiRule{}, fmt.Errorf("failed to read %q param: %w", rule.ParamRuleID, err)
|
||||||
|
}
|
||||||
|
obj, err := rh.m.ruleAPI(groupID, ruleID)
|
||||||
|
if err != nil {
|
||||||
|
return rule.ApiRule{}, errResponse(err, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
return obj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rh *requestHandler) getAlert(r *http.Request) (*rule.ApiAlert, error) {
|
||||||
|
groupID, err := strconv.ParseUint(r.FormValue(rule.ParamGroupID), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read %q param: %w", rule.ParamGroupID, err)
|
||||||
|
}
|
||||||
|
alertID, err := strconv.ParseUint(r.FormValue(rule.ParamAlertID), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read %q param: %w", rule.ParamAlertID, err)
|
||||||
}
|
}
|
||||||
a, err := rh.m.alertAPI(groupID, alertID)
|
a, err := rh.m.alertAPI(groupID, alertID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -244,7 +272,7 @@ func (rh *requestHandler) getAlert(r *http.Request) (*apiAlert, error) {
|
|||||||
type listGroupsResponse struct {
|
type listGroupsResponse struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Data struct {
|
Data struct {
|
||||||
Groups []*apiGroup `json:"groups"`
|
Groups []*rule.ApiGroup `json:"groups"`
|
||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,19 +338,19 @@ func (rf *rulesFilter) matchesGroup(group *rule.Group) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rh *requestHandler) groups(rf *rulesFilter) []*apiGroup {
|
func (rh *requestHandler) groups(rf *rulesFilter) []*rule.ApiGroup {
|
||||||
rh.m.groupsMu.RLock()
|
rh.m.groupsMu.RLock()
|
||||||
defer rh.m.groupsMu.RUnlock()
|
defer rh.m.groupsMu.RUnlock()
|
||||||
|
|
||||||
groups := make([]*apiGroup, 0)
|
groups := make([]*rule.ApiGroup, 0)
|
||||||
for _, group := range rh.m.groups {
|
for _, group := range rh.m.groups {
|
||||||
if !rf.matchesGroup(group) {
|
if !rf.matchesGroup(group) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
g := groupToAPI(group)
|
g := group.ToAPI()
|
||||||
// the returned list should always be non-nil
|
// the returned list should always be non-nil
|
||||||
// https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4221
|
// https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4221
|
||||||
filteredRules := make([]apiRule, 0)
|
filteredRules := make([]rule.ApiRule, 0)
|
||||||
for _, rule := range g.Rules {
|
for _, rule := range g.Rules {
|
||||||
if rf.ruleType != "" && rf.ruleType != rule.Type {
|
if rf.ruleType != "" && rf.ruleType != rule.Type {
|
||||||
continue
|
continue
|
||||||
@@ -350,7 +378,7 @@ func (rh *requestHandler) groups(rf *rulesFilter) []*apiGroup {
|
|||||||
groups = append(groups, g)
|
groups = append(groups, g)
|
||||||
}
|
}
|
||||||
// sort list of groups for deterministic output
|
// sort list of groups for deterministic output
|
||||||
slices.SortFunc(groups, func(a, b *apiGroup) int {
|
slices.SortFunc(groups, func(a, b *rule.ApiGroup) int {
|
||||||
if a.Name != b.Name {
|
if a.Name != b.Name {
|
||||||
return strings.Compare(a.Name, b.Name)
|
return strings.Compare(a.Name, b.Name)
|
||||||
}
|
}
|
||||||
@@ -375,32 +403,32 @@ func (rh *requestHandler) listGroups(rf *rulesFilter) ([]byte, error) {
|
|||||||
type listAlertsResponse struct {
|
type listAlertsResponse struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Data struct {
|
Data struct {
|
||||||
Alerts []*apiAlert `json:"alerts"`
|
Alerts []*rule.ApiAlert `json:"alerts"`
|
||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rh *requestHandler) groupAlerts() []groupAlerts {
|
func (rh *requestHandler) groupAlerts() []rule.GroupAlerts {
|
||||||
rh.m.groupsMu.RLock()
|
rh.m.groupsMu.RLock()
|
||||||
defer rh.m.groupsMu.RUnlock()
|
defer rh.m.groupsMu.RUnlock()
|
||||||
|
|
||||||
var gAlerts []groupAlerts
|
var gAlerts []rule.GroupAlerts
|
||||||
for _, g := range rh.m.groups {
|
for _, g := range rh.m.groups {
|
||||||
var alerts []*apiAlert
|
var alerts []*rule.ApiAlert
|
||||||
for _, r := range g.Rules {
|
for _, r := range g.Rules {
|
||||||
a, ok := r.(*rule.AlertingRule)
|
a, ok := r.(*rule.AlertingRule)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
alerts = append(alerts, ruleToAPIAlert(a)...)
|
alerts = append(alerts, a.AlertsToAPI()...)
|
||||||
}
|
}
|
||||||
if len(alerts) > 0 {
|
if len(alerts) > 0 {
|
||||||
gAlerts = append(gAlerts, groupAlerts{
|
gAlerts = append(gAlerts, rule.GroupAlerts{
|
||||||
Group: groupToAPI(g),
|
Group: g.ToAPI(),
|
||||||
Alerts: alerts,
|
Alerts: alerts,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
slices.SortFunc(gAlerts, func(a, b groupAlerts) int {
|
slices.SortFunc(gAlerts, func(a, b rule.GroupAlerts) int {
|
||||||
return strings.Compare(a.Group.Name, b.Group.Name)
|
return strings.Compare(a.Group.Name, b.Group.Name)
|
||||||
})
|
})
|
||||||
return gAlerts
|
return gAlerts
|
||||||
@@ -411,7 +439,7 @@ func (rh *requestHandler) listAlerts(rf *rulesFilter) ([]byte, error) {
|
|||||||
defer rh.m.groupsMu.RUnlock()
|
defer rh.m.groupsMu.RUnlock()
|
||||||
|
|
||||||
lr := listAlertsResponse{Status: "success"}
|
lr := listAlertsResponse{Status: "success"}
|
||||||
lr.Data.Alerts = make([]*apiAlert, 0)
|
lr.Data.Alerts = make([]*rule.ApiAlert, 0)
|
||||||
for _, group := range rh.m.groups {
|
for _, group := range rh.m.groups {
|
||||||
if !rf.matchesGroup(group) {
|
if !rf.matchesGroup(group) {
|
||||||
continue
|
continue
|
||||||
@@ -421,12 +449,12 @@ func (rh *requestHandler) listAlerts(rf *rulesFilter) ([]byte, error) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
lr.Data.Alerts = append(lr.Data.Alerts, ruleToAPIAlert(a)...)
|
lr.Data.Alerts = append(lr.Data.Alerts, a.AlertsToAPI()...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sort list of alerts for deterministic output
|
// sort list of alerts for deterministic output
|
||||||
slices.SortFunc(lr.Data.Alerts, func(a, b *apiAlert) int {
|
slices.SortFunc(lr.Data.Alerts, func(a, b *rule.ApiAlert) int {
|
||||||
return strings.Compare(a.ID, b.ID)
|
return strings.Compare(a.ID, b.ID)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -443,7 +471,7 @@ func (rh *requestHandler) listAlerts(rf *rulesFilter) ([]byte, error) {
|
|||||||
type listNotifiersResponse struct {
|
type listNotifiersResponse struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Data struct {
|
Data struct {
|
||||||
Notifiers []*apiNotifier `json:"notifiers"`
|
Notifiers []*notifier.ApiNotifier `json:"notifiers"`
|
||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,19 +479,20 @@ func (rh *requestHandler) listNotifiers() ([]byte, error) {
|
|||||||
targets := notifier.GetTargets()
|
targets := notifier.GetTargets()
|
||||||
|
|
||||||
lr := listNotifiersResponse{Status: "success"}
|
lr := listNotifiersResponse{Status: "success"}
|
||||||
lr.Data.Notifiers = make([]*apiNotifier, 0)
|
lr.Data.Notifiers = make([]*notifier.ApiNotifier, 0)
|
||||||
for protoName, protoTargets := range targets {
|
for protoName, protoTargets := range targets {
|
||||||
notifier := &apiNotifier{
|
nr := ¬ifier.ApiNotifier{
|
||||||
Kind: string(protoName),
|
Kind: protoName,
|
||||||
Targets: make([]*apiTarget, 0, len(protoTargets)),
|
Targets: make([]*notifier.ApiTarget, 0, len(protoTargets)),
|
||||||
}
|
}
|
||||||
for _, target := range protoTargets {
|
for _, target := range protoTargets {
|
||||||
notifier.Targets = append(notifier.Targets, &apiTarget{
|
nr.Targets = append(nr.Targets, ¬ifier.ApiTarget{
|
||||||
Address: target.Addr(),
|
Address: target.Addr(),
|
||||||
Labels: target.Labels.ToMap(),
|
Labels: target.Labels.ToMap(),
|
||||||
|
LastError: target.LastError(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
lr.Data.Notifiers = append(lr.Data.Notifiers, notifier)
|
lr.Data.Notifiers = append(lr.Data.Notifiers, nr)
|
||||||
}
|
}
|
||||||
|
|
||||||
b, err := json.Marshal(lr)
|
b, err := json.Marshal(lr)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/tpl"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/tpl"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/vmalertutil"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/vmalertutil"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/rule"
|
||||||
) %}
|
) %}
|
||||||
|
|
||||||
{% func Controls(prefix, currentIcon, currentText string, icons, filters map[string]string, search bool) %}
|
{% func Controls(prefix, currentIcon, currentText string, icons, filters map[string]string, search bool) %}
|
||||||
@@ -93,7 +94,7 @@
|
|||||||
{%= tpl.Footer(r) %}
|
{%= tpl.Footer(r) %}
|
||||||
{% endfunc %}
|
{% endfunc %}
|
||||||
|
|
||||||
{% func ListGroups(r *http.Request, groups []*apiGroup, filter string) %}
|
{% func ListGroups(r *http.Request, groups []*rule.ApiGroup, filter string) %}
|
||||||
{%code
|
{%code
|
||||||
prefix := vmalertutil.Prefix(r.URL.Path)
|
prefix := vmalertutil.Prefix(r.URL.Path)
|
||||||
filters := map[string]string{
|
filters := map[string]string{
|
||||||
@@ -222,7 +223,7 @@
|
|||||||
{% endfunc %}
|
{% endfunc %}
|
||||||
|
|
||||||
|
|
||||||
{% func ListAlerts(r *http.Request, groupAlerts []groupAlerts) %}
|
{% func ListAlerts(r *http.Request, groupAlerts []rule.GroupAlerts) %}
|
||||||
{%code prefix := vmalertutil.Prefix(r.URL.Path) %}
|
{%code prefix := vmalertutil.Prefix(r.URL.Path) %}
|
||||||
{%= tpl.Header(r, navItems, "Alerts", getLastConfigError()) %}
|
{%= tpl.Header(r, navItems, "Alerts", getLastConfigError()) %}
|
||||||
{%= Controls(prefix, "", "", nil, nil, true) %}
|
{%= Controls(prefix, "", "", nil, nil, true) %}
|
||||||
@@ -231,7 +232,7 @@
|
|||||||
{%code
|
{%code
|
||||||
g := ga.Group
|
g := ga.Group
|
||||||
var keys []string
|
var keys []string
|
||||||
alertsByRule := make(map[string][]*apiAlert)
|
alertsByRule := make(map[string][]*rule.ApiAlert)
|
||||||
for _, alert := range ga.Alerts {
|
for _, alert := range ga.Alerts {
|
||||||
if len(alertsByRule[alert.RuleID]) < 1 {
|
if len(alertsByRule[alert.RuleID]) < 1 {
|
||||||
keys = append(keys, alert.RuleID)
|
keys = append(keys, alert.RuleID)
|
||||||
@@ -378,7 +379,7 @@
|
|||||||
{%= tpl.Footer(r) %}
|
{%= tpl.Footer(r) %}
|
||||||
{% endfunc %}
|
{% endfunc %}
|
||||||
|
|
||||||
{% func Alert(r *http.Request, alert *apiAlert) %}
|
{% func Alert(r *http.Request, alert *rule.ApiAlert) %}
|
||||||
{%code prefix := vmalertutil.Prefix(r.URL.Path) %}
|
{%code prefix := vmalertutil.Prefix(r.URL.Path) %}
|
||||||
{%= tpl.Header(r, navItems, "", getLastConfigError()) %}
|
{%= tpl.Header(r, navItems, "", getLastConfigError()) %}
|
||||||
{%code
|
{%code
|
||||||
@@ -464,7 +465,7 @@
|
|||||||
{% endfunc %}
|
{% endfunc %}
|
||||||
|
|
||||||
|
|
||||||
{% func RuleDetails(r *http.Request, rule apiRule) %}
|
{% func RuleDetails(r *http.Request, rule rule.ApiRule) %}
|
||||||
{%code prefix := vmalertutil.Prefix(r.URL.Path) %}
|
{%code prefix := vmalertutil.Prefix(r.URL.Path) %}
|
||||||
{%= tpl.Header(r, navItems, "", getLastConfigError()) %}
|
{%= tpl.Header(r, navItems, "", getLastConfigError()) %}
|
||||||
{%code
|
{%code
|
||||||
@@ -649,7 +650,7 @@
|
|||||||
<span class="badge bg-warning text-dark" title="This firing state is kept because of `keep_firing_for`">stabilizing</span>
|
<span class="badge bg-warning text-dark" title="This firing state is kept because of `keep_firing_for`">stabilizing</span>
|
||||||
{% endfunc %}
|
{% endfunc %}
|
||||||
|
|
||||||
{% func seriesFetchedWarn(prefix string, r apiRule) %}
|
{% func seriesFetchedWarn(prefix string, r rule.ApiRule) %}
|
||||||
{% if isNoMatch(r) %}
|
{% if isNoMatch(r) %}
|
||||||
<svg
|
<svg
|
||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
@@ -663,7 +664,7 @@
|
|||||||
{% endfunc %}
|
{% endfunc %}
|
||||||
|
|
||||||
{%code
|
{%code
|
||||||
func isNoMatch (r apiRule) bool {
|
func isNoMatch (r rule.ApiRule) bool {
|
||||||
return r.LastSamples == 0 && r.LastSeriesFetched != nil && *r.LastSeriesFetched == 0
|
return r.LastSamples == 0 && r.LastSeriesFetched != nil && *r.LastSeriesFetched == 0
|
||||||
}
|
}
|
||||||
%}
|
%}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,7 @@ func TestHandler(t *testing.T) {
|
|||||||
m := &manager{groups: map[uint64]*rule.Group{}}
|
m := &manager{groups: map[uint64]*rule.Group{}}
|
||||||
var ar *rule.AlertingRule
|
var ar *rule.AlertingRule
|
||||||
var rr *rule.RecordingRule
|
var rr *rule.RecordingRule
|
||||||
|
var groupIDs []uint64
|
||||||
for _, dsType := range []string{"prometheus", "", "graphite"} {
|
for _, dsType := range []string{"prometheus", "", "graphite"} {
|
||||||
g := rule.NewGroup(config.Group{
|
g := rule.NewGroup(config.Group{
|
||||||
Name: "group",
|
Name: "group",
|
||||||
@@ -45,7 +46,9 @@ func TestHandler(t *testing.T) {
|
|||||||
ar = g.Rules[0].(*rule.AlertingRule)
|
ar = g.Rules[0].(*rule.AlertingRule)
|
||||||
rr = g.Rules[1].(*rule.RecordingRule)
|
rr = g.Rules[1].(*rule.RecordingRule)
|
||||||
g.ExecOnce(context.Background(), func() []notifier.Notifier { return nil }, nil, time.Time{})
|
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}
|
rh := &requestHandler{m: m}
|
||||||
|
|
||||||
@@ -82,22 +85,22 @@ func TestHandler(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("/vmalert/rule", func(t *testing.T) {
|
t.Run("/vmalert/rule", func(t *testing.T) {
|
||||||
a := ruleToAPI(ar)
|
a := ar.ToAPI()
|
||||||
getResp(t, ts.URL+"/vmalert/"+a.WebLink(), nil, 200)
|
getResp(t, ts.URL+"/vmalert/"+a.WebLink(), nil, 200)
|
||||||
r := ruleToAPI(rr)
|
r := rr.ToAPI()
|
||||||
getResp(t, ts.URL+"/vmalert/"+r.WebLink(), nil, 200)
|
getResp(t, ts.URL+"/vmalert/"+r.WebLink(), nil, 200)
|
||||||
})
|
})
|
||||||
t.Run("/vmalert/alert", func(t *testing.T) {
|
t.Run("/vmalert/alert", func(t *testing.T) {
|
||||||
alerts := ruleToAPIAlert(ar)
|
alerts := ar.AlertsToAPI()
|
||||||
for _, a := range alerts {
|
for _, a := range alerts {
|
||||||
getResp(t, ts.URL+"/vmalert/"+a.WebLink(), nil, 200)
|
getResp(t, ts.URL+"/vmalert/"+a.WebLink(), nil, 200)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("/vmalert/rule?badParam", func(t *testing.T) {
|
t.Run("/vmalert/rule?badParam", func(t *testing.T) {
|
||||||
params := fmt.Sprintf("?%s=0&%s=1", paramGroupID, paramRuleID)
|
params := fmt.Sprintf("?%s=0&%s=1", rule.ParamGroupID, rule.ParamRuleID)
|
||||||
getResp(t, ts.URL+"/vmalert/rule"+params, nil, 404)
|
getResp(t, ts.URL+"/vmalert/rule"+params, nil, 404)
|
||||||
|
|
||||||
params = fmt.Sprintf("?%s=1&%s=0", paramGroupID, paramRuleID)
|
params = fmt.Sprintf("?%s=1&%s=0", rule.ParamGroupID, rule.ParamRuleID)
|
||||||
getResp(t, ts.URL+"/vmalert/rule"+params, nil, 404)
|
getResp(t, ts.URL+"/vmalert/rule"+params, nil, 404)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -124,14 +127,14 @@ func TestHandler(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("/api/v1/alert?alertID&groupID", func(t *testing.T) {
|
t.Run("/api/v1/alert?alertID&groupID", func(t *testing.T) {
|
||||||
expAlert := newAlertAPI(ar, ar.GetAlerts()[0])
|
expAlert := rule.NewAlertAPI(ar, ar.GetAlerts()[0])
|
||||||
alert := &apiAlert{}
|
alert := &rule.ApiAlert{}
|
||||||
getResp(t, ts.URL+"/"+expAlert.APILink(), alert, 200)
|
getResp(t, ts.URL+"/"+expAlert.APILink(), alert, 200)
|
||||||
if !reflect.DeepEqual(alert, expAlert) {
|
if !reflect.DeepEqual(alert, expAlert) {
|
||||||
t.Fatalf("expected %v is equal to %v", alert, expAlert)
|
t.Fatalf("expected %v is equal to %v", alert, expAlert)
|
||||||
}
|
}
|
||||||
|
|
||||||
alert = &apiAlert{}
|
alert = &rule.ApiAlert{}
|
||||||
getResp(t, ts.URL+"/vmalert/"+expAlert.APILink(), alert, 200)
|
getResp(t, ts.URL+"/vmalert/"+expAlert.APILink(), alert, 200)
|
||||||
if !reflect.DeepEqual(alert, expAlert) {
|
if !reflect.DeepEqual(alert, expAlert) {
|
||||||
t.Fatalf("expected %v is equal to %v", alert, expAlert)
|
t.Fatalf("expected %v is equal to %v", alert, expAlert)
|
||||||
@@ -139,16 +142,16 @@ func TestHandler(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("/api/v1/alert?badParams", func(t *testing.T) {
|
t.Run("/api/v1/alert?badParams", func(t *testing.T) {
|
||||||
params := fmt.Sprintf("?%s=0&%s=1", paramGroupID, paramAlertID)
|
params := fmt.Sprintf("?%s=0&%s=1", rule.ParamGroupID, rule.ParamAlertID)
|
||||||
getResp(t, ts.URL+"/api/v1/alert"+params, nil, 404)
|
getResp(t, ts.URL+"/api/v1/alert"+params, nil, 404)
|
||||||
getResp(t, ts.URL+"/vmalert/api/v1/alert"+params, nil, 404)
|
getResp(t, ts.URL+"/vmalert/api/v1/alert"+params, nil, 404)
|
||||||
|
|
||||||
params = fmt.Sprintf("?%s=1&%s=0", paramGroupID, paramAlertID)
|
params = fmt.Sprintf("?%s=1&%s=0", rule.ParamGroupID, rule.ParamAlertID)
|
||||||
getResp(t, ts.URL+"/api/v1/alert"+params, nil, 404)
|
getResp(t, ts.URL+"/api/v1/alert"+params, nil, 404)
|
||||||
getResp(t, ts.URL+"/vmalert/api/v1/alert"+params, nil, 404)
|
getResp(t, ts.URL+"/vmalert/api/v1/alert"+params, nil, 404)
|
||||||
|
|
||||||
// bad request, alertID is missing
|
// bad request, alertID is missing
|
||||||
params = fmt.Sprintf("?%s=1", paramGroupID)
|
params = fmt.Sprintf("?%s=1", rule.ParamGroupID)
|
||||||
getResp(t, ts.URL+"/api/v1/alert"+params, nil, 400)
|
getResp(t, ts.URL+"/api/v1/alert"+params, nil, 400)
|
||||||
getResp(t, ts.URL+"/vmalert/api/v1/alert"+params, nil, 400)
|
getResp(t, ts.URL+"/vmalert/api/v1/alert"+params, nil, 400)
|
||||||
})
|
})
|
||||||
@@ -167,27 +170,42 @@ func TestHandler(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("/api/v1/rule?ruleID&groupID", func(t *testing.T) {
|
t.Run("/api/v1/rule?ruleID&groupID", func(t *testing.T) {
|
||||||
expRule := ruleToAPI(ar)
|
expRule := ar.ToAPI()
|
||||||
gotRule := apiRule{}
|
gotRule := rule.ApiRule{}
|
||||||
getResp(t, ts.URL+"/"+expRule.APILink(), &gotRule, 200)
|
getResp(t, ts.URL+"/"+expRule.APILink(), &gotRule, 200)
|
||||||
|
|
||||||
if expRule.ID != gotRule.ID {
|
if expRule.ID != gotRule.ID {
|
||||||
t.Fatalf("expected to get Rule %q; got %q instead", expRule.ID, gotRule.ID)
|
t.Fatalf("expected to get Rule %q; got %q instead", expRule.ID, gotRule.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
gotRule = apiRule{}
|
gotRule = rule.ApiRule{}
|
||||||
getResp(t, ts.URL+"/vmalert/"+expRule.APILink(), &gotRule, 200)
|
getResp(t, ts.URL+"/vmalert/"+expRule.APILink(), &gotRule, 200)
|
||||||
|
|
||||||
if expRule.ID != gotRule.ID {
|
if expRule.ID != gotRule.ID {
|
||||||
t.Fatalf("expected to get Rule %q; got %q instead", expRule.ID, gotRule.ID)
|
t.Fatalf("expected to get Rule %q; got %q instead", expRule.ID, gotRule.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
gotRuleWithUpdates := apiRuleWithUpdates{}
|
gotRuleWithUpdates := rule.ApiRuleWithUpdates{}
|
||||||
getResp(t, ts.URL+"/"+expRule.APILink(), &gotRuleWithUpdates, 200)
|
getResp(t, ts.URL+"/"+expRule.APILink(), &gotRuleWithUpdates, 200)
|
||||||
if len(gotRuleWithUpdates.StateUpdates) < 1 {
|
if len(gotRuleWithUpdates.StateUpdates) < 1 {
|
||||||
t.Fatalf("expected %+v to have state updates field not empty", gotRuleWithUpdates.StateUpdates)
|
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 := g.ToAPI()
|
||||||
|
gotGroup := rule.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 = rule.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) {
|
t.Run("/api/v1/rules&filters", func(t *testing.T) {
|
||||||
check := func(url string, statusCode, expGroups, expRules int) {
|
check := func(url string, statusCode, expGroups, expRules int) {
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ var (
|
|||||||
"See https://docs.victoriametrics.com/victoriametrics/vmauth/#load-balancing for details")
|
"See https://docs.victoriametrics.com/victoriametrics/vmauth/#load-balancing for details")
|
||||||
defaultLoadBalancingPolicy = flag.String("loadBalancingPolicy", "least_loaded", "The default load balancing policy to use for backend urls specified inside url_prefix section. "+
|
defaultLoadBalancingPolicy = flag.String("loadBalancingPolicy", "least_loaded", "The default load balancing policy to use for backend urls specified inside url_prefix section. "+
|
||||||
"Supported policies: least_loaded, first_available. See https://docs.victoriametrics.com/victoriametrics/vmauth/#load-balancing")
|
"Supported policies: least_loaded, first_available. See https://docs.victoriametrics.com/victoriametrics/vmauth/#load-balancing")
|
||||||
|
defaultMergeQueryArgs = flagutil.NewArrayString("mergeQueryArgs", "An optional list of client query arg names, which must be merged with args at backend urls. "+
|
||||||
|
"The rest of client query args are replaced by the corresponding query args from backend urls for security reasons; "+
|
||||||
|
"see https://docs.victoriametrics.com/victoriametrics/vmauth/#query-args-handling")
|
||||||
discoverBackendIPsGlobal = flag.Bool("discoverBackendIPs", false, "Whether to discover backend IPs via periodic DNS queries to hostnames specified in url_prefix. "+
|
discoverBackendIPsGlobal = flag.Bool("discoverBackendIPs", false, "Whether to discover backend IPs via periodic DNS queries to hostnames specified in url_prefix. "+
|
||||||
"This may be useful when url_prefix points to a hostname with dynamically scaled instances behind it. See https://docs.victoriametrics.com/victoriametrics/vmauth/#discovering-backend-ips")
|
"This may be useful when url_prefix points to a hostname with dynamically scaled instances behind it. See https://docs.victoriametrics.com/victoriametrics/vmauth/#discovering-backend-ips")
|
||||||
discoverBackendIPsInterval = flag.Duration("discoverBackendIPsInterval", 10*time.Second, "The interval for re-discovering backend IPs if -discoverBackendIPs command-line flag is set. "+
|
discoverBackendIPsInterval = flag.Duration("discoverBackendIPsInterval", 10*time.Second, "The interval for re-discovering backend IPs if -discoverBackendIPs command-line flag is set. "+
|
||||||
@@ -75,6 +78,7 @@ type UserInfo struct {
|
|||||||
DefaultURL *URLPrefix `yaml:"default_url,omitempty"`
|
DefaultURL *URLPrefix `yaml:"default_url,omitempty"`
|
||||||
RetryStatusCodes []int `yaml:"retry_status_codes,omitempty"`
|
RetryStatusCodes []int `yaml:"retry_status_codes,omitempty"`
|
||||||
LoadBalancingPolicy string `yaml:"load_balancing_policy,omitempty"`
|
LoadBalancingPolicy string `yaml:"load_balancing_policy,omitempty"`
|
||||||
|
MergeQueryArgs []string `yaml:"merge_query_args,omitempty"`
|
||||||
DropSrcPathPrefixParts *int `yaml:"drop_src_path_prefix_parts,omitempty"`
|
DropSrcPathPrefixParts *int `yaml:"drop_src_path_prefix_parts,omitempty"`
|
||||||
TLSCAFile string `yaml:"tls_ca_file,omitempty"`
|
TLSCAFile string `yaml:"tls_ca_file,omitempty"`
|
||||||
TLSCertFile string `yaml:"tls_cert_file,omitempty"`
|
TLSCertFile string `yaml:"tls_cert_file,omitempty"`
|
||||||
@@ -182,6 +186,11 @@ type URLMap struct {
|
|||||||
// LoadBalancingPolicy is load balancing policy among UrlPrefix backends.
|
// LoadBalancingPolicy is load balancing policy among UrlPrefix backends.
|
||||||
LoadBalancingPolicy string `yaml:"load_balancing_policy,omitempty"`
|
LoadBalancingPolicy string `yaml:"load_balancing_policy,omitempty"`
|
||||||
|
|
||||||
|
// MergeQueryArgs is a list of client query args, which must be merged with the existing backend query args.
|
||||||
|
//
|
||||||
|
// The rest of client query args are replaced with the corresponding backend query args for security reasons.
|
||||||
|
MergeQueryArgs []string `yaml:"merge_query_args,omitempty"`
|
||||||
|
|
||||||
// DropSrcPathPrefixParts is the number of `/`-delimited request path prefix parts to drop before proxying the request to backend.
|
// DropSrcPathPrefixParts is the number of `/`-delimited request path prefix parts to drop before proxying the request to backend.
|
||||||
DropSrcPathPrefixParts *int `yaml:"drop_src_path_prefix_parts,omitempty"`
|
DropSrcPathPrefixParts *int `yaml:"drop_src_path_prefix_parts,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -228,7 +237,7 @@ func (qa *QueryArg) MarshalYAML() (any, error) {
|
|||||||
return qa.sOriginal, nil
|
return qa.sOriginal, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// URLPrefix represents passed `url_prefix`
|
// URLPrefix represents the `url_prefix` from auth config.
|
||||||
type URLPrefix struct {
|
type URLPrefix struct {
|
||||||
// requests are re-tried on other backend urls for these http response status codes
|
// requests are re-tried on other backend urls for these http response status codes
|
||||||
retryStatusCodes []int
|
retryStatusCodes []int
|
||||||
@@ -236,6 +245,11 @@ type URLPrefix struct {
|
|||||||
// load balancing policy used
|
// load balancing policy used
|
||||||
loadBalancingPolicy string
|
loadBalancingPolicy string
|
||||||
|
|
||||||
|
// the list of client query args, which must be merged with backend query args.
|
||||||
|
//
|
||||||
|
// By default backend query args replace all the client query args for security reasons.
|
||||||
|
mergeQueryArgs []string
|
||||||
|
|
||||||
// how many request path prefix parts to drop before routing the request to backendURL
|
// how many request path prefix parts to drop before routing the request to backendURL
|
||||||
dropSrcPathPrefixParts int
|
dropSrcPathPrefixParts int
|
||||||
|
|
||||||
@@ -856,6 +870,7 @@ func (ui *UserInfo) getMetricLabels() (string, error) {
|
|||||||
func (ui *UserInfo) initURLs() error {
|
func (ui *UserInfo) initURLs() error {
|
||||||
retryStatusCodes := defaultRetryStatusCodes.Values()
|
retryStatusCodes := defaultRetryStatusCodes.Values()
|
||||||
loadBalancingPolicy := *defaultLoadBalancingPolicy
|
loadBalancingPolicy := *defaultLoadBalancingPolicy
|
||||||
|
mergeQueryArgs := *defaultMergeQueryArgs
|
||||||
dropSrcPathPrefixParts := 0
|
dropSrcPathPrefixParts := 0
|
||||||
discoverBackendIPs := *discoverBackendIPsGlobal
|
discoverBackendIPs := *discoverBackendIPsGlobal
|
||||||
if ui.RetryStatusCodes != nil {
|
if ui.RetryStatusCodes != nil {
|
||||||
@@ -864,6 +879,9 @@ func (ui *UserInfo) initURLs() error {
|
|||||||
if ui.LoadBalancingPolicy != "" {
|
if ui.LoadBalancingPolicy != "" {
|
||||||
loadBalancingPolicy = ui.LoadBalancingPolicy
|
loadBalancingPolicy = ui.LoadBalancingPolicy
|
||||||
}
|
}
|
||||||
|
if len(ui.MergeQueryArgs) != 0 {
|
||||||
|
mergeQueryArgs = ui.MergeQueryArgs
|
||||||
|
}
|
||||||
if ui.DropSrcPathPrefixParts != nil {
|
if ui.DropSrcPathPrefixParts != nil {
|
||||||
dropSrcPathPrefixParts = *ui.DropSrcPathPrefixParts
|
dropSrcPathPrefixParts = *ui.DropSrcPathPrefixParts
|
||||||
}
|
}
|
||||||
@@ -871,16 +889,18 @@ func (ui *UserInfo) initURLs() error {
|
|||||||
discoverBackendIPs = *ui.DiscoverBackendIPs
|
discoverBackendIPs = *ui.DiscoverBackendIPs
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui.URLPrefix != nil {
|
up := ui.URLPrefix
|
||||||
if err := ui.URLPrefix.sanitizeAndInitialize(); err != nil {
|
if up != nil {
|
||||||
|
if err := up.sanitizeAndInitialize(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
ui.URLPrefix.retryStatusCodes = retryStatusCodes
|
up.retryStatusCodes = retryStatusCodes
|
||||||
ui.URLPrefix.dropSrcPathPrefixParts = dropSrcPathPrefixParts
|
up.dropSrcPathPrefixParts = dropSrcPathPrefixParts
|
||||||
ui.URLPrefix.discoverBackendIPs = discoverBackendIPs
|
up.discoverBackendIPs = discoverBackendIPs
|
||||||
if err := ui.URLPrefix.setLoadBalancingPolicy(loadBalancingPolicy); err != nil {
|
if err := up.setLoadBalancingPolicy(loadBalancingPolicy); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
up.mergeQueryArgs = mergeQueryArgs
|
||||||
}
|
}
|
||||||
if ui.DefaultURL != nil {
|
if ui.DefaultURL != nil {
|
||||||
if err := ui.DefaultURL.sanitizeAndInitialize(); err != nil {
|
if err := ui.DefaultURL.sanitizeAndInitialize(); err != nil {
|
||||||
@@ -899,6 +919,7 @@ func (ui *UserInfo) initURLs() error {
|
|||||||
}
|
}
|
||||||
rscs := retryStatusCodes
|
rscs := retryStatusCodes
|
||||||
lbp := loadBalancingPolicy
|
lbp := loadBalancingPolicy
|
||||||
|
mqa := mergeQueryArgs
|
||||||
dsp := dropSrcPathPrefixParts
|
dsp := dropSrcPathPrefixParts
|
||||||
dbd := discoverBackendIPs
|
dbd := discoverBackendIPs
|
||||||
if e.RetryStatusCodes != nil {
|
if e.RetryStatusCodes != nil {
|
||||||
@@ -907,6 +928,9 @@ func (ui *UserInfo) initURLs() error {
|
|||||||
if e.LoadBalancingPolicy != "" {
|
if e.LoadBalancingPolicy != "" {
|
||||||
lbp = e.LoadBalancingPolicy
|
lbp = e.LoadBalancingPolicy
|
||||||
}
|
}
|
||||||
|
if len(e.MergeQueryArgs) != 0 {
|
||||||
|
mqa = e.MergeQueryArgs
|
||||||
|
}
|
||||||
if e.DropSrcPathPrefixParts != nil {
|
if e.DropSrcPathPrefixParts != nil {
|
||||||
dsp = *e.DropSrcPathPrefixParts
|
dsp = *e.DropSrcPathPrefixParts
|
||||||
}
|
}
|
||||||
@@ -917,6 +941,7 @@ func (ui *UserInfo) initURLs() error {
|
|||||||
if err := e.URLPrefix.setLoadBalancingPolicy(lbp); err != nil {
|
if err := e.URLPrefix.setLoadBalancingPolicy(lbp); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
e.URLPrefix.mergeQueryArgs = mqa
|
||||||
e.URLPrefix.dropSrcPathPrefixParts = dsp
|
e.URLPrefix.dropSrcPathPrefixParts = dsp
|
||||||
e.URLPrefix.discoverBackendIPs = dbd
|
e.URLPrefix.discoverBackendIPs = dbd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ users:
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseAuthConfigSuccess(t *testing.T) {
|
func TestParseAuthConfigSuccess(t *testing.T) {
|
||||||
f := func(s string, expectedAuthConfig map[string]*UserInfo) {
|
f := func(s string, expectedAuthConfig map[string]*UserInfo, expectedUnauthorizedUserConfig *UserInfo) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
ac, err := parseAuthConfig([]byte(s))
|
ac, err := parseAuthConfig([]byte(s))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -294,15 +294,19 @@ func TestParseAuthConfigSuccess(t *testing.T) {
|
|||||||
if err := areEqualConfigs(m, expectedAuthConfig); err != nil {
|
if err := areEqualConfigs(m, expectedAuthConfig); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := areEqualConfigs(ac.UnauthorizedUser, expectedUnauthorizedUserConfig); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
insecureSkipVerifyTrue := true
|
insecureSkipVerifyTrue := true
|
||||||
|
|
||||||
// Empty config
|
// Empty config
|
||||||
f(``, map[string]*UserInfo{})
|
f(``, map[string]*UserInfo{}, nil)
|
||||||
|
|
||||||
// Empty users
|
// Empty users
|
||||||
f(`users: []`, map[string]*UserInfo{})
|
f(`users: []`, map[string]*UserInfo{}, nil)
|
||||||
|
|
||||||
// Single user
|
// Single user
|
||||||
f(`
|
f(`
|
||||||
@@ -320,7 +324,7 @@ users:
|
|||||||
MaxConcurrentRequests: 5,
|
MaxConcurrentRequests: 5,
|
||||||
TLSInsecureSkipVerify: &insecureSkipVerifyTrue,
|
TLSInsecureSkipVerify: &insecureSkipVerifyTrue,
|
||||||
},
|
},
|
||||||
})
|
}, nil)
|
||||||
|
|
||||||
// Single user with auth_token
|
// Single user with auth_token
|
||||||
f(`
|
f(`
|
||||||
@@ -344,7 +348,7 @@ users:
|
|||||||
TLSCertFile: "foo/baz",
|
TLSCertFile: "foo/baz",
|
||||||
TLSKeyFile: "foo/foo",
|
TLSKeyFile: "foo/foo",
|
||||||
},
|
},
|
||||||
})
|
}, nil)
|
||||||
|
|
||||||
// Multiple url_prefix entries
|
// Multiple url_prefix entries
|
||||||
insecureSkipVerifyFalse := false
|
insecureSkipVerifyFalse := false
|
||||||
@@ -359,6 +363,7 @@ users:
|
|||||||
tls_insecure_skip_verify: false
|
tls_insecure_skip_verify: false
|
||||||
retry_status_codes: [500, 501]
|
retry_status_codes: [500, 501]
|
||||||
load_balancing_policy: first_available
|
load_balancing_policy: first_available
|
||||||
|
merge_query_args: [foo, bar]
|
||||||
drop_src_path_prefix_parts: 1
|
drop_src_path_prefix_parts: 1
|
||||||
discover_backend_ips: true
|
discover_backend_ips: true
|
||||||
`, map[string]*UserInfo{
|
`, map[string]*UserInfo{
|
||||||
@@ -372,10 +377,11 @@ users:
|
|||||||
TLSInsecureSkipVerify: &insecureSkipVerifyFalse,
|
TLSInsecureSkipVerify: &insecureSkipVerifyFalse,
|
||||||
RetryStatusCodes: []int{500, 501},
|
RetryStatusCodes: []int{500, 501},
|
||||||
LoadBalancingPolicy: "first_available",
|
LoadBalancingPolicy: "first_available",
|
||||||
|
MergeQueryArgs: []string{"foo", "bar"},
|
||||||
DropSrcPathPrefixParts: intp(1),
|
DropSrcPathPrefixParts: intp(1),
|
||||||
DiscoverBackendIPs: &discoverBackendIPsTrue,
|
DiscoverBackendIPs: &discoverBackendIPsTrue,
|
||||||
},
|
},
|
||||||
})
|
}, nil)
|
||||||
|
|
||||||
// Multiple users
|
// Multiple users
|
||||||
f(`
|
f(`
|
||||||
@@ -393,7 +399,7 @@ users:
|
|||||||
Username: "bar",
|
Username: "bar",
|
||||||
URLPrefix: mustParseURL("https://bar/x/"),
|
URLPrefix: mustParseURL("https://bar/x/"),
|
||||||
},
|
},
|
||||||
})
|
}, nil)
|
||||||
|
|
||||||
// non-empty URLMap
|
// non-empty URLMap
|
||||||
sharedUserInfo := &UserInfo{
|
sharedUserInfo := &UserInfo{
|
||||||
@@ -443,7 +449,7 @@ users:
|
|||||||
`, map[string]*UserInfo{
|
`, map[string]*UserInfo{
|
||||||
getHTTPAuthBearerToken("foo"): sharedUserInfo,
|
getHTTPAuthBearerToken("foo"): sharedUserInfo,
|
||||||
getHTTPAuthBasicToken("foo", ""): sharedUserInfo,
|
getHTTPAuthBasicToken("foo", ""): sharedUserInfo,
|
||||||
})
|
}, nil)
|
||||||
|
|
||||||
// Multiple users with the same name - this should work, since these users have different passwords
|
// Multiple users with the same name - this should work, since these users have different passwords
|
||||||
f(`
|
f(`
|
||||||
@@ -465,7 +471,7 @@ users:
|
|||||||
Password: "bar",
|
Password: "bar",
|
||||||
URLPrefix: mustParseURL("https://bar/x"),
|
URLPrefix: mustParseURL("https://bar/x"),
|
||||||
},
|
},
|
||||||
})
|
}, nil)
|
||||||
|
|
||||||
// with default url
|
// with default url
|
||||||
keepOriginalHost := true
|
keepOriginalHost := true
|
||||||
@@ -481,6 +487,8 @@ users:
|
|||||||
- "foo: bar"
|
- "foo: bar"
|
||||||
- "xxx: y"
|
- "xxx: y"
|
||||||
keep_original_host: true
|
keep_original_host: true
|
||||||
|
load_balancing_policy: first_available
|
||||||
|
merge_query_args: [foo, bar]
|
||||||
default_url:
|
default_url:
|
||||||
- http://default1/select/0/prometheus
|
- http://default1/select/0/prometheus
|
||||||
- http://default2/select/0/prometheus
|
- http://default2/select/0/prometheus
|
||||||
@@ -505,6 +513,8 @@ users:
|
|||||||
},
|
},
|
||||||
KeepOriginalHost: &keepOriginalHost,
|
KeepOriginalHost: &keepOriginalHost,
|
||||||
},
|
},
|
||||||
|
LoadBalancingPolicy: "first_available",
|
||||||
|
MergeQueryArgs: []string{"foo", "bar"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
DefaultURL: mustParseURLs([]string{
|
DefaultURL: mustParseURLs([]string{
|
||||||
@@ -532,6 +542,8 @@ users:
|
|||||||
},
|
},
|
||||||
KeepOriginalHost: &keepOriginalHost,
|
KeepOriginalHost: &keepOriginalHost,
|
||||||
},
|
},
|
||||||
|
LoadBalancingPolicy: "first_available",
|
||||||
|
MergeQueryArgs: []string{"foo", "bar"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
DefaultURL: mustParseURLs([]string{
|
DefaultURL: mustParseURLs([]string{
|
||||||
@@ -539,7 +551,7 @@ users:
|
|||||||
"http://default2/select/0/prometheus",
|
"http://default2/select/0/prometheus",
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
})
|
}, nil)
|
||||||
|
|
||||||
// With metric_labels
|
// With metric_labels
|
||||||
f(`
|
f(`
|
||||||
@@ -591,6 +603,23 @@ users:
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
// unauthorized_user
|
||||||
|
f(`
|
||||||
|
unauthorized_user:
|
||||||
|
merge_query_args: [extra_filters]
|
||||||
|
url_map:
|
||||||
|
- src_paths: ["/select/.+"]
|
||||||
|
url_prefix: 'http://victoria-logs:9428/?extra_filters={env="prod"}'
|
||||||
|
`, nil, &UserInfo{
|
||||||
|
MergeQueryArgs: []string{"extra_filters"},
|
||||||
|
URLMaps: []URLMap{
|
||||||
|
{
|
||||||
|
SrcPaths: getRegexs([]string{"/select/.+"}),
|
||||||
|
URLPrefix: mustParseURL(`http://victoria-logs:9428/?extra_filters={env="prod"}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -884,7 +913,7 @@ func removeMetrics(m map[string]*UserInfo) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func areEqualConfigs(a, b map[string]*UserInfo) error {
|
func areEqualConfigs(a, b any) error {
|
||||||
aData, err := yaml.Marshal(a)
|
aData, err := yaml.Marshal(a)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot marshal a: %w", err)
|
return fmt.Errorf("cannot marshal a: %w", err)
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ func processRequest(w http.ResponseWriter, r *http.Request, ui *UserInfo) {
|
|||||||
query.Set("request_path", u.String())
|
query.Set("request_path", u.String())
|
||||||
targetURL.RawQuery = query.Encode()
|
targetURL.RawQuery = query.Encode()
|
||||||
} else { // Update path for regular routes.
|
} else { // Update path for regular routes.
|
||||||
targetURL = mergeURLs(targetURL, u, up.dropSrcPathPrefixParts)
|
targetURL = mergeURLs(targetURL, u, up.dropSrcPathPrefixParts, up.mergeQueryArgs)
|
||||||
}
|
}
|
||||||
|
|
||||||
wasLocalRetry := false
|
wasLocalRetry := false
|
||||||
@@ -372,20 +372,54 @@ func tryProcessingRequest(w http.ResponseWriter, r *http.Request, targetURL *url
|
|||||||
updateHeadersByConfig(w.Header(), hc.ResponseHeaders)
|
updateHeadersByConfig(w.Header(), hc.ResponseHeaders)
|
||||||
w.WriteHeader(res.StatusCode)
|
w.WriteHeader(res.StatusCode)
|
||||||
|
|
||||||
copyBuf := copyBufPool.Get()
|
err = copyStreamToClient(w, res.Body)
|
||||||
copyBuf.B = bytesutil.ResizeNoCopyNoOverallocate(copyBuf.B, 16*1024)
|
|
||||||
_, err = io.CopyBuffer(w, res.Body, copyBuf.B)
|
|
||||||
copyBufPool.Put(copyBuf)
|
|
||||||
_ = res.Body.Close()
|
_ = res.Body.Close()
|
||||||
if err != nil && !netutil.IsTrivialNetworkError(err) {
|
if err != nil && !netutil.IsTrivialNetworkError(err) && !errors.Is(err, context.Canceled) {
|
||||||
remoteAddr := httpserver.GetQuotedRemoteAddr(r)
|
remoteAddr := httpserver.GetQuotedRemoteAddr(r)
|
||||||
requestURI := httpserver.GetRequestURI(r)
|
requestURI := httpserver.GetRequestURI(r)
|
||||||
|
|
||||||
logger.Warnf("remoteAddr: %s; requestURI: %s; error when proxying response body from %s: %s", remoteAddr, requestURI, targetURL, err)
|
logger.Warnf("remoteAddr: %s; requestURI: %s; error when proxying response body from %s: %s", remoteAddr, requestURI, targetURL, err)
|
||||||
return true, false
|
return true, false
|
||||||
}
|
}
|
||||||
return true, false
|
return true, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func copyStreamToClient(client io.Writer, backend io.Reader) error {
|
||||||
|
copyBuf := copyBufPool.Get()
|
||||||
|
copyBuf.B = bytesutil.ResizeNoCopyNoOverallocate(copyBuf.B, 16*1024)
|
||||||
|
defer copyBufPool.Put(copyBuf)
|
||||||
|
buf := copyBuf.B
|
||||||
|
|
||||||
|
flusher, ok := client.(http.Flusher)
|
||||||
|
if !ok {
|
||||||
|
logger.Panicf("BUG: client must implement net/http.Flusher interface; got %T", client)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
n, backendErr := backend.Read(buf)
|
||||||
|
if n > 0 {
|
||||||
|
data := buf[:n]
|
||||||
|
n, clientErr := client.Write(data)
|
||||||
|
if clientErr != nil {
|
||||||
|
return fmt.Errorf("cannot write data to client: %w", clientErr)
|
||||||
|
}
|
||||||
|
if n != len(data) {
|
||||||
|
logger.Panicf("BUG: unexpected number of bytes written returned by client.Write; got %d; want %d", n, len(data))
|
||||||
|
}
|
||||||
|
// Flush the read data from the backend to the client as fast as possible
|
||||||
|
// in order to reduce delays for data propagation.
|
||||||
|
// See https://github.com/VictoriaMetrics/VictoriaLogs/issues/667
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
if backendErr != nil {
|
||||||
|
if backendErr == io.EOF {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot read data from backend: %w", backendErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var copyBufPool bytesutil.ByteBufferPool
|
var copyBufPool bytesutil.ByteBufferPool
|
||||||
|
|
||||||
func copyHeader(dst, src http.Header) {
|
func copyHeader(dst, src http.Header) {
|
||||||
|
|||||||
@@ -514,6 +514,11 @@ func (w *fakeResponseWriter) getResponse() string {
|
|||||||
return w.bb.String()
|
return w.bb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flush implements net/http.Flusher
|
||||||
|
func (w *fakeResponseWriter) Flush() {
|
||||||
|
// Nothing to do.
|
||||||
|
}
|
||||||
|
|
||||||
func (w *fakeResponseWriter) Header() http.Header {
|
func (w *fakeResponseWriter) Header() http.Header {
|
||||||
if w.h == nil {
|
if w.h == nil {
|
||||||
w.h = http.Header{}
|
w.h = http.Header{}
|
||||||
|
|||||||
@@ -8,29 +8,42 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func mergeURLs(uiURL, requestURI *url.URL, dropSrcPathPrefixParts int) *url.URL {
|
func mergeURLs(uiURL, requestURI *url.URL, dropSrcPathPrefixParts int, mergeQueryArgs []string) *url.URL {
|
||||||
targetURL := *uiURL
|
targetURL := *uiURL
|
||||||
|
|
||||||
srcPath := dropPrefixParts(requestURI.Path, dropSrcPathPrefixParts)
|
srcPath := dropPrefixParts(requestURI.Path, dropSrcPathPrefixParts)
|
||||||
if strings.HasPrefix(srcPath, "/") {
|
if strings.HasPrefix(srcPath, "/") {
|
||||||
targetURL.Path = strings.TrimSuffix(targetURL.Path, "/")
|
targetURL.Path = strings.TrimSuffix(targetURL.Path, "/")
|
||||||
}
|
}
|
||||||
targetURL.Path += srcPath
|
targetURL.Path += srcPath
|
||||||
requestParams := requestURI.Query()
|
requestParams := requestURI.Query()
|
||||||
// fast path
|
|
||||||
if len(requestParams) == 0 {
|
if len(requestParams) == 0 {
|
||||||
return &targetURL
|
return &targetURL
|
||||||
}
|
}
|
||||||
// merge query parameters from requests.
|
|
||||||
uiParams := targetURL.Query()
|
// Merge client query args with backend query args
|
||||||
|
targetParams := targetURL.Query()
|
||||||
|
uiParams := url.Values{}
|
||||||
|
|
||||||
|
// Copy all the target query args
|
||||||
|
for k, v := range targetParams {
|
||||||
|
for i := range v {
|
||||||
|
uiParams.Add(k, v[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the client query args if they do not clash with target args.
|
||||||
for k, v := range requestParams {
|
for k, v := range requestParams {
|
||||||
// skip clashed query params from original request
|
if targetParams.Has(k) && !slices.Contains(mergeQueryArgs, k) {
|
||||||
if exist := uiParams.Get(k); len(exist) > 0 {
|
// Skip clashed client query params for security reasons
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for i := range v {
|
for i := range v {
|
||||||
uiParams.Add(k, v[i])
|
uiParams.Add(k, v[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
targetURL.RawQuery = uiParams.Encode()
|
targetURL.RawQuery = uiParams.Encode()
|
||||||
return &targetURL
|
return &targetURL
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ func TestCreateTargetURLSuccess(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
bu := up.getBackendURL()
|
bu := up.getBackendURL()
|
||||||
target := mergeURLs(bu.url, u, up.dropSrcPathPrefixParts)
|
target := mergeURLs(bu.url, u, up.dropSrcPathPrefixParts, up.mergeQueryArgs)
|
||||||
bu.put()
|
bu.put()
|
||||||
|
|
||||||
gotTarget := target.String()
|
gotTarget := target.String()
|
||||||
@@ -352,7 +352,7 @@ func TestUserInfoGetBackendURL_SRV(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
bu := up.getBackendURL()
|
bu := up.getBackendURL()
|
||||||
target := mergeURLs(bu.url, u, up.dropSrcPathPrefixParts)
|
target := mergeURLs(bu.url, u, up.dropSrcPathPrefixParts, up.mergeQueryArgs)
|
||||||
bu.put()
|
bu.put()
|
||||||
|
|
||||||
gotTarget := target.String()
|
gotTarget := target.String()
|
||||||
@@ -528,3 +528,43 @@ func (r *fakeResolver) LookupIPAddr(_ context.Context, host string) ([]net.IPAdd
|
|||||||
func (r *fakeResolver) LookupMX(_ context.Context, _ string) ([]*net.MX, error) {
|
func (r *fakeResolver) LookupMX(_ context.Context, _ string) ([]*net.MX, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMergeURLs(t *testing.T) {
|
||||||
|
f := func(clientURL, backendURL string, dropSrcPathPrefixParts int, mergeQueryArgs []string, resultURLExpected string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
cu, err := url.Parse(clientURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cannot parse client url %q: %s", clientURL, err)
|
||||||
|
}
|
||||||
|
cu = normalizeURL(cu)
|
||||||
|
|
||||||
|
bu, err := url.Parse(backendURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cannot parse backend url %q: %s", backendURL, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ru := mergeURLs(bu, cu, dropSrcPathPrefixParts, mergeQueryArgs)
|
||||||
|
resultURL := ru.String()
|
||||||
|
if resultURL != resultURLExpected {
|
||||||
|
t.Fatalf("unexpected resultURL\ngot\n%s\nwant\n%s", resultURL, resultURLExpected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f("http://foo:1234", "https://backend/foo/bar?baz=abc&de", 0, nil, "https://backend/foo/bar?baz=abc&de")
|
||||||
|
f("http://foo:1234", "https://backend/foo/bar/?baz=abc&de", 0, nil, "https://backend/foo/bar/?baz=abc&de")
|
||||||
|
f("https://foo:1234/", "https://backend/foo/bar?baz=abc&de", 0, nil, "https://backend/foo/bar?baz=abc&de")
|
||||||
|
f("https://foo:1234/", "http://backend:8888/foo/bar/?baz=abc&de", 0, nil, "http://backend:8888/foo/bar/?baz=abc&de")
|
||||||
|
|
||||||
|
// merge paths
|
||||||
|
f("http://foo:1234/x/y?z=xxx", "https://backend/foo/bar?baz=abc&de", 0, nil, "https://backend/foo/bar/x/y?baz=abc&de=&z=xxx")
|
||||||
|
|
||||||
|
// "hacky" url
|
||||||
|
f("http://foo:1234/../../x/../y?z=xxx", "https://backend/foo/bar?baz=abc&de", 0, nil, "https://backend/foo/bar/y?baz=abc&de=&z=xxx")
|
||||||
|
|
||||||
|
// make sure that the client args are overridden by server args by default
|
||||||
|
f("http://foo:1234/x/y?password=hack&qqq=www", "https://backend/foo/bar?password=abc", 0, nil, "https://backend/foo/bar/x/y?password=abc&qqq=www")
|
||||||
|
|
||||||
|
// allow overriding the selected query args
|
||||||
|
f("http://foo:1234/x/y?baz=xxx&qqq=www", "https://backend/foo/bar?baz=abc", 0, []string{"baz"}, "https://backend/foo/bar/x/y?baz=abc&baz=xxx&qqq=www")
|
||||||
|
}
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("cannot create backup: %s", err)
|
logger.Fatalf("cannot create backup: %s", err)
|
||||||
}
|
}
|
||||||
pushmetrics.Stop()
|
pushmetrics.StopAndPush()
|
||||||
|
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
logger.Infof("gracefully shutting down http server for metrics at %q", listenAddrs)
|
logger.Infof("gracefully shutting down http server for metrics at %q", listenAddrs)
|
||||||
|
|||||||
@@ -63,10 +63,7 @@ func (ts *TimeSeries) write(w io.Writer) (int, error) {
|
|||||||
// Split long lines with more than 10K samples into multiple JSON lines.
|
// Split long lines with more than 10K samples into multiple JSON lines.
|
||||||
// This should limit memory usage at VictoriaMetrics during data ingestion,
|
// This should limit memory usage at VictoriaMetrics during data ingestion,
|
||||||
// since it allocates memory for the whole JSON line and processes it in one go.
|
// since it allocates memory for the whole JSON line and processes it in one go.
|
||||||
batchSize := 10000
|
batchSize := min(10000, len(timestamps))
|
||||||
if batchSize > len(timestamps) {
|
|
||||||
batchSize = len(timestamps)
|
|
||||||
}
|
|
||||||
timestampsBatch := timestamps[:batchSize]
|
timestampsBatch := timestamps[:batchSize]
|
||||||
valuesBatch := values[:batchSize]
|
valuesBatch := values[:batchSize]
|
||||||
timestamps = timestamps[batchSize:]
|
timestamps = timestamps[batchSize:]
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ func (p *vmNativeProcessor) runSingle(ctx context.Context, f native.Filter, srcU
|
|||||||
pr := bar.NewProxyReader(reader)
|
pr := bar.NewProxyReader(reader)
|
||||||
if pr != nil {
|
if pr != nil {
|
||||||
reader = pr
|
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}
|
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...)
|
log.Printf(initMessage, initParams...)
|
||||||
if len(ranges) > 1 {
|
if len(ranges) > 1 {
|
||||||
log.Printf("Selected time range will be split into %d ranges according to %q step", len(ranges), p.filter.Chunk)
|
log.Printf("Selected time range will be split into %d ranges according to %q step", len(ranges), p.filter.Chunk)
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/common"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/common"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/relabel"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/relabel"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prommetadata"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
|
"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"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus/stream"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus/stream"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
|
||||||
@@ -30,7 +30,7 @@ func InsertHandler(req *http.Request) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
encoding := req.Header.Get("Content-Encoding")
|
encoding := req.Header.Get("Content-Encoding")
|
||||||
return stream.Parse(req.Body, defaultTimestamp, encoding, true, promscrape.IsMetadataEnabled(), func(rows []prometheus.Row, _ []prometheus.Metadata) error {
|
return stream.Parse(req.Body, defaultTimestamp, encoding, true, prommetadata.IsEnabled(), func(rows []prometheus.Row, _ []prometheus.Metadata) error {
|
||||||
return insertRows(rows, extraLabels)
|
return insertRows(rows, extraLabels)
|
||||||
}, func(s string) {
|
}, func(s string) {
|
||||||
httpserver.LogError(req, s)
|
httpserver.LogError(req, s)
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ func main() {
|
|||||||
if err := a.Run(ctx); err != nil {
|
if err := a.Run(ctx); err != nil {
|
||||||
logger.Fatalf("cannot restore from backup: %s", err)
|
logger.Fatalf("cannot restore from backup: %s", err)
|
||||||
}
|
}
|
||||||
pushmetrics.Stop()
|
pushmetrics.StopAndPush()
|
||||||
srcFS.MustStop()
|
srcFS.MustStop()
|
||||||
dstFS.MustStop()
|
dstFS.MustStop()
|
||||||
|
|
||||||
|
|||||||
@@ -142,6 +142,12 @@ func (s *series) summarize(aggrFunc aggrFunc, startTime, endTime, step int64, xF
|
|||||||
}
|
}
|
||||||
|
|
||||||
func execExpr(ec *evalConfig, query string) (nextSeriesFunc, error) {
|
func execExpr(ec *evalConfig, query string) (nextSeriesFunc, error) {
|
||||||
|
// Validate query length to prevent memory exhaustion
|
||||||
|
maxLen := searchutil.GetMaxQueryLen()
|
||||||
|
if len(query) > maxLen {
|
||||||
|
return nil, fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(query), maxLen)
|
||||||
|
}
|
||||||
|
|
||||||
expr, err := graphiteql.Parse(query)
|
expr, err := graphiteql.Parse(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot parse %q: %w", query, err)
|
return nil, fmt.Errorf("cannot parse %q: %w", query, err)
|
||||||
@@ -191,13 +197,13 @@ func newNextSeriesForSearchQuery(ec *evalConfig, sq *storage.SearchQuery, expr g
|
|||||||
}
|
}
|
||||||
s.summarize(aggrAvg, ec.startTime, ec.endTime, ec.storageStep, 0)
|
s.summarize(aggrAvg, ec.startTime, ec.endTime, ec.storageStep, 0)
|
||||||
t := timerpool.Get(30 * time.Second)
|
t := timerpool.Get(30 * time.Second)
|
||||||
|
defer timerpool.Put(t)
|
||||||
select {
|
select {
|
||||||
case seriesCh <- s:
|
case seriesCh <- s:
|
||||||
case <-t.C:
|
case <-t.C:
|
||||||
logger.Errorf("resource leak when processing the %s (full query: %s); please report this error to VictoriaMetrics developers",
|
logger.Errorf("resource leak when processing the %s (full query: %s); please report this error to VictoriaMetrics developers",
|
||||||
expr.AppendString(nil), ec.originalQuery)
|
expr.AppendString(nil), ec.originalQuery)
|
||||||
}
|
}
|
||||||
timerpool.Put(t)
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
close(seriesCh)
|
close(seriesCh)
|
||||||
|
|||||||
@@ -4070,6 +4070,9 @@ func TestExecExprFailure(t *testing.T) {
|
|||||||
|
|
||||||
f(`holtWintersConfidenceArea(group(time("foo.baz",15),time("foo.baz",15)))`)
|
f(`holtWintersConfidenceArea(group(time("foo.baz",15),time("foo.baz",15)))`)
|
||||||
f(`holtWintersConfidenceArea()`)
|
f(`holtWintersConfidenceArea()`)
|
||||||
|
|
||||||
|
// too long query
|
||||||
|
f(`sumSeries(` + strings.Repeat("metric.very.long.name.that.takes.space,", 500) + `metric.final)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func compareSeries(ss, ssExpected []*series, expr graphiteql.Expr) error {
|
func compareSeries(ss, ssExpected []*series, expr graphiteql.Expr) error {
|
||||||
|
|||||||
@@ -1218,10 +1218,7 @@ func transformDelay(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, er
|
|||||||
values := s.Values
|
values := s.Values
|
||||||
stepsLocal := steps
|
stepsLocal := steps
|
||||||
if stepsLocal < 0 {
|
if stepsLocal < 0 {
|
||||||
stepsLocal = -stepsLocal
|
stepsLocal = min(-stepsLocal, len(values))
|
||||||
if stepsLocal > len(values) {
|
|
||||||
stepsLocal = len(values)
|
|
||||||
}
|
|
||||||
copy(values, values[stepsLocal:])
|
copy(values, values[stepsLocal:])
|
||||||
for i := len(values) - 1; i >= len(values)-stepsLocal; i-- {
|
for i := len(values) - 1; i >= len(values)-stepsLocal; i-- {
|
||||||
values[i] = nan
|
values[i] = nan
|
||||||
@@ -4663,20 +4660,14 @@ func transformSubstr(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, e
|
|||||||
if start > len(splitName) {
|
if start > len(splitName) {
|
||||||
start = len(splitName)
|
start = len(splitName)
|
||||||
} else if start < 0 {
|
} else if start < 0 {
|
||||||
start = len(splitName) + start
|
start = max(len(splitName)+start, 0)
|
||||||
if start < 0 {
|
|
||||||
start = 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if stop == 0 {
|
if stop == 0 {
|
||||||
stop = len(splitName)
|
stop = len(splitName)
|
||||||
} else if stop > len(splitName) {
|
} else if stop > len(splitName) {
|
||||||
stop = len(splitName)
|
stop = len(splitName)
|
||||||
} else if stop < 0 {
|
} else if stop < 0 {
|
||||||
stop = len(splitName) + stop
|
stop = max(len(splitName)+stop, 0)
|
||||||
if stop < 0 {
|
|
||||||
stop = 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if stop < start {
|
if stop < start {
|
||||||
stop = start
|
stop = start
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package vmselect
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -10,6 +11,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/VictoriaMetrics/metrics"
|
||||||
|
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/graphite"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/graphite"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/prometheus"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/prometheus"
|
||||||
@@ -17,6 +20,7 @@ import (
|
|||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/searchutil"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/searchutil"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/stats"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/stats"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage"
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/buildinfo"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||||
@@ -26,7 +30,6 @@ import (
|
|||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timerpool"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timerpool"
|
||||||
"github.com/VictoriaMetrics/metrics"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -48,13 +51,10 @@ var (
|
|||||||
var slowQueries = metrics.NewCounter(`vm_slow_queries_total`)
|
var slowQueries = metrics.NewCounter(`vm_slow_queries_total`)
|
||||||
|
|
||||||
func getDefaultMaxConcurrentRequests() int {
|
func getDefaultMaxConcurrentRequests() int {
|
||||||
n := cgroup.AvailableCPUs() * 2
|
// A single request can saturate all the CPU cores, so there is no sense
|
||||||
if n > 16 {
|
// in allowing higher number of concurrent requests - they will just contend
|
||||||
// A single request can saturate all the CPU cores, so there is no sense
|
// for unavailable CPU time.
|
||||||
// in allowing higher number of concurrent requests - they will just contend
|
n := min(cgroup.AvailableCPUs()*2, 16)
|
||||||
// for unavailable CPU time.
|
|
||||||
n = 16
|
|
||||||
}
|
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +67,7 @@ func Init() {
|
|||||||
prometheus.InitMaxUniqueTimeseries(*maxConcurrentRequests)
|
prometheus.InitMaxUniqueTimeseries(*maxConcurrentRequests)
|
||||||
|
|
||||||
concurrencyLimitCh = make(chan struct{}, *maxConcurrentRequests)
|
concurrencyLimitCh = make(chan struct{}, *maxConcurrentRequests)
|
||||||
|
initVMUIConfig()
|
||||||
initVMAlertProxy()
|
initVMAlertProxy()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,10 +129,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
|||||||
default:
|
default:
|
||||||
// Sleep for a while until giving up. This should resolve short bursts in requests.
|
// Sleep for a while until giving up. This should resolve short bursts in requests.
|
||||||
concurrencyLimitReached.Inc()
|
concurrencyLimitReached.Inc()
|
||||||
d := searchutil.GetMaxQueryDuration(r)
|
d := min(searchutil.GetMaxQueryDuration(r), *maxQueueDuration)
|
||||||
if d > *maxQueueDuration {
|
|
||||||
d = *maxQueueDuration
|
|
||||||
}
|
|
||||||
t := timerpool.Get(d)
|
t := timerpool.Get(d)
|
||||||
select {
|
select {
|
||||||
case concurrencyLimitCh <- struct{}{}:
|
case concurrencyLimitCh <- struct{}{}:
|
||||||
@@ -460,6 +458,11 @@ func handleStaticAndSimpleRequests(w http.ResponseWriter, r *http.Request, path
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(path, "/vmui/") {
|
if strings.HasPrefix(path, "/vmui/") {
|
||||||
|
if path == "/vmui/config.json" {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
fmt.Fprint(w, vmuiConfig)
|
||||||
|
return true
|
||||||
|
}
|
||||||
if strings.HasPrefix(path, "/vmui/static/") {
|
if strings.HasPrefix(path, "/vmui/static/") {
|
||||||
// Allow clients caching static contents for long period of time, since it shouldn't change over time.
|
// Allow clients caching static contents for long period of time, since it shouldn't change over time.
|
||||||
// Path to static contents (such as js and css) must be changed whenever its contents is changed.
|
// Path to static contents (such as js and css) must be changed whenever its contents is changed.
|
||||||
@@ -734,8 +737,40 @@ func proxyVMAlertRequests(w http.ResponseWriter, r *http.Request) {
|
|||||||
var (
|
var (
|
||||||
vmalertProxyHost string
|
vmalertProxyHost string
|
||||||
vmalertProxy *nethttputil.ReverseProxy
|
vmalertProxy *nethttputil.ReverseProxy
|
||||||
|
vmuiConfig string
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func initVMUIConfig() {
|
||||||
|
var cfg struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
License struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
} `json:"license"`
|
||||||
|
VMAlert struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
} `json:"vmalert"`
|
||||||
|
}
|
||||||
|
data, err := vmuiFiles.ReadFile("vmui/config.json")
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalf("cannot read vmui default config: %s", err)
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(data, &cfg)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalf("cannot parse vmui default config: %s", err)
|
||||||
|
}
|
||||||
|
cfg.Version = buildinfo.ShortVersion()
|
||||||
|
if cfg.Version == "" {
|
||||||
|
// buildinfo.ShortVersion() may return empty result for builds without tags
|
||||||
|
cfg.Version = buildinfo.Version
|
||||||
|
}
|
||||||
|
cfg.VMAlert.Enabled = len(*vmalertProxyURL) != 0
|
||||||
|
data, err = json.Marshal(&cfg)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalf("cannot create vmui config: %s", err)
|
||||||
|
}
|
||||||
|
vmuiConfig = string(data)
|
||||||
|
}
|
||||||
|
|
||||||
// initVMAlertProxy must be called after flag.Parse(), since it uses command-line flags.
|
// initVMAlertProxy must be called after flag.Parse(), since it uses command-line flags.
|
||||||
func initVMAlertProxy() {
|
func initVMAlertProxy() {
|
||||||
if len(*vmalertProxyURL) == 0 {
|
if len(*vmalertProxyURL) == 0 {
|
||||||
|
|||||||
@@ -203,10 +203,7 @@ var defaultMaxWorkersPerQuery = func() int {
|
|||||||
// for processing an average query, without significant impact on inter-CPU communications.
|
// for processing an average query, without significant impact on inter-CPU communications.
|
||||||
const maxWorkersLimit = 32
|
const maxWorkersLimit = 32
|
||||||
|
|
||||||
n := gomaxprocs
|
n := min(gomaxprocs, maxWorkersLimit)
|
||||||
if n > maxWorkersLimit {
|
|
||||||
n = maxWorkersLimit
|
|
||||||
}
|
|
||||||
return n
|
return n
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -279,10 +276,7 @@ func (rss *Results) runParallel(qt *querytracer.Tracer, f func(rs *Result, worke
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Prepare worker channels.
|
// Prepare worker channels.
|
||||||
workers := len(tsws)
|
workers := min(len(tsws), maxWorkers)
|
||||||
if workers > maxWorkers {
|
|
||||||
workers = maxWorkers
|
|
||||||
}
|
|
||||||
itemsPerWorker := (len(tsws) + workers - 1) / workers
|
itemsPerWorker := (len(tsws) + workers - 1) / workers
|
||||||
workChs := make([]chan *timeseriesWork, workers)
|
workChs := make([]chan *timeseriesWork, workers)
|
||||||
for i := range workChs {
|
for i := range workChs {
|
||||||
@@ -497,10 +491,7 @@ func (pts *packedTimeseries) unpackTo(dst []*sortBlock, tbf *tmpBlocksFile, tr s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Prepare worker channels.
|
// Prepare worker channels.
|
||||||
workers := len(upws)
|
workers := min(len(upws), gomaxprocs)
|
||||||
if workers > gomaxprocs {
|
|
||||||
workers = gomaxprocs
|
|
||||||
}
|
|
||||||
if workers < 1 {
|
if workers < 1 {
|
||||||
workers = 1
|
workers = 1
|
||||||
}
|
}
|
||||||
@@ -1153,10 +1144,7 @@ func ProcessSearchQuery(qt *querytracer.Tracer, sq *storage.SearchQuery, deadlin
|
|||||||
// metricNamesBuf is used for holding all the loaded unique metric names at m and orderedMetricNames.
|
// metricNamesBuf is used for holding all the loaded unique metric names at m and orderedMetricNames.
|
||||||
// It should reduce pressure on Go GC by reducing the number of string allocations
|
// It should reduce pressure on Go GC by reducing the number of string allocations
|
||||||
// when constructing metricName string from byte slice.
|
// when constructing metricName string from byte slice.
|
||||||
metricNamesBufCap := maxSeriesCount * 100
|
metricNamesBufCap := min(maxSeriesCount*100, maxFastAllocBlockSize)
|
||||||
if metricNamesBufCap > maxFastAllocBlockSize {
|
|
||||||
metricNamesBufCap = maxFastAllocBlockSize
|
|
||||||
}
|
|
||||||
metricNamesBuf := make([]byte, 0, metricNamesBufCap)
|
metricNamesBuf := make([]byte, 0, metricNamesBufCap)
|
||||||
|
|
||||||
// brssPool is used for holding all the blockRefs objects across all the loaded time series.
|
// brssPool is used for holding all the blockRefs objects across all the loaded time series.
|
||||||
@@ -1165,10 +1153,7 @@ func ProcessSearchQuery(qt *querytracer.Tracer, sq *storage.SearchQuery, deadlin
|
|||||||
|
|
||||||
// brsPool is used for holding the most of blockRefs.brs slices across all the loaded time series.
|
// brsPool is used for holding the most of blockRefs.brs slices across all the loaded time series.
|
||||||
// It should reduce pressure on Go GC by reducing the number of allocations for blockRefs.brs slices.
|
// It should reduce pressure on Go GC by reducing the number of allocations for blockRefs.brs slices.
|
||||||
brsPoolCap := uintptr(maxSeriesCount)
|
brsPoolCap := min(uintptr(maxSeriesCount), maxFastAllocBlockSize/unsafe.Sizeof(blockRef{}))
|
||||||
if brsPoolCap > maxFastAllocBlockSize/unsafe.Sizeof(blockRef{}) {
|
|
||||||
brsPoolCap = maxFastAllocBlockSize / unsafe.Sizeof(blockRef{})
|
|
||||||
}
|
|
||||||
brsPool := make([]blockRef, 0, brsPoolCap)
|
brsPool := make([]blockRef, 0, brsPoolCap)
|
||||||
|
|
||||||
// m maps from metricName to the index of blockRefs inside brssPool
|
// m maps from metricName to the index of blockRefs inside brssPool
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import (
|
|||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/encoding"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/encoding"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputil"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputil"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||||
@@ -38,7 +37,6 @@ var (
|
|||||||
latencyOffset = flag.Duration("search.latencyOffset", time.Second*30, "The time when data points become visible in query results after the collection. "+
|
latencyOffset = flag.Duration("search.latencyOffset", time.Second*30, "The time when data points become visible in query results after the collection. "+
|
||||||
"It can be overridden on per-query basis via latency_offset arg. "+
|
"It can be overridden on per-query basis via latency_offset arg. "+
|
||||||
"Too small value can result in incomplete last points for query results")
|
"Too small value can result in incomplete last points for query results")
|
||||||
maxQueryLen = flagutil.NewBytes("search.maxQueryLen", 16*1024, "The maximum search query length in bytes")
|
|
||||||
maxLookback = flag.Duration("search.maxLookback", 0, "Synonym to -query.lookback-delta from Prometheus. "+
|
maxLookback = flag.Duration("search.maxLookback", 0, "Synonym to -query.lookback-delta from Prometheus. "+
|
||||||
"The value is dynamically detected from interval between time series datapoints if not set. It can be overridden on per-query basis via max_lookback arg. "+
|
"The value is dynamically detected from interval between time series datapoints if not set. It can be overridden on per-query basis via max_lookback arg. "+
|
||||||
"See also '-search.maxStalenessInterval' flag, which has the same meaning due to historical reasons")
|
"See also '-search.maxStalenessInterval' flag, which has the same meaning due to historical reasons")
|
||||||
@@ -733,8 +731,9 @@ func QueryHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWr
|
|||||||
step = defaultStep
|
step = defaultStep
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(query) > maxQueryLen.IntN() {
|
maxLen := searchutil.GetMaxQueryLen()
|
||||||
return fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(query), maxQueryLen.N)
|
if len(query) > maxLen {
|
||||||
|
return fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(query), maxLen)
|
||||||
}
|
}
|
||||||
etfs, err := searchutil.GetExtraTagFilters(r)
|
etfs, err := searchutil.GetExtraTagFilters(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -904,8 +903,9 @@ func queryRangeHandler(qt *querytracer.Tracer, startTime time.Time, w http.Respo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate input args.
|
// Validate input args.
|
||||||
if len(query) > maxQueryLen.IntN() {
|
maxLen := searchutil.GetMaxQueryLen()
|
||||||
return fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(query), maxQueryLen.N)
|
if len(query) > maxLen {
|
||||||
|
return fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(query), maxLen)
|
||||||
}
|
}
|
||||||
if start > end {
|
if start > end {
|
||||||
end = start + defaultStep
|
end = start + defaultStep
|
||||||
@@ -1089,12 +1089,9 @@ func getRoundDigits(r *http.Request) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getLatencyOffsetMilliseconds(r *http.Request) (int64, error) {
|
func getLatencyOffsetMilliseconds(r *http.Request) (int64, error) {
|
||||||
d := latencyOffset.Milliseconds()
|
// Zero latency offset may be useful for some use cases.
|
||||||
if d < 0 {
|
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2061#issuecomment-1299109836
|
||||||
// Zero latency offset may be useful for some use cases.
|
d := max(latencyOffset.Milliseconds(), 0)
|
||||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2061#issuecomment-1299109836
|
|
||||||
d = 0
|
|
||||||
}
|
|
||||||
return httputil.GetDuration(r, "latency_offset", d)
|
return httputil.GetDuration(r, "latency_offset", d)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -161,11 +161,8 @@ func aggrFuncAny(afa *aggrFuncArg) ([]*timeseries, error) {
|
|||||||
afe := func(tss []*timeseries, _ *metricsql.ModifierExpr) []*timeseries {
|
afe := func(tss []*timeseries, _ *metricsql.ModifierExpr) []*timeseries {
|
||||||
return tss[:1]
|
return tss[:1]
|
||||||
}
|
}
|
||||||
limit := afa.ae.Limit
|
// Only a single time series per group must be returned
|
||||||
if limit > 1 {
|
limit := min(afa.ae.Limit, 1)
|
||||||
// Only a single time series per group must be returned
|
|
||||||
limit = 1
|
|
||||||
}
|
|
||||||
return aggrFuncExt(afe, tss, &afa.ae.Modifier, limit, true)
|
return aggrFuncExt(afe, tss, &afa.ae.Modifier, limit, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1002,10 +1002,7 @@ func getKeepMetricNames(expr metricsql.Expr) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func doParallel(tss []*timeseries, f func(ts *timeseries, values []float64, timestamps []int64, workerID uint) ([]float64, []int64)) {
|
func doParallel(tss []*timeseries, f func(ts *timeseries, values []float64, timestamps []int64, workerID uint) ([]float64, []int64)) {
|
||||||
workers := netstorage.MaxWorkers()
|
workers := min(netstorage.MaxWorkers(), len(tss))
|
||||||
if workers > len(tss) {
|
|
||||||
workers = len(tss)
|
|
||||||
}
|
|
||||||
seriesPerWorker := (len(tss) + workers - 1) / workers
|
seriesPerWorker := (len(tss) + workers - 1) / workers
|
||||||
workChs := make([]chan *timeseries, workers)
|
workChs := make([]chan *timeseries, workers)
|
||||||
for i := range workChs {
|
for i := range workChs {
|
||||||
@@ -1079,10 +1076,7 @@ func evalInstantRollup(qt *querytracer.Tracer, ec *EvalConfig, funcName string,
|
|||||||
return evalRollupFuncNoCache(qt, ecCopy, funcName, rf, expr, me, iafc, window, pointsPerSeries)
|
return evalRollupFuncNoCache(qt, ecCopy, funcName, rf, expr, me, iafc, window, pointsPerSeries)
|
||||||
}
|
}
|
||||||
tooBigOffset := func(offset int64) bool {
|
tooBigOffset := func(offset int64) bool {
|
||||||
maxOffset := window / 2
|
maxOffset := min(window/2, 1800*1000)
|
||||||
if maxOffset > 1800*1000 {
|
|
||||||
maxOffset = 1800 * 1000
|
|
||||||
}
|
|
||||||
return offset >= maxOffset
|
return offset >= maxOffset
|
||||||
}
|
}
|
||||||
deleteCachedSeries := func(qt *querytracer.Tracer) {
|
deleteCachedSeries := func(qt *querytracer.Tracer) {
|
||||||
@@ -1156,15 +1150,23 @@ func evalInstantRollup(qt *querytracer.Tracer, ec *EvalConfig, funcName string,
|
|||||||
}
|
}
|
||||||
qt.Printf("optimized calculation for instant rollup avg_over_time(m[d]) as (sum_over_time(m[d]) / count_over_time(m[d]))")
|
qt.Printf("optimized calculation for instant rollup avg_over_time(m[d]) as (sum_over_time(m[d]) / count_over_time(m[d]))")
|
||||||
fe := expr.(*metricsql.FuncExpr)
|
fe := expr.(*metricsql.FuncExpr)
|
||||||
feSum := *fe
|
// copy RollupExpr to drop possible offset,
|
||||||
feSum.Name = "sum_over_time"
|
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9762
|
||||||
feCount := *fe
|
newArg := copyRollupExpr(fe.Args[0].(*metricsql.RollupExpr))
|
||||||
feCount.Name = "count_over_time"
|
newArg.Offset = nil
|
||||||
be := &metricsql.BinaryOpExpr{
|
be := &metricsql.BinaryOpExpr{
|
||||||
Op: "/",
|
Op: "/",
|
||||||
KeepMetricNames: fe.KeepMetricNames,
|
KeepMetricNames: fe.KeepMetricNames,
|
||||||
Left: &feSum,
|
Left: &metricsql.FuncExpr{
|
||||||
Right: &feCount,
|
Name: "sum_over_time",
|
||||||
|
Args: []metricsql.Expr{newArg},
|
||||||
|
KeepMetricNames: fe.KeepMetricNames,
|
||||||
|
},
|
||||||
|
Right: &metricsql.FuncExpr{
|
||||||
|
Name: "count_over_time",
|
||||||
|
Args: []metricsql.Expr{newArg},
|
||||||
|
KeepMetricNames: fe.KeepMetricNames,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return evalExpr(qt, ec, be)
|
return evalExpr(qt, ec, be)
|
||||||
case "rate":
|
case "rate":
|
||||||
@@ -1178,8 +1180,12 @@ func evalInstantRollup(qt *querytracer.Tracer, ec *EvalConfig, funcName string,
|
|||||||
fe := afe.Args[0].(*metricsql.FuncExpr)
|
fe := afe.Args[0].(*metricsql.FuncExpr)
|
||||||
feIncrease := *fe
|
feIncrease := *fe
|
||||||
feIncrease.Name = "increase"
|
feIncrease.Name = "increase"
|
||||||
re := fe.Args[0].(*metricsql.RollupExpr)
|
// copy RollupExpr to drop possible offset,
|
||||||
d := re.Window.Duration(ec.Step)
|
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9762
|
||||||
|
newArg := copyRollupExpr(fe.Args[0].(*metricsql.RollupExpr))
|
||||||
|
newArg.Offset = nil
|
||||||
|
feIncrease.Args = []metricsql.Expr{newArg}
|
||||||
|
d := newArg.Window.Duration(ec.Step)
|
||||||
if d == 0 {
|
if d == 0 {
|
||||||
d = ec.Step
|
d = ec.Step
|
||||||
}
|
}
|
||||||
@@ -1199,8 +1205,12 @@ func evalInstantRollup(qt *querytracer.Tracer, ec *EvalConfig, funcName string,
|
|||||||
fe := expr.(*metricsql.FuncExpr)
|
fe := expr.(*metricsql.FuncExpr)
|
||||||
feIncrease := *fe
|
feIncrease := *fe
|
||||||
feIncrease.Name = "increase"
|
feIncrease.Name = "increase"
|
||||||
re := fe.Args[0].(*metricsql.RollupExpr)
|
// copy RollupExpr to drop possible offset,
|
||||||
d := re.Window.Duration(ec.Step)
|
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9762
|
||||||
|
newArg := copyRollupExpr(fe.Args[0].(*metricsql.RollupExpr))
|
||||||
|
newArg.Offset = nil
|
||||||
|
feIncrease.Args = []metricsql.Expr{newArg}
|
||||||
|
d := newArg.Window.Duration(ec.Step)
|
||||||
if d == 0 {
|
if d == 0 {
|
||||||
d = ec.Step
|
d = ec.Step
|
||||||
}
|
}
|
||||||
@@ -2005,3 +2015,23 @@ func dropStaleNaNs(funcName string, values []float64, timestamps []int64) ([]flo
|
|||||||
}
|
}
|
||||||
return dstValues, dstTimestamps
|
return dstValues, dstTimestamps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func copyRollupExpr(re *metricsql.RollupExpr) *metricsql.RollupExpr {
|
||||||
|
var newRe metricsql.RollupExpr
|
||||||
|
newRe.Expr = re.Expr
|
||||||
|
newRe.InheritStep = re.InheritStep
|
||||||
|
newRe.At = re.At
|
||||||
|
if re.Window != nil {
|
||||||
|
newRe.Window = &metricsql.DurationExpr{}
|
||||||
|
*newRe.Window = *re.Window
|
||||||
|
}
|
||||||
|
if re.Offset != nil {
|
||||||
|
newRe.Offset = &metricsql.DurationExpr{}
|
||||||
|
*newRe.Offset = *re.Offset
|
||||||
|
}
|
||||||
|
if re.Step != nil {
|
||||||
|
newRe.Step = &metricsql.DurationExpr{}
|
||||||
|
*newRe.Step = *re.Step
|
||||||
|
}
|
||||||
|
return &newRe
|
||||||
|
}
|
||||||
|
|||||||
@@ -820,17 +820,11 @@ func seekFirstTimestampIdxAfter(timestamps []int64, seekTimestamp int64, nHint i
|
|||||||
if len(timestamps) == 0 || timestamps[0] > seekTimestamp {
|
if len(timestamps) == 0 || timestamps[0] > seekTimestamp {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
startIdx := nHint - 2
|
startIdx := max(nHint-2, 0)
|
||||||
if startIdx < 0 {
|
|
||||||
startIdx = 0
|
|
||||||
}
|
|
||||||
if startIdx >= len(timestamps) {
|
if startIdx >= len(timestamps) {
|
||||||
startIdx = len(timestamps) - 1
|
startIdx = len(timestamps) - 1
|
||||||
}
|
}
|
||||||
endIdx := nHint + 2
|
endIdx := min(nHint+2, len(timestamps))
|
||||||
if endIdx > len(timestamps) {
|
|
||||||
endIdx = len(timestamps)
|
|
||||||
}
|
|
||||||
if startIdx > 0 && timestamps[startIdx] <= seekTimestamp {
|
if startIdx > 0 && timestamps[startIdx] <= seekTimestamp {
|
||||||
timestamps = timestamps[startIdx:]
|
timestamps = timestamps[startIdx:]
|
||||||
endIdx -= startIdx
|
endIdx -= startIdx
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/VictoriaMetrics/metricsql"
|
||||||
|
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputil"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputil"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||||
"github.com/VictoriaMetrics/metricsql"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -20,6 +22,7 @@ var (
|
|||||||
maxStatusRequestDuration = flag.Duration("search.maxStatusRequestDuration", time.Minute*5, "The maximum duration for /api/v1/status/* requests")
|
maxStatusRequestDuration = flag.Duration("search.maxStatusRequestDuration", time.Minute*5, "The maximum duration for /api/v1/status/* requests")
|
||||||
maxLabelsAPIDuration = flag.Duration("search.maxLabelsAPIDuration", time.Second*5, "The maximum duration for /api/v1/labels, /api/v1/label/.../values and /api/v1/series requests. "+
|
maxLabelsAPIDuration = flag.Duration("search.maxLabelsAPIDuration", time.Second*5, "The maximum duration for /api/v1/labels, /api/v1/label/.../values and /api/v1/series requests. "+
|
||||||
"See also -search.maxLabelsAPISeries and -search.ignoreExtraFiltersAtLabelsAPI")
|
"See also -search.maxLabelsAPISeries and -search.ignoreExtraFiltersAtLabelsAPI")
|
||||||
|
maxQueryLen = flagutil.NewBytes("search.maxQueryLen", 16*1024, "The maximum search query length in bytes")
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetMaxQueryDuration returns the maximum duration for query from r.
|
// GetMaxQueryDuration returns the maximum duration for query from r.
|
||||||
@@ -227,3 +230,8 @@ func toTagFilter(dst *storage.TagFilter, src *metricsql.LabelFilter) {
|
|||||||
dst.IsRegexp = src.IsRegexp
|
dst.IsRegexp = src.IsRegexp
|
||||||
dst.IsNegative = src.IsNegative
|
dst.IsNegative = src.IsNegative
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetMaxQueryLen returns the current value of the search.maxQueryLen flag.
|
||||||
|
func GetMaxQueryLen() int {
|
||||||
|
return maxQueryLen.IntN()
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ However, there are some [intentional differences](https://medium.com/@romanhavro
|
|||||||
|
|
||||||
[Standalone MetricsQL package](https://godoc.org/github.com/VictoriaMetrics/metricsql) can be used for parsing MetricsQL in external apps.
|
[Standalone MetricsQL package](https://godoc.org/github.com/VictoriaMetrics/metricsql) can be used for parsing MetricsQL in external apps.
|
||||||
|
|
||||||
If you are unfamiliar with PromQL, then it is suggested reading [this tutorial for beginners](https://medium.com/@valyala/promql-tutorial-for-beginners-9ab455142085)
|
If you are unfamiliar with PromQL, we suggest reading [this tutorial for beginners](https://medium.com/@valyala/promql-tutorial-for-beginners-9ab455142085)
|
||||||
and introduction into [basic querying via MetricsQL](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#metricsql).
|
and introduction into [basic querying via MetricsQL](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#metricsql).
|
||||||
|
|
||||||
The following functionality is implemented differently in MetricsQL compared to PromQL. This improves user experience:
|
The following functionality is implemented differently in MetricsQL compared to PromQL. This improves user experience:
|
||||||
@@ -69,13 +69,13 @@ The list of MetricsQL features on top of PromQL:
|
|||||||
See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#selecting-graphite-metrics).
|
See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#selecting-graphite-metrics).
|
||||||
VictoriaMetrics can be used as Graphite datasource in Grafana. See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#graphite-api-usage) for details.
|
VictoriaMetrics can be used as Graphite datasource in Grafana. See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#graphite-api-usage) for details.
|
||||||
See also [label_graphite_group](#label_graphite_group) function, which can be used for extracting the given groups from Graphite metric name.
|
See also [label_graphite_group](#label_graphite_group) function, which can be used for extracting the given groups from Graphite metric name.
|
||||||
* Lookbehind window in square brackets for [rollup functions](#rollup-functions) may be omitted. VictoriaMetrics automatically selects the lookbehind window
|
* The lookbehind window in square brackets for [rollup functions](#rollup-functions) may be omitted. VictoriaMetrics automatically selects the lookbehind window
|
||||||
depending on the `step` query arg passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query)
|
depending on the `step` query arg passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query)
|
||||||
and the real interval between [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples) (aka `scrape_interval`).
|
and the real interval between [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples) (aka `scrape_interval`).
|
||||||
For instance, the following query is valid in VictoriaMetrics: `rate(node_network_receive_bytes_total)`.
|
For instance, the following query is valid in VictoriaMetrics: `rate(node_network_receive_bytes_total)`.
|
||||||
It is roughly equivalent to `rate(node_network_receive_bytes_total[$__interval])` when used in Grafana.
|
It is roughly equivalent to `rate(node_network_receive_bytes_total[$__interval])` when used in Grafana.
|
||||||
The difference is documented in [rate() docs](#rate).
|
The difference is documented in [rate() docs](#rate).
|
||||||
* Numeric values can contain `_` delimiters for better readability. For example, `1_234_567_890` can be used in queries instead of `1234567890`.
|
* Numeric values may include underscore delimiters for better readability. For example, `1_234_567_890` can be used in queries instead of `1234567890`.
|
||||||
* [Series selectors](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering) accept multiple `or` filters. For example, `{env="prod",job="a" or env="dev",job="b"}`
|
* [Series selectors](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering) accept multiple `or` filters. For example, `{env="prod",job="a" or env="dev",job="b"}`
|
||||||
selects series with `{env="prod",job="a"}` or `{env="dev",job="b"}` labels.
|
selects series with `{env="prod",job="a"}` or `{env="dev",job="b"}` labels.
|
||||||
See [these docs](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering-by-multiple-or-filters) for details.
|
See [these docs](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering-by-multiple-or-filters) for details.
|
||||||
@@ -111,8 +111,8 @@ The list of MetricsQL features on top of PromQL:
|
|||||||
* Metric names and labels names may contain escaped chars. For example, `foo\-bar{baz\=aa="b"}` is valid expression.
|
* Metric names and labels names may contain escaped chars. For example, `foo\-bar{baz\=aa="b"}` is valid expression.
|
||||||
It returns time series with name `foo-bar` containing label `baz=aa` with value `b`.
|
It returns time series with name `foo-bar` containing label `baz=aa` with value `b`.
|
||||||
Additionally, the following escape sequences are supported:
|
Additionally, the following escape sequences are supported:
|
||||||
- `\xXX`, where `XX` is hexadecimal representation of the escaped ascii char.
|
* `\xXX`, where `XX` is hexadecimal representation of the escaped ascii char.
|
||||||
- `\uXXXX`, where `XXXX` is a hexadecimal representation of the escaped unicode char.
|
* `\uXXXX`, where `XXXX` is a hexadecimal representation of the escaped unicode char.
|
||||||
* Aggregate functions support optional `limit N` suffix in order to limit the number of output series.
|
* Aggregate functions support optional `limit N` suffix in order to limit the number of output series.
|
||||||
For example, `sum(x) by (y) limit 3` limits the number of output time series after the aggregation to 3.
|
For example, `sum(x) by (y) limit 3` limits the number of output time series after the aggregation to 3.
|
||||||
All the other time series are dropped.
|
All the other time series are dropped.
|
||||||
@@ -138,8 +138,9 @@ This may result in `duplicate time series` error when the function is applied to
|
|||||||
This error can be fixed by applying `keep_metric_names` modifier to the function or binary operator.
|
This error can be fixed by applying `keep_metric_names` modifier to the function or binary operator.
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
- `rate({__name__=~"foo|bar"}) keep_metric_names` leaves `foo` and `bar` metric names in the returned time series.
|
|
||||||
- `({__name__=~"foo|bar"} / 10) keep_metric_names` leaves `foo` and `bar` metric names in the returned time series.
|
* `rate({__name__=~"foo|bar"}) keep_metric_names` leaves `foo` and `bar` metric names in the returned time series.
|
||||||
|
* `({__name__=~"foo|bar"} / 10) keep_metric_names` leaves `foo` and `bar` metric names in the returned time series.
|
||||||
|
|
||||||
## MetricsQL functions
|
## MetricsQL functions
|
||||||
|
|
||||||
@@ -166,10 +167,10 @@ Additional details:
|
|||||||
* If the given [series selector](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering) returns multiple time series,
|
* If the given [series selector](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering) returns multiple time series,
|
||||||
then rollups are calculated individually per each returned series.
|
then rollups are calculated individually per each returned series.
|
||||||
* If lookbehind window in square brackets is missing, then it is automatically set to the following value:
|
* If lookbehind window in square brackets is missing, then it is automatically set to the following value:
|
||||||
- To `step` value passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query) or [/api/v1/query](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#instant-query)
|
* To `step` value passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query) or [/api/v1/query](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#instant-query)
|
||||||
for all the [rollup functions](#rollup-functions) except of [default_rollup](#default_rollup) and [rate](#rate). This value is known as `$__interval` in Grafana or `1i` in MetricsQL.
|
for all the [rollup functions](#rollup-functions) except of [default_rollup](#default_rollup) and [rate](#rate). This value is known as `$__interval` in Grafana or `1i` in MetricsQL.
|
||||||
For example, `avg_over_time(temperature)` is automatically transformed to `avg_over_time(temperature[1i])`.
|
For example, `avg_over_time(temperature)` is automatically transformed to `avg_over_time(temperature[1i])`.
|
||||||
- To the `max(step, scrape_interval)`, where `scrape_interval` is the interval between [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples)
|
* To the `max(step, scrape_interval)`, where `scrape_interval` is the interval between [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples)
|
||||||
for [default_rollup](#default_rollup) and [rate](#rate) functions. This allows avoiding unexpected gaps on the graph when `step` is smaller than `scrape_interval`.
|
for [default_rollup](#default_rollup) and [rate](#rate) functions. This allows avoiding unexpected gaps on the graph when `step` is smaller than `scrape_interval`.
|
||||||
* Every [series selector](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering) in MetricsQL must be wrapped into a rollup function.
|
* Every [series selector](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering) in MetricsQL must be wrapped into a rollup function.
|
||||||
Otherwise, it is automatically wrapped into [default_rollup](#default_rollup). For example, `foo{bar="baz"}`
|
Otherwise, it is automatically wrapped into [default_rollup](#default_rollup). For example, `foo{bar="baz"}`
|
||||||
@@ -666,8 +667,9 @@ This function is usually applied to [gauges](https://docs.victoriametrics.com/vi
|
|||||||
|
|
||||||
`outlier_iqr_over_time(series_selector[d])` is a [rollup function](#rollup-functions), which returns the last sample on the given lookbehind window `d`
|
`outlier_iqr_over_time(series_selector[d])` is a [rollup function](#rollup-functions), which returns the last sample on the given lookbehind window `d`
|
||||||
if its value is either smaller than the `q25-1.5*iqr` or bigger than `q75+1.5*iqr` where:
|
if its value is either smaller than the `q25-1.5*iqr` or bigger than `q75+1.5*iqr` where:
|
||||||
- `iqr` is an [Interquartile range](https://en.wikipedia.org/wiki/Interquartile_range) over [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples) on the lookbehind window `d`
|
|
||||||
- `q25` and `q75` are 25th and 75th [percentiles](https://en.wikipedia.org/wiki/Percentile) over [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples) on the lookbehind window `d`.
|
* `iqr` is an [Interquartile range](https://en.wikipedia.org/wiki/Interquartile_range) over [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples) on the lookbehind window `d`
|
||||||
|
* `q25` and `q75` are 25th and 75th [percentiles](https://en.wikipedia.org/wiki/Percentile) over [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples) on the lookbehind window `d`.
|
||||||
|
|
||||||
The `outlier_iqr_over_time()` is useful for detecting anomalies in gauge values based on the previous history of values.
|
The `outlier_iqr_over_time()` is useful for detecting anomalies in gauge values based on the previous history of values.
|
||||||
For example, `outlier_iqr_over_time(memory_usage_bytes[1h])` triggers when `memory_usage_bytes` suddenly goes outside the usual value range for the last hour.
|
For example, `outlier_iqr_over_time(memory_usage_bytes[1h])` triggers when `memory_usage_bytes` suddenly goes outside the usual value range for the last hour.
|
||||||
@@ -759,7 +761,6 @@ This function is usually applied to [counters](https://docs.victoriametrics.com/
|
|||||||
|
|
||||||
See also [increase_prometheus](#increase_prometheus) and [rate](#rate).
|
See also [increase_prometheus](#increase_prometheus) and [rate](#rate).
|
||||||
|
|
||||||
|
|
||||||
#### rate_over_sum
|
#### rate_over_sum
|
||||||
|
|
||||||
`rate_over_sum(series_selector[d])` is a [rollup function](#rollup-functions), which calculates per-second rate over the sum of [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples)
|
`rate_over_sum(series_selector[d])` is a [rollup function](#rollup-functions), which calculates per-second rate over the sum of [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples)
|
||||||
@@ -1106,7 +1107,6 @@ This function is usually applied to [gauges](https://docs.victoriametrics.com/vi
|
|||||||
|
|
||||||
See also [zscore](#zscore), [range_trim_zscore](#range_trim_zscore) and [outlier_iqr_over_time](#outlier_iqr_over_time).
|
See also [zscore](#zscore), [range_trim_zscore](#range_trim_zscore) and [outlier_iqr_over_time](#outlier_iqr_over_time).
|
||||||
|
|
||||||
|
|
||||||
### Transform functions
|
### Transform functions
|
||||||
|
|
||||||
**Transform functions** calculate transformations over [rollup results](#rollup-functions).
|
**Transform functions** calculate transformations over [rollup results](#rollup-functions).
|
||||||
@@ -1851,7 +1851,6 @@ The list of supported label manipulation functions:
|
|||||||
`alias(q, "name")` is [label manipulation function](#label-manipulation-functions), which sets the given `name` to all the time series returned by `q`.
|
`alias(q, "name")` is [label manipulation function](#label-manipulation-functions), which sets the given `name` to all the time series returned by `q`.
|
||||||
For example, `alias(up, "foobar")` would rename `up` series to `foobar` series.
|
For example, `alias(up, "foobar")` would rename `up` series to `foobar` series.
|
||||||
|
|
||||||
|
|
||||||
#### drop_common_labels
|
#### drop_common_labels
|
||||||
|
|
||||||
`drop_common_labels(q1, ...., qN)` is [label manipulation function](#label-manipulation-functions), which drops common `label="value"` pairs
|
`drop_common_labels(q1, ...., qN)` is [label manipulation function](#label-manipulation-functions), which drops common `label="value"` pairs
|
||||||
@@ -1877,7 +1876,7 @@ For example, `label_graphite_group({__graphite__="foo*.bar.*"}, 0, 2)` would sub
|
|||||||
|
|
||||||
This function is useful for aggregating Graphite metrics with [aggregate functions](#aggregate-functions). For example, the following query would return per-app memory usage:
|
This function is useful for aggregating Graphite metrics with [aggregate functions](#aggregate-functions). For example, the following query would return per-app memory usage:
|
||||||
|
|
||||||
```
|
```metricsql
|
||||||
sum by (__name__) (
|
sum by (__name__) (
|
||||||
label_graphite_group({__graphite__="app*.host*.memory_usage"}, 0)
|
label_graphite_group({__graphite__="app*.host*.memory_usage"}, 0)
|
||||||
)
|
)
|
||||||
@@ -2003,7 +2002,6 @@ would return series in the following order of `bar` label values: `101`, `15`, `
|
|||||||
|
|
||||||
See also [sort_by_label_numeric](#sort_by_label_numeric) and [sort_by_label_desc](#sort_by_label_desc).
|
See also [sort_by_label_numeric](#sort_by_label_numeric) and [sort_by_label_desc](#sort_by_label_desc).
|
||||||
|
|
||||||
|
|
||||||
### Aggregate functions
|
### Aggregate functions
|
||||||
|
|
||||||
**Aggregate functions** calculate aggregates over groups of [rollup results](#rollup-functions).
|
**Aggregate functions** calculate aggregates over groups of [rollup results](#rollup-functions).
|
||||||
@@ -2179,8 +2177,9 @@ per each `group_labels` for all the time series returned by `q`. The aggregate i
|
|||||||
`outliers_iqr(q)` is [aggregate function](#aggregate-functions), which returns time series from `q` with at least a single point
|
`outliers_iqr(q)` is [aggregate function](#aggregate-functions), which returns time series from `q` with at least a single point
|
||||||
outside e.g. [Interquartile range outlier bounds](https://en.wikipedia.org/wiki/Interquartile_range) `[q25-1.5*iqr .. q75+1.5*iqr]`
|
outside e.g. [Interquartile range outlier bounds](https://en.wikipedia.org/wiki/Interquartile_range) `[q25-1.5*iqr .. q75+1.5*iqr]`
|
||||||
comparing to other time series at the given point, where:
|
comparing to other time series at the given point, where:
|
||||||
- `iqr` is an [Interquartile range](https://en.wikipedia.org/wiki/Interquartile_range) calculated independently per each point on the graph across `q` series.
|
|
||||||
- `q25` and `q75` are 25th and 75th [percentiles](https://en.wikipedia.org/wiki/Percentile) calculated independently per each point on the graph across `q` series.
|
* `iqr` is an [Interquartile range](https://en.wikipedia.org/wiki/Interquartile_range) calculated independently per each point on the graph across `q` series.
|
||||||
|
* `q25` and `q75` are 25th and 75th [percentiles](https://en.wikipedia.org/wiki/Percentile) calculated independently per each point on the graph across `q` series.
|
||||||
|
|
||||||
The `outliers_iqr()` is useful for detecting anomalous series in the group of series. For example, `outliers_iqr(temperature) by (country)` returns
|
The `outliers_iqr()` is useful for detecting anomalous series in the group of series. For example, `outliers_iqr(temperature) by (country)` returns
|
||||||
per-country series with anomalous outlier values comparing to the rest of per-country series.
|
per-country series with anomalous outlier values comparing to the rest of per-country series.
|
||||||
@@ -2349,10 +2348,10 @@ VictoriaMetrics performs subqueries in the following way:
|
|||||||
VictoriaMetrics performs the following implicit conversions for incoming queries before starting the calculations:
|
VictoriaMetrics performs the following implicit conversions for incoming queries before starting the calculations:
|
||||||
|
|
||||||
* If lookbehind window in square brackets is missing inside [rollup function](#rollup-functions), then it is automatically set to the following value:
|
* If lookbehind window in square brackets is missing inside [rollup function](#rollup-functions), then it is automatically set to the following value:
|
||||||
- To `step` value passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query) or [/api/v1/query](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#instant-query)
|
* To `step` value passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query) or [/api/v1/query](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#instant-query)
|
||||||
for all the [rollup functions](#rollup-functions) except of [default_rollup](#default_rollup) and [rate](#rate). This value is known as `$__interval` in Grafana or `1i` in MetricsQL.
|
for all the [rollup functions](#rollup-functions) except of [default_rollup](#default_rollup) and [rate](#rate). This value is known as `$__interval` in Grafana or `1i` in MetricsQL.
|
||||||
For example, `avg_over_time(temperature)` is automatically transformed to `avg_over_time(temperature[1i])`.
|
For example, `avg_over_time(temperature)` is automatically transformed to `avg_over_time(temperature[1i])`.
|
||||||
- To the `max(step, scrape_interval)`, where `scrape_interval` is the interval between [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples)
|
* To the `max(step, scrape_interval)`, where `scrape_interval` is the interval between [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples)
|
||||||
for [default_rollup](#default_rollup) and [rate](#rate) functions. This allows avoiding unexpected gaps on the graph when `step` is smaller than `scrape_interval`.
|
for [default_rollup](#default_rollup) and [rate](#rate) functions. This allows avoiding unexpected gaps on the graph when `step` is smaller than `scrape_interval`.
|
||||||
* All the [series selectors](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering),
|
* All the [series selectors](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering),
|
||||||
which aren't wrapped into [rollup functions](#rollup-functions), are automatically wrapped into [default_rollup](#default_rollup) function.
|
which aren't wrapped into [rollup functions](#rollup-functions), are automatically wrapped into [default_rollup](#default_rollup) function.
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
209
app/vmselect/vmui/assets/index-DQcPcJrn.js
Normal file
209
app/vmselect/vmui/assets/index-DQcPcJrn.js
Normal file
File diff suppressed because one or more lines are too long
1
app/vmselect/vmui/assets/index-dWApsAEM.css
Normal file
1
app/vmselect/vmui/assets/index-dWApsAEM.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
@@ -36,10 +36,10 @@
|
|||||||
<meta property="og:title" content="UI for VictoriaMetrics">
|
<meta property="og:title" content="UI for VictoriaMetrics">
|
||||||
<meta property="og:url" content="https://victoriametrics.com/">
|
<meta property="og:url" content="https://victoriametrics.com/">
|
||||||
<meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data">
|
<meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data">
|
||||||
<script type="module" crossorigin src="./assets/index-Ck5nH8JI.js"></script>
|
<script type="module" crossorigin src="./assets/index-DQcPcJrn.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="./assets/vendor-BVRvRxZ2.js">
|
<link rel="modulepreload" crossorigin href="./assets/vendor-DY9kCvzk.js">
|
||||||
<link rel="stylesheet" crossorigin href="./assets/vendor-D1GxaB_c.css">
|
<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-dWApsAEM.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
|||||||
@@ -683,7 +683,7 @@ func writeStorageMetrics(w io.Writer, strg *storage.Storage) {
|
|||||||
metrics.WriteCounterUint64(w, `vm_cache_eviction_bytes_total{type="storage/metricIDs", reason="miss_percentage"}`, m.MetricIDCacheMissEvictionBytes)
|
metrics.WriteCounterUint64(w, `vm_cache_eviction_bytes_total{type="storage/metricIDs", reason="miss_percentage"}`, m.MetricIDCacheMissEvictionBytes)
|
||||||
metrics.WriteCounterUint64(w, `vm_cache_eviction_bytes_total{type="storage/metricIDs", reason="expiration"}`, m.MetricIDCacheExpireEvictionBytes)
|
metrics.WriteCounterUint64(w, `vm_cache_eviction_bytes_total{type="storage/metricIDs", reason="expiration"}`, m.MetricIDCacheExpireEvictionBytes)
|
||||||
|
|
||||||
metrics.WriteCounterUint64(w, `vm_deleted_metrics_total{type="indexdb"}`, idbm.DeletedMetricsCount)
|
metrics.WriteCounterUint64(w, `vm_deleted_metrics_total{type="indexdb"}`, m.DeletedMetricsCount)
|
||||||
|
|
||||||
metrics.WriteCounterUint64(w, `vm_cache_collisions_total{type="storage/tsid"}`, m.TSIDCacheCollisions)
|
metrics.WriteCounterUint64(w, `vm_cache_collisions_total{type="storage/tsid"}`, m.TSIDCacheCollisions)
|
||||||
metrics.WriteCounterUint64(w, `vm_cache_collisions_total{type="storage/metricName"}`, m.MetricNameCacheCollisions)
|
metrics.WriteCounterUint64(w, `vm_cache_collisions_total{type="storage/metricName"}`, m.MetricNameCacheCollisions)
|
||||||
|
|||||||
@@ -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
|
# Sets a custom location for the npm cache, preventing access errors in system directories
|
||||||
ENV NPM_CONFIG_CACHE=/build/.npm
|
ENV NPM_CONFIG_CACHE=/build/.npm
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.25.0 AS build-web-stage
|
FROM golang:1.25.1 AS build-web-stage
|
||||||
COPY build /build
|
COPY build /build
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<link rel="apple-touch-icon" href="/favicon.svg"/>
|
<link rel="apple-touch-icon" href="/favicon.svg"/>
|
||||||
<link rel="mask-icon" href="/favicon.svg" color="#000000">
|
<link rel="mask-icon" href="/favicon.svg" color="#000000">
|
||||||
|
|
||||||
|
<meta name="robots" content="noindex">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5"/>
|
||||||
<meta name="theme-color" content="#000000"/>
|
<meta name="theme-color" content="#000000"/>
|
||||||
<meta name="description" content="Explore and troubleshoot your VictoriaMetrics data"/>
|
<meta name="description" content="Explore and troubleshoot your VictoriaMetrics data"/>
|
||||||
|
|||||||
545
app/vmui/packages/vmui/package-lock.json
generated
545
app/vmui/packages/vmui/package-lock.json
generated
@@ -17,7 +17,7 @@
|
|||||||
"react-input-mask": "^2.0.4",
|
"react-input-mask": "^2.0.4",
|
||||||
"react-router-dom": "^7.6.3",
|
"react-router-dom": "^7.6.3",
|
||||||
"uplot": "^1.6.32",
|
"uplot": "^1.6.32",
|
||||||
"vite": "^7.0.4",
|
"vite": "^7.1.5",
|
||||||
"web-vitals": "^5.0.3"
|
"web-vitals": "^5.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1177,7 +1177,7 @@
|
|||||||
"version": "0.3.12",
|
"version": "0.3.12",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
|
||||||
"integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
|
"integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||||
@@ -1188,24 +1188,36 @@
|
|||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"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": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.4",
|
"version": "1.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
|
||||||
"integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==",
|
"integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
"version": "0.3.29",
|
"version": "0.3.29",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
|
||||||
"integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
|
"integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/resolve-uri": "^3.1.0",
|
"@jridgewell/resolve-uri": "^3.1.0",
|
||||||
@@ -1250,6 +1262,316 @@
|
|||||||
"node": ">= 8"
|
"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": {
|
"node_modules/@preact/preset-vite": {
|
||||||
"version": "2.10.2",
|
"version": "2.10.2",
|
||||||
"resolved": "https://registry.npmjs.org/@preact/preset-vite/-/preset-vite-2.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/@preact/preset-vite/-/preset-vite-2.10.2.tgz",
|
||||||
@@ -1750,9 +2072,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "24.0.12",
|
"version": "24.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
|
||||||
"integrity": "sha512-LtOrbvDf5ndC9Xi+4QZjVL0woFymF/xSTKZKPgrrl7H7XoeDvnD+E2IclKVDyaK9UM756W/3BXqSU+JEHopA9g==",
|
"integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2221,7 +2543,7 @@
|
|||||||
"version": "8.15.0",
|
"version": "8.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
@@ -2523,7 +2845,7 @@
|
|||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fill-range": "^7.1.1"
|
"fill-range": "^7.1.1"
|
||||||
@@ -2572,6 +2894,14 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT/X11"
|
"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": {
|
"node_modules/cac": {
|
||||||
"version": "6.7.14",
|
"version": "6.7.14",
|
||||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||||
@@ -2702,6 +3032,23 @@
|
|||||||
"node": ">= 16"
|
"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": {
|
"node_modules/classnames": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
||||||
@@ -2750,6 +3097,14 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -3060,6 +3415,20 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/doctrine": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||||
@@ -3808,7 +4177,7 @@
|
|||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"to-regex-range": "^5.0.1"
|
"to-regex-range": "^5.0.1"
|
||||||
@@ -4522,7 +4891,7 @@
|
|||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -4577,7 +4946,7 @@
|
|||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-extglob": "^2.1.1"
|
"is-extglob": "^2.1.1"
|
||||||
@@ -4616,7 +4985,7 @@
|
|||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.12.0"
|
"node": ">=0.12.0"
|
||||||
@@ -5119,7 +5488,7 @@
|
|||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"braces": "^3.0.3",
|
"braces": "^3.0.3",
|
||||||
@@ -5184,6 +5553,14 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/node-html-parser": {
|
||||||
"version": "6.1.13",
|
"version": "6.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz",
|
||||||
@@ -5512,7 +5889,7 @@
|
|||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
@@ -5754,6 +6131,21 @@
|
|||||||
"react-dom": ">=18"
|
"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": {
|
"node_modules/redent": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
||||||
@@ -6054,6 +6446,28 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/sass-embedded": {
|
||||||
"version": "1.89.2",
|
"version": "1.89.2",
|
||||||
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.89.2.tgz",
|
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.89.2.tgz",
|
||||||
@@ -6583,6 +6997,29 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/stack-trace": {
|
||||||
"version": "1.0.0-pre2",
|
"version": "1.0.0-pre2",
|
||||||
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-1.0.0-pre2.tgz",
|
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-1.0.0-pre2.tgz",
|
||||||
@@ -6849,6 +7286,26 @@
|
|||||||
"node": ">=16.0.0"
|
"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": {
|
"node_modules/tinybench": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||||
@@ -6864,13 +7321,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.14",
|
"version": "0.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
|
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.5.0",
|
||||||
"picomatch": "^4.0.2"
|
"picomatch": "^4.0.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
@@ -6880,10 +7337,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinyglobby/node_modules/fdir": {
|
"node_modules/tinyglobby/node_modules/fdir": {
|
||||||
"version": "6.4.6",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
|
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"picomatch": "^3 || ^4"
|
"picomatch": "^3 || ^4"
|
||||||
},
|
},
|
||||||
@@ -6894,9 +7354,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinyglobby/node_modules/picomatch": {
|
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@@ -6959,7 +7419,7 @@
|
|||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-number": "^7.0.0"
|
"is-number": "^7.0.0"
|
||||||
@@ -7200,17 +7660,17 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.0.4",
|
"version": "7.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz",
|
||||||
"integrity": "sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA==",
|
"integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.6",
|
"fdir": "^6.5.0",
|
||||||
"picomatch": "^4.0.2",
|
"picomatch": "^4.0.3",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"rollup": "^4.40.0",
|
"rollup": "^4.43.0",
|
||||||
"tinyglobby": "^0.2.14"
|
"tinyglobby": "^0.2.15"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vite": "bin/vite.js"
|
"vite": "bin/vite.js"
|
||||||
@@ -7315,10 +7775,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite/node_modules/fdir": {
|
"node_modules/vite/node_modules/fdir": {
|
||||||
"version": "6.4.6",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
|
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"picomatch": "^3 || ^4"
|
"picomatch": "^3 || ^4"
|
||||||
},
|
},
|
||||||
@@ -7329,9 +7792,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite/node_modules/picomatch": {
|
"node_modules/vite/node_modules/picomatch": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"react-input-mask": "^2.0.4",
|
"react-input-mask": "^2.0.4",
|
||||||
"react-router-dom": "^7.6.3",
|
"react-router-dom": "^7.6.3",
|
||||||
"uplot": "^1.6.32",
|
"uplot": "^1.6.32",
|
||||||
"vite": "^7.0.4",
|
"vite": "^7.1.5",
|
||||||
"web-vitals": "^5.0.3"
|
"web-vitals": "^5.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -18,84 +18,96 @@ import QueryAnalyzer from "./pages/QueryAnalyzer";
|
|||||||
import DownsamplingFilters from "./pages/DownsamplingFilters";
|
import DownsamplingFilters from "./pages/DownsamplingFilters";
|
||||||
import RetentionFilters from "./pages/RetentionFilters";
|
import RetentionFilters from "./pages/RetentionFilters";
|
||||||
import RawQueryPage from "./pages/RawQueryPage";
|
import RawQueryPage from "./pages/RawQueryPage";
|
||||||
|
import ExploreRules from "./pages/ExploreAlerts/ExploreRules";
|
||||||
|
import ExploreNotifiers from "./pages/ExploreAlerts/ExploreNotifiers";
|
||||||
|
|
||||||
const App: FC = () => {
|
const App: FC = () => {
|
||||||
const [loadedTheme, setLoadedTheme] = useState(false);
|
const [loadedTheme, setLoadedTheme] = useState(false);
|
||||||
|
|
||||||
return <>
|
return (
|
||||||
<HashRouter>
|
<>
|
||||||
<AppContextProvider>
|
<HashRouter>
|
||||||
<>
|
<AppContextProvider>
|
||||||
<ThemeProvider onLoaded={setLoadedTheme}/>
|
<>
|
||||||
{loadedTheme && (
|
<ThemeProvider onLoaded={setLoadedTheme} />
|
||||||
<Routes>
|
{loadedTheme && (
|
||||||
<Route
|
<Routes>
|
||||||
path={"/"}
|
|
||||||
element={<MainLayout/>}
|
|
||||||
>
|
|
||||||
<Route
|
<Route
|
||||||
path={router.home}
|
path={"/"}
|
||||||
element={<CustomPanel/>}
|
element={<MainLayout />}
|
||||||
/>
|
>
|
||||||
<Route
|
<Route
|
||||||
path={router.rawQuery}
|
path={router.home}
|
||||||
element={<RawQueryPage/>}
|
element={<CustomPanel />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={router.metrics}
|
path={router.rawQuery}
|
||||||
element={<ExploreMetrics/>}
|
element={<RawQueryPage />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={router.cardinality}
|
path={router.metrics}
|
||||||
element={<CardinalityPanel/>}
|
element={<ExploreMetrics />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={router.topQueries}
|
path={router.cardinality}
|
||||||
element={<TopQueries/>}
|
element={<CardinalityPanel />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={router.trace}
|
path={router.topQueries}
|
||||||
element={<TracePage/>}
|
element={<TopQueries />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={router.queryAnalyzer}
|
path={router.trace}
|
||||||
element={<QueryAnalyzer/>}
|
element={<TracePage />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={router.dashboards}
|
path={router.queryAnalyzer}
|
||||||
element={<DashboardsLayout/>}
|
element={<QueryAnalyzer />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={router.withTemplate}
|
path={router.dashboards}
|
||||||
element={<WithTemplate/>}
|
element={<DashboardsLayout />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={router.relabel}
|
path={router.withTemplate}
|
||||||
element={<Relabel/>}
|
element={<WithTemplate />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={router.activeQueries}
|
path={router.relabel}
|
||||||
element={<ActiveQueries/>}
|
element={<Relabel />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={router.icons}
|
path={router.activeQueries}
|
||||||
element={<PreviewIcons/>}
|
element={<ActiveQueries />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={router.downsamplingDebug}
|
path={router.icons}
|
||||||
element={<DownsamplingFilters/>}
|
element={<PreviewIcons />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={router.retentionDebug}
|
path={router.downsamplingDebug}
|
||||||
element={<RetentionFilters/>}
|
element={<DownsamplingFilters />}
|
||||||
/>
|
/>
|
||||||
</Route>
|
<Route
|
||||||
</Routes>
|
path={router.retentionDebug}
|
||||||
)}
|
element={<RetentionFilters />}
|
||||||
</>
|
/>
|
||||||
</AppContextProvider>
|
<Route
|
||||||
</HashRouter>
|
path={router.rules}
|
||||||
</>;
|
element={<ExploreRules />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={router.notifiers}
|
||||||
|
element={<ExploreNotifiers />}
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</AppContextProvider>
|
||||||
|
</HashRouter>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export const getAccountIds = (server: string) =>
|
import { getUrlWithoutTenant } from "../utils/tenants";
|
||||||
`${server.replace(/^(.+)(\/select.+)/, "$1")}/admin/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");
|
if (reduceMemUsage) params.set("reduce_mem_usage", "1");
|
||||||
return `${server}/api/v1/export?${params}`;
|
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}`;
|
||||||
|
};
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ However, there are some [intentional differences](https://medium.com/@romanhavro
|
|||||||
|
|
||||||
[Standalone MetricsQL package](https://godoc.org/github.com/VictoriaMetrics/metricsql) can be used for parsing MetricsQL in external apps.
|
[Standalone MetricsQL package](https://godoc.org/github.com/VictoriaMetrics/metricsql) can be used for parsing MetricsQL in external apps.
|
||||||
|
|
||||||
If you are unfamiliar with PromQL, then it is suggested reading [this tutorial for beginners](https://medium.com/@valyala/promql-tutorial-for-beginners-9ab455142085)
|
If you are unfamiliar with PromQL, we suggest reading [this tutorial for beginners](https://medium.com/@valyala/promql-tutorial-for-beginners-9ab455142085)
|
||||||
and introduction into [basic querying via MetricsQL](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#metricsql).
|
and introduction into [basic querying via MetricsQL](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#metricsql).
|
||||||
|
|
||||||
The following functionality is implemented differently in MetricsQL compared to PromQL. This improves user experience:
|
The following functionality is implemented differently in MetricsQL compared to PromQL. This improves user experience:
|
||||||
@@ -69,13 +69,13 @@ The list of MetricsQL features on top of PromQL:
|
|||||||
See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#selecting-graphite-metrics).
|
See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#selecting-graphite-metrics).
|
||||||
VictoriaMetrics can be used as Graphite datasource in Grafana. See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#graphite-api-usage) for details.
|
VictoriaMetrics can be used as Graphite datasource in Grafana. See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#graphite-api-usage) for details.
|
||||||
See also [label_graphite_group](#label_graphite_group) function, which can be used for extracting the given groups from Graphite metric name.
|
See also [label_graphite_group](#label_graphite_group) function, which can be used for extracting the given groups from Graphite metric name.
|
||||||
* Lookbehind window in square brackets for [rollup functions](#rollup-functions) may be omitted. VictoriaMetrics automatically selects the lookbehind window
|
* The lookbehind window in square brackets for [rollup functions](#rollup-functions) may be omitted. VictoriaMetrics automatically selects the lookbehind window
|
||||||
depending on the `step` query arg passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query)
|
depending on the `step` query arg passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query)
|
||||||
and the real interval between [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples) (aka `scrape_interval`).
|
and the real interval between [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples) (aka `scrape_interval`).
|
||||||
For instance, the following query is valid in VictoriaMetrics: `rate(node_network_receive_bytes_total)`.
|
For instance, the following query is valid in VictoriaMetrics: `rate(node_network_receive_bytes_total)`.
|
||||||
It is roughly equivalent to `rate(node_network_receive_bytes_total[$__interval])` when used in Grafana.
|
It is roughly equivalent to `rate(node_network_receive_bytes_total[$__interval])` when used in Grafana.
|
||||||
The difference is documented in [rate() docs](#rate).
|
The difference is documented in [rate() docs](#rate).
|
||||||
* Numeric values can contain `_` delimiters for better readability. For example, `1_234_567_890` can be used in queries instead of `1234567890`.
|
* Numeric values may include underscore delimiters for better readability. For example, `1_234_567_890` can be used in queries instead of `1234567890`.
|
||||||
* [Series selectors](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering) accept multiple `or` filters. For example, `{env="prod",job="a" or env="dev",job="b"}`
|
* [Series selectors](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering) accept multiple `or` filters. For example, `{env="prod",job="a" or env="dev",job="b"}`
|
||||||
selects series with `{env="prod",job="a"}` or `{env="dev",job="b"}` labels.
|
selects series with `{env="prod",job="a"}` or `{env="dev",job="b"}` labels.
|
||||||
See [these docs](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering-by-multiple-or-filters) for details.
|
See [these docs](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering-by-multiple-or-filters) for details.
|
||||||
@@ -111,8 +111,8 @@ The list of MetricsQL features on top of PromQL:
|
|||||||
* Metric names and labels names may contain escaped chars. For example, `foo\-bar{baz\=aa="b"}` is valid expression.
|
* Metric names and labels names may contain escaped chars. For example, `foo\-bar{baz\=aa="b"}` is valid expression.
|
||||||
It returns time series with name `foo-bar` containing label `baz=aa` with value `b`.
|
It returns time series with name `foo-bar` containing label `baz=aa` with value `b`.
|
||||||
Additionally, the following escape sequences are supported:
|
Additionally, the following escape sequences are supported:
|
||||||
- `\xXX`, where `XX` is hexadecimal representation of the escaped ascii char.
|
* `\xXX`, where `XX` is hexadecimal representation of the escaped ascii char.
|
||||||
- `\uXXXX`, where `XXXX` is a hexadecimal representation of the escaped unicode char.
|
* `\uXXXX`, where `XXXX` is a hexadecimal representation of the escaped unicode char.
|
||||||
* Aggregate functions support optional `limit N` suffix in order to limit the number of output series.
|
* Aggregate functions support optional `limit N` suffix in order to limit the number of output series.
|
||||||
For example, `sum(x) by (y) limit 3` limits the number of output time series after the aggregation to 3.
|
For example, `sum(x) by (y) limit 3` limits the number of output time series after the aggregation to 3.
|
||||||
All the other time series are dropped.
|
All the other time series are dropped.
|
||||||
@@ -138,8 +138,9 @@ This may result in `duplicate time series` error when the function is applied to
|
|||||||
This error can be fixed by applying `keep_metric_names` modifier to the function or binary operator.
|
This error can be fixed by applying `keep_metric_names` modifier to the function or binary operator.
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
- `rate({__name__=~"foo|bar"}) keep_metric_names` leaves `foo` and `bar` metric names in the returned time series.
|
|
||||||
- `({__name__=~"foo|bar"} / 10) keep_metric_names` leaves `foo` and `bar` metric names in the returned time series.
|
* `rate({__name__=~"foo|bar"}) keep_metric_names` leaves `foo` and `bar` metric names in the returned time series.
|
||||||
|
* `({__name__=~"foo|bar"} / 10) keep_metric_names` leaves `foo` and `bar` metric names in the returned time series.
|
||||||
|
|
||||||
## MetricsQL functions
|
## MetricsQL functions
|
||||||
|
|
||||||
@@ -166,10 +167,10 @@ Additional details:
|
|||||||
* If the given [series selector](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering) returns multiple time series,
|
* If the given [series selector](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering) returns multiple time series,
|
||||||
then rollups are calculated individually per each returned series.
|
then rollups are calculated individually per each returned series.
|
||||||
* If lookbehind window in square brackets is missing, then it is automatically set to the following value:
|
* If lookbehind window in square brackets is missing, then it is automatically set to the following value:
|
||||||
- To `step` value passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query) or [/api/v1/query](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#instant-query)
|
* To `step` value passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query) or [/api/v1/query](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#instant-query)
|
||||||
for all the [rollup functions](#rollup-functions) except of [default_rollup](#default_rollup) and [rate](#rate). This value is known as `$__interval` in Grafana or `1i` in MetricsQL.
|
for all the [rollup functions](#rollup-functions) except of [default_rollup](#default_rollup) and [rate](#rate). This value is known as `$__interval` in Grafana or `1i` in MetricsQL.
|
||||||
For example, `avg_over_time(temperature)` is automatically transformed to `avg_over_time(temperature[1i])`.
|
For example, `avg_over_time(temperature)` is automatically transformed to `avg_over_time(temperature[1i])`.
|
||||||
- To the `max(step, scrape_interval)`, where `scrape_interval` is the interval between [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples)
|
* To the `max(step, scrape_interval)`, where `scrape_interval` is the interval between [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples)
|
||||||
for [default_rollup](#default_rollup) and [rate](#rate) functions. This allows avoiding unexpected gaps on the graph when `step` is smaller than `scrape_interval`.
|
for [default_rollup](#default_rollup) and [rate](#rate) functions. This allows avoiding unexpected gaps on the graph when `step` is smaller than `scrape_interval`.
|
||||||
* Every [series selector](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering) in MetricsQL must be wrapped into a rollup function.
|
* Every [series selector](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering) in MetricsQL must be wrapped into a rollup function.
|
||||||
Otherwise, it is automatically wrapped into [default_rollup](#default_rollup). For example, `foo{bar="baz"}`
|
Otherwise, it is automatically wrapped into [default_rollup](#default_rollup). For example, `foo{bar="baz"}`
|
||||||
@@ -666,8 +667,9 @@ This function is usually applied to [gauges](https://docs.victoriametrics.com/vi
|
|||||||
|
|
||||||
`outlier_iqr_over_time(series_selector[d])` is a [rollup function](#rollup-functions), which returns the last sample on the given lookbehind window `d`
|
`outlier_iqr_over_time(series_selector[d])` is a [rollup function](#rollup-functions), which returns the last sample on the given lookbehind window `d`
|
||||||
if its value is either smaller than the `q25-1.5*iqr` or bigger than `q75+1.5*iqr` where:
|
if its value is either smaller than the `q25-1.5*iqr` or bigger than `q75+1.5*iqr` where:
|
||||||
- `iqr` is an [Interquartile range](https://en.wikipedia.org/wiki/Interquartile_range) over [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples) on the lookbehind window `d`
|
|
||||||
- `q25` and `q75` are 25th and 75th [percentiles](https://en.wikipedia.org/wiki/Percentile) over [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples) on the lookbehind window `d`.
|
* `iqr` is an [Interquartile range](https://en.wikipedia.org/wiki/Interquartile_range) over [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples) on the lookbehind window `d`
|
||||||
|
* `q25` and `q75` are 25th and 75th [percentiles](https://en.wikipedia.org/wiki/Percentile) over [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples) on the lookbehind window `d`.
|
||||||
|
|
||||||
The `outlier_iqr_over_time()` is useful for detecting anomalies in gauge values based on the previous history of values.
|
The `outlier_iqr_over_time()` is useful for detecting anomalies in gauge values based on the previous history of values.
|
||||||
For example, `outlier_iqr_over_time(memory_usage_bytes[1h])` triggers when `memory_usage_bytes` suddenly goes outside the usual value range for the last hour.
|
For example, `outlier_iqr_over_time(memory_usage_bytes[1h])` triggers when `memory_usage_bytes` suddenly goes outside the usual value range for the last hour.
|
||||||
@@ -759,7 +761,6 @@ This function is usually applied to [counters](https://docs.victoriametrics.com/
|
|||||||
|
|
||||||
See also [increase_prometheus](#increase_prometheus) and [rate](#rate).
|
See also [increase_prometheus](#increase_prometheus) and [rate](#rate).
|
||||||
|
|
||||||
|
|
||||||
#### rate_over_sum
|
#### rate_over_sum
|
||||||
|
|
||||||
`rate_over_sum(series_selector[d])` is a [rollup function](#rollup-functions), which calculates per-second rate over the sum of [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples)
|
`rate_over_sum(series_selector[d])` is a [rollup function](#rollup-functions), which calculates per-second rate over the sum of [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples)
|
||||||
@@ -1106,7 +1107,6 @@ This function is usually applied to [gauges](https://docs.victoriametrics.com/vi
|
|||||||
|
|
||||||
See also [zscore](#zscore), [range_trim_zscore](#range_trim_zscore) and [outlier_iqr_over_time](#outlier_iqr_over_time).
|
See also [zscore](#zscore), [range_trim_zscore](#range_trim_zscore) and [outlier_iqr_over_time](#outlier_iqr_over_time).
|
||||||
|
|
||||||
|
|
||||||
### Transform functions
|
### Transform functions
|
||||||
|
|
||||||
**Transform functions** calculate transformations over [rollup results](#rollup-functions).
|
**Transform functions** calculate transformations over [rollup results](#rollup-functions).
|
||||||
@@ -1851,7 +1851,6 @@ The list of supported label manipulation functions:
|
|||||||
`alias(q, "name")` is [label manipulation function](#label-manipulation-functions), which sets the given `name` to all the time series returned by `q`.
|
`alias(q, "name")` is [label manipulation function](#label-manipulation-functions), which sets the given `name` to all the time series returned by `q`.
|
||||||
For example, `alias(up, "foobar")` would rename `up` series to `foobar` series.
|
For example, `alias(up, "foobar")` would rename `up` series to `foobar` series.
|
||||||
|
|
||||||
|
|
||||||
#### drop_common_labels
|
#### drop_common_labels
|
||||||
|
|
||||||
`drop_common_labels(q1, ...., qN)` is [label manipulation function](#label-manipulation-functions), which drops common `label="value"` pairs
|
`drop_common_labels(q1, ...., qN)` is [label manipulation function](#label-manipulation-functions), which drops common `label="value"` pairs
|
||||||
@@ -1877,7 +1876,7 @@ For example, `label_graphite_group({__graphite__="foo*.bar.*"}, 0, 2)` would sub
|
|||||||
|
|
||||||
This function is useful for aggregating Graphite metrics with [aggregate functions](#aggregate-functions). For example, the following query would return per-app memory usage:
|
This function is useful for aggregating Graphite metrics with [aggregate functions](#aggregate-functions). For example, the following query would return per-app memory usage:
|
||||||
|
|
||||||
```
|
```metricsql
|
||||||
sum by (__name__) (
|
sum by (__name__) (
|
||||||
label_graphite_group({__graphite__="app*.host*.memory_usage"}, 0)
|
label_graphite_group({__graphite__="app*.host*.memory_usage"}, 0)
|
||||||
)
|
)
|
||||||
@@ -2003,7 +2002,6 @@ would return series in the following order of `bar` label values: `101`, `15`, `
|
|||||||
|
|
||||||
See also [sort_by_label_numeric](#sort_by_label_numeric) and [sort_by_label_desc](#sort_by_label_desc).
|
See also [sort_by_label_numeric](#sort_by_label_numeric) and [sort_by_label_desc](#sort_by_label_desc).
|
||||||
|
|
||||||
|
|
||||||
### Aggregate functions
|
### Aggregate functions
|
||||||
|
|
||||||
**Aggregate functions** calculate aggregates over groups of [rollup results](#rollup-functions).
|
**Aggregate functions** calculate aggregates over groups of [rollup results](#rollup-functions).
|
||||||
@@ -2179,8 +2177,9 @@ per each `group_labels` for all the time series returned by `q`. The aggregate i
|
|||||||
`outliers_iqr(q)` is [aggregate function](#aggregate-functions), which returns time series from `q` with at least a single point
|
`outliers_iqr(q)` is [aggregate function](#aggregate-functions), which returns time series from `q` with at least a single point
|
||||||
outside e.g. [Interquartile range outlier bounds](https://en.wikipedia.org/wiki/Interquartile_range) `[q25-1.5*iqr .. q75+1.5*iqr]`
|
outside e.g. [Interquartile range outlier bounds](https://en.wikipedia.org/wiki/Interquartile_range) `[q25-1.5*iqr .. q75+1.5*iqr]`
|
||||||
comparing to other time series at the given point, where:
|
comparing to other time series at the given point, where:
|
||||||
- `iqr` is an [Interquartile range](https://en.wikipedia.org/wiki/Interquartile_range) calculated independently per each point on the graph across `q` series.
|
|
||||||
- `q25` and `q75` are 25th and 75th [percentiles](https://en.wikipedia.org/wiki/Percentile) calculated independently per each point on the graph across `q` series.
|
* `iqr` is an [Interquartile range](https://en.wikipedia.org/wiki/Interquartile_range) calculated independently per each point on the graph across `q` series.
|
||||||
|
* `q25` and `q75` are 25th and 75th [percentiles](https://en.wikipedia.org/wiki/Percentile) calculated independently per each point on the graph across `q` series.
|
||||||
|
|
||||||
The `outliers_iqr()` is useful for detecting anomalous series in the group of series. For example, `outliers_iqr(temperature) by (country)` returns
|
The `outliers_iqr()` is useful for detecting anomalous series in the group of series. For example, `outliers_iqr(temperature) by (country)` returns
|
||||||
per-country series with anomalous outlier values comparing to the rest of per-country series.
|
per-country series with anomalous outlier values comparing to the rest of per-country series.
|
||||||
@@ -2349,10 +2348,10 @@ VictoriaMetrics performs subqueries in the following way:
|
|||||||
VictoriaMetrics performs the following implicit conversions for incoming queries before starting the calculations:
|
VictoriaMetrics performs the following implicit conversions for incoming queries before starting the calculations:
|
||||||
|
|
||||||
* If lookbehind window in square brackets is missing inside [rollup function](#rollup-functions), then it is automatically set to the following value:
|
* If lookbehind window in square brackets is missing inside [rollup function](#rollup-functions), then it is automatically set to the following value:
|
||||||
- To `step` value passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query) or [/api/v1/query](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#instant-query)
|
* To `step` value passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query) or [/api/v1/query](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#instant-query)
|
||||||
for all the [rollup functions](#rollup-functions) except of [default_rollup](#default_rollup) and [rate](#rate). This value is known as `$__interval` in Grafana or `1i` in MetricsQL.
|
for all the [rollup functions](#rollup-functions) except of [default_rollup](#default_rollup) and [rate](#rate). This value is known as `$__interval` in Grafana or `1i` in MetricsQL.
|
||||||
For example, `avg_over_time(temperature)` is automatically transformed to `avg_over_time(temperature[1i])`.
|
For example, `avg_over_time(temperature)` is automatically transformed to `avg_over_time(temperature[1i])`.
|
||||||
- To the `max(step, scrape_interval)`, where `scrape_interval` is the interval between [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples)
|
* To the `max(step, scrape_interval)`, where `scrape_interval` is the interval between [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples)
|
||||||
for [default_rollup](#default_rollup) and [rate](#rate) functions. This allows avoiding unexpected gaps on the graph when `step` is smaller than `scrape_interval`.
|
for [default_rollup](#default_rollup) and [rate](#rate) functions. This allows avoiding unexpected gaps on the graph when `step` is smaller than `scrape_interval`.
|
||||||
* All the [series selectors](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering),
|
* All the [series selectors](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering),
|
||||||
which aren't wrapped into [rollup functions](#rollup-functions), are automatically wrapped into [default_rollup](#default_rollup) function.
|
which aren't wrapped into [rollup functions](#rollup-functions), are automatically wrapped into [default_rollup](#default_rollup) function.
|
||||||
|
|||||||
@@ -30,7 +30,13 @@ const delayOptions: AutoRefreshOption[] = [
|
|||||||
{ seconds: 7200, title: "2h" }
|
{ 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 { isMobile } = useDeviceDetect();
|
||||||
|
|
||||||
const dispatch = useTimeDispatch();
|
const dispatch = useTimeDispatch();
|
||||||
@@ -56,6 +62,9 @@ export const ExecutionControls: FC = () => {
|
|||||||
|
|
||||||
const handleUpdate = () => {
|
const handleUpdate = () => {
|
||||||
dispatch({ type: "RUN_QUERY" });
|
dispatch({ type: "RUN_QUERY" });
|
||||||
|
if (!useAutorefresh && isMobile) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -77,91 +86,118 @@ export const ExecutionControls: FC = () => {
|
|||||||
handleChange(d);
|
handleChange(d);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <>
|
return (
|
||||||
<div className="vm-execution-controls">
|
<>
|
||||||
<div
|
<div className="vm-execution-controls">
|
||||||
className={classNames({
|
<div
|
||||||
"vm-execution-controls-buttons": true,
|
className={classNames({
|
||||||
"vm-execution-controls-buttons_mobile": isMobile,
|
"vm-execution-controls-buttons": true,
|
||||||
"vm-header-button": !appModeEnable,
|
"vm-execution-controls-buttons_mobile": isMobile,
|
||||||
})}
|
"vm-header-button": !appModeEnable,
|
||||||
>
|
"vm-autorefresh": useAutorefresh,
|
||||||
{!isMobile && (
|
})}
|
||||||
<Tooltip title="Refresh dashboard">
|
>
|
||||||
<Button
|
{useAutorefresh ? (
|
||||||
variant="contained"
|
isMobile ? (
|
||||||
color="primary"
|
<div
|
||||||
onClick={handleUpdate}
|
className="vm-mobile-option"
|
||||||
startIcon={<RefreshIcon/>}
|
onClick={toggleOpenOptions}
|
||||||
ariaLabel="refresh dashboard"
|
>
|
||||||
/>
|
<span className="vm-mobile-option__icon"><RestartIcon/></span>
|
||||||
</Tooltip>
|
<div className="vm-mobile-option-text">
|
||||||
)}
|
<span className="vm-mobile-option-text__label">Auto-refresh</span>
|
||||||
{isMobile ? (
|
<span className="vm-mobile-option-text__value">{selectedDelay.title}</span>
|
||||||
<div
|
</div>
|
||||||
className="vm-mobile-option"
|
<span className="vm-mobile-option__arrow"><ArrowDownIcon/></span>
|
||||||
onClick={toggleOpenOptions}
|
</div>
|
||||||
>
|
) : (
|
||||||
<span className="vm-mobile-option__icon"><RestartIcon/></span>
|
<>
|
||||||
<div className="vm-mobile-option-text">
|
<Tooltip title={tooltip}>
|
||||||
<span className="vm-mobile-option-text__label">Auto-refresh</span>
|
<Button
|
||||||
<span className="vm-mobile-option-text__value">{selectedDelay.title}</span>
|
variant="contained"
|
||||||
</div>
|
color="primary"
|
||||||
<span className="vm-mobile-option__arrow"><ArrowDownIcon/></span>
|
onClick={handleUpdate}
|
||||||
</div>
|
startIcon={<RefreshIcon/>}
|
||||||
) : (
|
ariaLabel={tooltip}
|
||||||
<Tooltip title="Auto-refresh control">
|
/>
|
||||||
<div ref={optionsButtonRef}>
|
</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
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
fullWidth
|
onClick={handleUpdate}
|
||||||
endIcon={(
|
startIcon={<RefreshIcon/>}
|
||||||
<div
|
ariaLabel={tooltip}
|
||||||
className={classNames({
|
/>
|
||||||
"vm-execution-controls-buttons__arrow": true,
|
)
|
||||||
"vm-execution-controls-buttons__arrow_open": openOptions,
|
)}
|
||||||
})}
|
</div>
|
||||||
>
|
|
||||||
<ArrowDownIcon/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
onClick={toggleOpenOptions}
|
|
||||||
>
|
|
||||||
{selectedDelay.title}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{useAutorefresh && (
|
||||||
<Popper
|
<Popper
|
||||||
open={openOptions}
|
open={openOptions}
|
||||||
placement="bottom-right"
|
placement="bottom-right"
|
||||||
onClose={handleCloseOptions}
|
onClose={handleCloseOptions}
|
||||||
buttonRef={optionsButtonRef}
|
buttonRef={optionsButtonRef}
|
||||||
title={isMobile ? "Auto-refresh duration" : undefined}
|
title={isMobile ? "Auto-refresh duration" : undefined}
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
className={classNames({
|
|
||||||
"vm-execution-controls-list": true,
|
|
||||||
"vm-execution-controls-list_mobile": isMobile,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{delayOptions.map(d => (
|
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"vm-list-item": true,
|
"vm-execution-controls-list": true,
|
||||||
"vm-list-item_mobile": isMobile,
|
"vm-execution-controls-list_mobile": isMobile,
|
||||||
"vm-list-item_active": d.seconds === selectedDelay.seconds
|
|
||||||
})}
|
})}
|
||||||
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>
|
||||||
</div>
|
)}
|
||||||
</Popper>
|
</>
|
||||||
</>;
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,10 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
border-radius: calc($button-radius + 1px);
|
border-radius: calc($button-radius + 1px);
|
||||||
min-width: 107px;
|
|
||||||
|
:is(.vm-autorefresh) {
|
||||||
|
min-width: 107px;
|
||||||
|
}
|
||||||
|
|
||||||
&_mobile {
|
&_mobile {
|
||||||
flex-direction: column;
|
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 Tooltip from "../Main/Tooltip/Tooltip";
|
||||||
import Button from "../Main/Button/Button";
|
import Button from "../Main/Button/Button";
|
||||||
import { DownloadIcon } from "../Main/Icons";
|
import { DownloadIcon } from "../Main/Icons";
|
||||||
import Popper from "../Main/Popper/Popper";
|
import Popper from "../Main/Popper/Popper";
|
||||||
import { useRef } from "react";
|
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import useBoolean from "../../hooks/useBoolean";
|
import useBoolean from "../../hooks/useBoolean";
|
||||||
|
|
||||||
interface DownloadButtonProps {
|
interface DownloadButtonProps<T extends string> {
|
||||||
title: string;
|
title: string;
|
||||||
downloadFormatOptions?: string[];
|
downloadFormatOptions?: T[];
|
||||||
onDownload: (format?: string) => void;
|
onDownload: (format?: T) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** TODO: Currently unused, later will be added for the exporting metrics */
|
const DownloadButton = <T extends string>({ title, downloadFormatOptions, onDownload }: DownloadButtonProps<T>) => {
|
||||||
const DownloadButton: FC<DownloadButtonProps> = ({ title, downloadFormatOptions, onDownload }) => {
|
|
||||||
const {
|
const {
|
||||||
value: isPopupOpen,
|
value: isPopupOpen,
|
||||||
setTrue: onOpenPopup,
|
setTrue: onOpenPopup,
|
||||||
@@ -35,9 +33,19 @@ const DownloadButton: FC<DownloadButtonProps> = ({ title, downloadFormatOptions,
|
|||||||
}
|
}
|
||||||
}, [onDownload, onClosePopup, isPopupOpen, onOpenPopup]);
|
}, [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 onDownloadFormatClick = useCallback((event: Event) => {
|
||||||
const button = event.currentTarget as HTMLButtonElement;
|
const button = event.currentTarget as HTMLButtonElement;
|
||||||
onDownload(button.textContent ?? undefined);
|
const format = button.textContent;
|
||||||
|
if (format && isDownloadFormat(format)) {
|
||||||
|
onDownload(format);
|
||||||
|
} else {
|
||||||
|
onDownload();
|
||||||
|
}
|
||||||
|
onClosePopup();
|
||||||
}, [onDownload]);
|
}, [onDownload]);
|
||||||
|
|
||||||
return (
|
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,106 @@
|
|||||||
|
import { useMemo } from "preact/compat";
|
||||||
|
import "./style.scss";
|
||||||
|
import { Alert as APIAlert } from "../../../types";
|
||||||
|
import { createSearchParams } from "react-router-dom";
|
||||||
|
import Button from "../../Main/Button/Button";
|
||||||
|
import Badges, { BadgeColor } from "../Badges";
|
||||||
|
import {
|
||||||
|
SearchIcon,
|
||||||
|
} from "../../Main/Icons";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import CodeExample from "../../Main/CodeExample/CodeExample";
|
||||||
|
|
||||||
|
interface BaseAlertProps {
|
||||||
|
item: APIAlert;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BaseAlert = ({ item }: BaseAlertProps) => {
|
||||||
|
const query = item?.expression;
|
||||||
|
const alertLabels = item?.labels || {};
|
||||||
|
const alertLabelsItems = useMemo(() => {
|
||||||
|
return Object.fromEntries(Object.entries(alertLabels).map(([name, value]) => [name, {
|
||||||
|
color: "passive" as BadgeColor,
|
||||||
|
value: value,
|
||||||
|
}]));
|
||||||
|
}, [alertLabels]);
|
||||||
|
|
||||||
|
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>
|
||||||
|
<colgroup>
|
||||||
|
<col className="vm-col-md"/>
|
||||||
|
<col/>
|
||||||
|
</colgroup>
|
||||||
|
<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>Query</td>
|
||||||
|
<td>
|
||||||
|
<CodeExample
|
||||||
|
code={query}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Active at</td>
|
||||||
|
<td>{dayjs(item.activeAt).format("DD MMM YYYY HH:mm:ss")}</td>
|
||||||
|
</tr>
|
||||||
|
{!!Object.keys(alertLabels).length && (
|
||||||
|
<tr>
|
||||||
|
<td>Labels</td>
|
||||||
|
<td>
|
||||||
|
<Badges
|
||||||
|
items={alertLabelsItems}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{!!Object.keys(item.annotations || {}).length && (
|
||||||
|
<>
|
||||||
|
<span className="title">Annotations</span>
|
||||||
|
<table>
|
||||||
|
<colgroup>
|
||||||
|
<col className="vm-col-md"/>
|
||||||
|
<col/>
|
||||||
|
</colgroup>
|
||||||
|
<tbody>
|
||||||
|
{Object.entries(item.annotations || {}).map(([name, value]) => (
|
||||||
|
<tr key={name}>
|
||||||
|
<td>{name}</td>
|
||||||
|
<td>{value}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BaseAlert;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
@use "src/styles/variables" as *;
|
||||||
|
|
||||||
|
.vm-modal.vm-explore-alerts {
|
||||||
|
.vm-modal-content {
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-explore-alerts-alert-item {
|
||||||
|
row-gap: $padding-global;
|
||||||
|
margin: $padding-global;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-col-md {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-button {
|
||||||
|
color: $color-passive;
|
||||||
|
border: 1px solid $color-passive;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-code-example {
|
||||||
|
.vm-button {
|
||||||
|
background-color: $color-code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
word-break: break-word;
|
||||||
|
table-layout: fixed;
|
||||||
|
width: 100%;
|
||||||
|
td, th {
|
||||||
|
line-height: 30px;
|
||||||
|
padding: 4px $padding-small;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: left;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0 $padding-small;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import { useMemo } from "preact/compat";
|
||||||
|
import "./style.scss";
|
||||||
|
import { Group as APIGroup } from "../../../types";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { formatDuration } from "../helpers";
|
||||||
|
import Badges, { BadgeColor } from "../Badges";
|
||||||
|
|
||||||
|
interface BaseGroupProps {
|
||||||
|
group: APIGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BaseGroup = ({ group }: BaseGroupProps) => {
|
||||||
|
const groupLabels = group?.labels || {};
|
||||||
|
const groupLabelsItems = useMemo(() => {
|
||||||
|
return Object.fromEntries(Object.entries(groupLabels).map(([name, value]) => [name, {
|
||||||
|
color: "passive" as BadgeColor,
|
||||||
|
value: value,
|
||||||
|
}]));
|
||||||
|
}, [groupLabels]);
|
||||||
|
|
||||||
|
const groupParams = group?.params || [];
|
||||||
|
const groupParamsItems = useMemo(() => {
|
||||||
|
return Object.fromEntries(groupParams.map(value => [value, {
|
||||||
|
color: "passive" as BadgeColor,
|
||||||
|
}]));
|
||||||
|
}, [groupParams]);
|
||||||
|
|
||||||
|
const groupHeaders = group?.headers || [];
|
||||||
|
const groupHeadersItems = useMemo(() => {
|
||||||
|
return Object.fromEntries(groupHeaders.map(value => [value, {
|
||||||
|
color: "passive" as BadgeColor,
|
||||||
|
}]));
|
||||||
|
}, [groupHeaders]);
|
||||||
|
|
||||||
|
const groupNotifierHeaders = group?.notifier_headers || [];
|
||||||
|
const groupNotifierHeadersItems = useMemo(() => {
|
||||||
|
return Object.fromEntries(groupNotifierHeaders.map(value => [value, {
|
||||||
|
color: "passive" as BadgeColor,
|
||||||
|
}]));
|
||||||
|
}, [groupNotifierHeaders]);
|
||||||
|
return (
|
||||||
|
<div className="vm-explore-alerts-group">
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
{!!Object.keys(groupLabels).length && (
|
||||||
|
<tr>
|
||||||
|
<td className="vm-col-md">Labels</td>
|
||||||
|
<td>
|
||||||
|
<Badges
|
||||||
|
items={groupLabelsItems}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{!!groupParams.length && (
|
||||||
|
<tr>
|
||||||
|
<td className="vm-col-md">Params</td>
|
||||||
|
<td>
|
||||||
|
<Badges
|
||||||
|
items={groupParamsItems}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{!!groupHeaders.length && (
|
||||||
|
<tr>
|
||||||
|
<td className="vm-col-md">Headers</td>
|
||||||
|
<td>
|
||||||
|
<Badges
|
||||||
|
items={groupHeadersItems}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{!!groupNotifierHeaders.length && (
|
||||||
|
<tr>
|
||||||
|
<td className="vm-col-md">Notifier headers</td>
|
||||||
|
<td>
|
||||||
|
<Badges
|
||||||
|
items={groupNotifierHeadersItems}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BaseGroup;
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
@use "src/styles/variables" as *;
|
||||||
|
|
||||||
|
.vm-modal.vm-explore-alerts {
|
||||||
|
.vm-modal-content {
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-explore-alerts-group {
|
||||||
|
row-gap: $padding-global;
|
||||||
|
margin: $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-md {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
tr.hoverable {
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
background-color: $color-background-hover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
td, th {
|
||||||
|
line-height: 30px;
|
||||||
|
padding: 4px $padding-small;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: left;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 $padding-small;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
import { useMemo } from "preact/compat";
|
||||||
|
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";
|
||||||
|
import CodeExample from "../../Main/CodeExample/CodeExample";
|
||||||
|
|
||||||
|
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 ruleLabels = item?.labels || {};
|
||||||
|
const ruleLabelsItems = useMemo(() => {
|
||||||
|
return Object.fromEntries(Object.entries(ruleLabels).map(([name, value]) => [name, {
|
||||||
|
color: "passive" as BadgeColor,
|
||||||
|
value: value,
|
||||||
|
}]));
|
||||||
|
}, [ruleLabels]);
|
||||||
|
|
||||||
|
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">
|
||||||
|
<table>
|
||||||
|
<colgroup>
|
||||||
|
<col className="vm-col-md"/>
|
||||||
|
<col/>
|
||||||
|
</colgroup>
|
||||||
|
<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>Query</td>
|
||||||
|
<td>
|
||||||
|
<CodeExample
|
||||||
|
code={query}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{!!item.duration && (
|
||||||
|
<tr>
|
||||||
|
<td>For</td>
|
||||||
|
<td>{formatDuration(item.duration)}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{!!item.lastEvaluation && (
|
||||||
|
<tr>
|
||||||
|
<td>Last evaluation</td>
|
||||||
|
<td>{dayjs(item.lastEvaluation).format("DD MMM YYYY HH:mm:ss")}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{!!item.lastError && item.health !== "ok" && (
|
||||||
|
<tr>
|
||||||
|
<td>Last error</td>
|
||||||
|
<td>
|
||||||
|
<Alert variant="error">{item.lastError}</Alert>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{!!Object.keys(ruleLabelsItems).length && (
|
||||||
|
<tr>
|
||||||
|
<td>Labels</td>
|
||||||
|
<td>
|
||||||
|
<Badges
|
||||||
|
items={ruleLabelsItems}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{!!Object.keys(item?.annotations || {}).length && (
|
||||||
|
<>
|
||||||
|
<span className="title">Annotations</span>
|
||||||
|
<table>
|
||||||
|
<colgroup>
|
||||||
|
<col className="vm-col-md"/>
|
||||||
|
<col/>
|
||||||
|
</colgroup>
|
||||||
|
<tbody>
|
||||||
|
{Object.entries(item.annotations || {}).map(([name, value]) => (
|
||||||
|
<tr key={name}>
|
||||||
|
<td>{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>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Updated at</th>
|
||||||
|
<th>Series returned</th>
|
||||||
|
<th>Series fetched</th>
|
||||||
|
<th>Duration</th>
|
||||||
|
<th>Executed at</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{item.updates.map((update) => (
|
||||||
|
<tr
|
||||||
|
key={update.at}
|
||||||
|
>
|
||||||
|
<td>{dayjs(update.time).format("DD MMM YYYY HH:mm:ss")}</td>
|
||||||
|
<td>{update.samples}</td>
|
||||||
|
<td>{update.series_fetched}</td>
|
||||||
|
<td>{formatDuration(update.duration / 1e9)}</td>
|
||||||
|
<td>{dayjs(update.at).format("DD MMM YYYY HH:mm:ss")}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!!item?.alerts?.length && (
|
||||||
|
<>
|
||||||
|
<span className="title">Alerts</span>
|
||||||
|
<table>
|
||||||
|
<colgroup>
|
||||||
|
<col className="vm-col-sm"/>
|
||||||
|
<col className="vm-col-sm"/>
|
||||||
|
<col className="vm-col-sm"/>
|
||||||
|
<col/>
|
||||||
|
<col className="vm-col-hidden"/>
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Active since</th>
|
||||||
|
<th>State</th>
|
||||||
|
<th>Value</th>
|
||||||
|
<th className="title">Labels</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{item.alerts.map((alert) => (
|
||||||
|
<tr
|
||||||
|
id={`alert-${alert.id}`}
|
||||||
|
key={alert.id}
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
{dayjs(alert.activeAt).format("DD MMM YYYY HH:mm:ss")}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Badges
|
||||||
|
items={{ [alert.state]: { color: alert.state as BadgeColor } }}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<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>
|
||||||
|
<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,64 @@
|
|||||||
|
@use "src/styles/variables" as *;
|
||||||
|
|
||||||
|
.vm-modal.vm-explore-alerts {
|
||||||
|
.vm-modal-content {
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-explore-alerts-rule-item {
|
||||||
|
row-gap: $padding-global;
|
||||||
|
margin: $padding-global;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-col-hidden {
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-button {
|
||||||
|
color: $color-passive;
|
||||||
|
border: 1px solid $color-passive;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-code-example {
|
||||||
|
.vm-button {
|
||||||
|
background-color: $color-code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-col-sm {
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-col-md {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
word-break: break-word;
|
||||||
|
table-layout: fixed;
|
||||||
|
width: 100%;
|
||||||
|
td, th {
|
||||||
|
line-height: 30px;
|
||||||
|
padding: 4px $padding-small;
|
||||||
|
vertical-align: middle;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: left;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
td.align-center {
|
||||||
|
text-align: center
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0 $padding-small;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
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>
|
||||||
|
<div className="vm-explore-alerts-controls">
|
||||||
|
<Badges
|
||||||
|
align="end"
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GroupHeaderHeader;
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-explore-alerts-controls {
|
||||||
|
display: flex;
|
||||||
|
column-gap: $padding-global;
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { FC, useMemo } 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const badgesItems = useMemo(() => {
|
||||||
|
return Object.fromEntries(Object.entries(states || {}).map(([name, value]) => [name, {
|
||||||
|
color: name.toLowerCase().replace(" ", "-") as BadgeColor,
|
||||||
|
value: value == 1 ? 0 : value,
|
||||||
|
}]));
|
||||||
|
}, [states]);
|
||||||
|
|
||||||
|
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>
|
||||||
|
<div className="vm-explore-alerts-controls">
|
||||||
|
<Badges
|
||||||
|
align="end"
|
||||||
|
items={badgesItems}
|
||||||
|
/>
|
||||||
|
{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}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ItemHeader;
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
@use "src/styles/variables" as *;
|
||||||
|
|
||||||
|
.vm-explore-alerts-item-header {
|
||||||
|
display: flex;
|
||||||
|
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 {
|
||||||
|
.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;
|
||||||
|
overflow: hidden;
|
||||||
|
svg {
|
||||||
|
fill: $color-text-disabled;
|
||||||
|
width: 14px;
|
||||||
|
min-width: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__file {
|
||||||
|
color: $color-text-disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
font-size: 85%;
|
||||||
|
background-color: $color-hover-black;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-explore-alerts-controls {
|
||||||
|
display: flex;
|
||||||
|
column-gap: $padding-global;
|
||||||
|
}
|
||||||
@@ -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,38 @@
|
|||||||
|
@use "src/styles/variables" as *;
|
||||||
|
|
||||||
|
.vm-explore-alerts-notifier-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: $padding-global;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: $padding-global;
|
||||||
|
|
||||||
|
&_mobile {
|
||||||
|
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,45 @@
|
|||||||
|
@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;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-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,62 @@
|
|||||||
|
import { FC, useMemo } 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, { BadgeColor } from "../Badges";
|
||||||
|
|
||||||
|
interface TargetProps {
|
||||||
|
target: APITarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Target: FC<TargetProps> = ({ target }) => {
|
||||||
|
const state = target?.lastError ? "unhealthy" : "ok";
|
||||||
|
const targetLabels = target?.labels || {};
|
||||||
|
const badgesItems = useMemo(() => {
|
||||||
|
return Object.fromEntries(Object.entries(targetLabels).map(([name, value]) => [name, {
|
||||||
|
value: value,
|
||||||
|
color: "passive" as BadgeColor,
|
||||||
|
}]));
|
||||||
|
}, [targetLabels]);
|
||||||
|
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>
|
||||||
|
{!!Object.keys(targetLabels).length && (
|
||||||
|
<tr>
|
||||||
|
<td className="vm-col-md">Labels</td>
|
||||||
|
<td>
|
||||||
|
<Badges
|
||||||
|
items={badgesItems}
|
||||||
|
/>
|
||||||
|
</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;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user