Compare commits
63 Commits
configwatc
...
vmui/show-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fe88c2ca5 | ||
|
|
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 |
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)"
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/common"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/remotewrite"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prommetadata"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/opentelemetry/firehose"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/opentelemetry/stream"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
|
||||
@@ -68,7 +68,7 @@ func insertRows(at *auth.Token, tss []prompb.TimeSeries, mms []prompb.MetricMeta
|
||||
ctx.WriteRequest.Timeseries = tssDst
|
||||
|
||||
var metadataTotal int
|
||||
if promscrape.IsMetadataEnabled() {
|
||||
if prommetadata.IsEnabled() {
|
||||
var accountID, projectID uint32
|
||||
if at != nil {
|
||||
accountID = at.AccountID
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/remotewrite"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prommetadata"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus/stream"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
|
||||
@@ -36,7 +36,7 @@ func InsertHandler(at *auth.Token, req *http.Request) error {
|
||||
return err
|
||||
}
|
||||
encoding := req.Header.Get("Content-Encoding")
|
||||
return stream.Parse(req.Body, defaultTimestamp, encoding, true, 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)
|
||||
}, func(s string) {
|
||||
httpserver.LogError(req, s)
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/common"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/remotewrite"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prommetadata"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/promremotewrite/stream"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/tenantmetrics"
|
||||
@@ -71,7 +71,7 @@ func insertRows(at *auth.Token, timeseries []prompb.TimeSeries, mms []prompb.Met
|
||||
ctx.WriteRequest.Timeseries = tssDst
|
||||
|
||||
var metadataTotal int
|
||||
if promscrape.IsMetadataEnabled() {
|
||||
if prommetadata.IsEnabled() {
|
||||
var accountID, projectID uint32
|
||||
if at != nil {
|
||||
accountID = at.AccountID
|
||||
|
||||
@@ -93,10 +93,7 @@ func TestParseRetryAfterHeader(t *testing.T) {
|
||||
|
||||
// helper calculate the max possible time duration calculated by timeutil.AddJitterToDuration.
|
||||
func helper(d time.Duration) time.Duration {
|
||||
dv := d / 10
|
||||
if dv > 10*time.Second {
|
||||
dv = 10 * time.Second
|
||||
}
|
||||
dv := min(d/10, 10*time.Second)
|
||||
|
||||
return d + dv
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/netutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutil"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
)
|
||||
@@ -84,7 +85,8 @@ func UnitTest(files []string, disableGroupLabel bool, externalLabels []string, e
|
||||
defer server.Close()
|
||||
} else {
|
||||
httpListenAddr = httpListenPort
|
||||
ln, err := net.Listen("tcp", fmt.Sprintf(":%s", httpListenPort))
|
||||
|
||||
ln, err := net.Listen(netutil.GetTCPNetwork(), fmt.Sprintf(":%s", httpListenPort))
|
||||
if err != nil {
|
||||
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:]
|
||||
}
|
||||
_, err = notifier.Init(nil, labels, externalURL)
|
||||
_, err = notifier.Init(labels, externalURL)
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to init notifier: %v", err)
|
||||
}
|
||||
|
||||
@@ -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: "1"}},b=[]Label{{Name: "a", Value: "1"}}, return 0
|
||||
func LabelCompare(a, b Labels) int {
|
||||
l := len(a)
|
||||
if len(b) < l {
|
||||
l = len(b)
|
||||
}
|
||||
l := min(len(b), len(a))
|
||||
|
||||
for i := 0; i < l; i++ {
|
||||
if a[i].Name != b[i].Name {
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -83,8 +82,7 @@ absolute path to all .tpl files in root.
|
||||
)
|
||||
|
||||
var (
|
||||
alertURLGeneratorFn notifier.AlertURLGenerator
|
||||
extURL *url.URL
|
||||
extURL *url.URL
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -121,7 +119,7 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
alertURLGeneratorFn, err = getAlertURLGenerator(extURL, *externalAlertSource, *validateTemplates)
|
||||
err = notifier.InitAlertURLGeneratorFn(extURL, *externalAlertSource, *validateTemplates)
|
||||
if err != nil {
|
||||
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:]
|
||||
}
|
||||
|
||||
nts, err := notifier.Init(alertURLGeneratorFn, labels, *externalURL)
|
||||
nts, err := notifier.Init(labels, *externalURL)
|
||||
if err != nil {
|
||||
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))
|
||||
}
|
||||
|
||||
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() {
|
||||
const s = `
|
||||
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) {
|
||||
originalRulePath := *rulePath
|
||||
originalExternalURL := extURL
|
||||
|
||||
@@ -30,7 +30,7 @@ type manager struct {
|
||||
}
|
||||
|
||||
// groupAPI generates apiGroup object from group by its ID(hash)
|
||||
func (m *manager) groupAPI(gID uint64) (*apiGroup, error) {
|
||||
func (m *manager) groupAPI(gID uint64) (*rule.ApiGroup, error) {
|
||||
m.groupsMu.RLock()
|
||||
defer m.groupsMu.RUnlock()
|
||||
|
||||
@@ -38,28 +38,28 @@ func (m *manager) groupAPI(gID uint64) (*apiGroup, error) {
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("can't find group with id %d", gID)
|
||||
}
|
||||
return groupToAPI(g), nil
|
||||
return rule.GroupToAPI(g), nil
|
||||
}
|
||||
|
||||
// ruleAPI generates apiRule object from alert by its ID(hash)
|
||||
func (m *manager) ruleAPI(gID, rID uint64) (apiRule, error) {
|
||||
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 apiRule{}, fmt.Errorf("can't find group with id %d", gID)
|
||||
return rule.ApiRule{}, fmt.Errorf("can't find group with id %d", gID)
|
||||
}
|
||||
for _, rule := range g.Rules {
|
||||
if rule.ID() == rID {
|
||||
return ruleToAPI(rule), nil
|
||||
for _, r := range g.Rules {
|
||||
if r.ID() == rID {
|
||||
return rule.RuleToAPI(r), 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)
|
||||
func (m *manager) alertAPI(gID, aID uint64) (*apiAlert, error) {
|
||||
func (m *manager) alertAPI(gID, aID uint64) (*rule.ApiAlert, error) {
|
||||
m.groupsMu.RLock()
|
||||
defer m.groupsMu.RUnlock()
|
||||
|
||||
@@ -72,7 +72,7 @@ func (m *manager) alertAPI(gID, aID uint64) (*apiAlert, error) {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if apiAlert := alertToAPI(ar, aID); apiAlert != nil {
|
||||
if apiAlert := rule.AlertToAPI(ar, aID); apiAlert != nil {
|
||||
return apiAlert, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestAlertExecTemplate(t *testing.T) {
|
||||
)
|
||||
extLabels["cluster"] = extCluster
|
||||
extLabels["dc"] = extDC
|
||||
_, err := Init(nil, extLabels, extURL)
|
||||
_, err := Init(extLabels, extURL)
|
||||
checkErr(t, err)
|
||||
|
||||
f := func(alert *Alert, annotations map[string]string, tplExpected map[string]string) {
|
||||
|
||||
@@ -4,10 +4,13 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
||||
"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")
|
||||
)
|
||||
|
||||
// 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
|
||||
// configWatcher provides a list of Notifier objects discovered
|
||||
// from static config or via service discovery.
|
||||
@@ -90,7 +129,7 @@ var (
|
||||
// - configuration via file. Supports live reloads and service discovery.
|
||||
//
|
||||
// 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
|
||||
externalLabels = extLabels
|
||||
_, err := url.Parse(externalURL)
|
||||
@@ -117,7 +156,7 @@ func Init(gen AlertURLGenerator, extLabels map[string]string, extURL string) (fu
|
||||
}
|
||||
|
||||
if len(*addrs) > 0 {
|
||||
notifiers, err := notifiersFromFlags(gen)
|
||||
notifiers, err := notifiersFromFlags(AlertURLGeneratorFn)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
cw, err = newWatcher(*configPath, gen)
|
||||
cw, err = newWatcher(*configPath, AlertURLGeneratorFn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to init config watcher: %w", err)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package notifier
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"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"}
|
||||
|
||||
fn, err := Init(nil, nil, "")
|
||||
fn, err := Init(nil, "")
|
||||
if err != nil {
|
||||
t.Fatalf("%s", err)
|
||||
}
|
||||
@@ -52,7 +54,7 @@ func TestInitNegative(t *testing.T) {
|
||||
*configPath = path
|
||||
*addrs = flagutil.ArrayString{addr}
|
||||
*blackHole = bh
|
||||
if _, err := Init(nil, nil, ""); err == nil {
|
||||
if _, err := Init(nil, ""); err == nil {
|
||||
t.Fatalf("expected to get error; got nil instead")
|
||||
}
|
||||
}
|
||||
@@ -69,7 +71,7 @@ func TestBlackHole(t *testing.T) {
|
||||
|
||||
*blackHole = true
|
||||
|
||||
fn, err := Init(nil, nil, "")
|
||||
fn, err := Init(nil, "")
|
||||
if err != nil {
|
||||
t.Fatalf("%s", err)
|
||||
}
|
||||
@@ -91,3 +93,30 @@ func TestBlackHole(t *testing.T) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package rule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
@@ -472,18 +471,6 @@ func (g *Group) UpdateWith(newGroup *Group) {
|
||||
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;
|
||||
// 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 {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package rule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -8,81 +8,26 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/rule"
|
||||
)
|
||||
|
||||
const (
|
||||
// ParamGroupID is group id key in url parameter
|
||||
paramGroupID = "group_id"
|
||||
ParamGroupID = "group_id"
|
||||
// ParamAlertID is alert id key in url parameter
|
||||
paramAlertID = "alert_id"
|
||||
ParamAlertID = "alert_id"
|
||||
// ParamRuleID is rule id key in url parameter
|
||||
paramRuleID = "rule_id"
|
||||
ParamRuleID = "rule_id"
|
||||
|
||||
RuleTypeRecording = "recording"
|
||||
RuleTypeAlerting = "alerting"
|
||||
)
|
||||
|
||||
type apiNotifier struct {
|
||||
Kind string `json:"kind"`
|
||||
Targets []*apiTarget `json:"targets"`
|
||||
}
|
||||
|
||||
type apiTarget struct {
|
||||
Address string `json:"address"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
// LastError contains the error faced while sending to notifier.
|
||||
LastError string `json:"lastError"`
|
||||
}
|
||||
|
||||
// apiAlert represents a notifier.AlertingRule state
|
||||
// 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 {
|
||||
// ApiGroup represents a Group for web view
|
||||
type ApiGroup struct {
|
||||
// Name is the group name as present in the config
|
||||
Name string `json:"name"`
|
||||
// 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 float64 `json:"interval"`
|
||||
// LastEvaluation is the timestamp of the last time the Group was executed
|
||||
@@ -111,27 +56,27 @@ type apiGroup struct {
|
||||
// EvalDelay will adjust the `time` parameter of rule evaluation requests to compensate intentional query delay from datasource.
|
||||
EvalDelay float64 `json:"eval_delay,omitempty"`
|
||||
// Unhealthy unhealthy rules count
|
||||
unhealthy int
|
||||
Unhealthy int
|
||||
// Healthy passing rules count
|
||||
healthy int
|
||||
Healthy int
|
||||
// NoMatch not matching rules count
|
||||
noMatch int
|
||||
NoMatch int
|
||||
}
|
||||
|
||||
// APILink returns a link to the group's JSON representation.
|
||||
func (ag *apiGroup) APILink() string {
|
||||
return fmt.Sprintf("api/v1/group?%s=%s", paramGroupID, ag.ID)
|
||||
func (ag *ApiGroup) APILink() string {
|
||||
return fmt.Sprintf("api/v1/group?%s=%s", ParamGroupID, ag.ID)
|
||||
}
|
||||
|
||||
// groupAlerts represents a group of alerts for WEB view
|
||||
type groupAlerts struct {
|
||||
Group *apiGroup
|
||||
Alerts []*apiAlert
|
||||
// GroupAlerts represents a Group with its Alerts for web view
|
||||
type GroupAlerts struct {
|
||||
Group *ApiGroup
|
||||
Alerts []*ApiAlert
|
||||
}
|
||||
|
||||
// apiRule represents a Rule for web view
|
||||
// ApiRule represents a Rule for web view
|
||||
// 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
|
||||
// "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.
|
||||
@@ -153,7 +98,7 @@ type apiRule struct {
|
||||
// LastEvaluation is the timestamp of the last time the rule was executed
|
||||
LastEvaluation time.Time `json:"lastEvaluation"`
|
||||
// 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.
|
||||
// It MUST be one of "ok", "err", "unknown"
|
||||
Health string `json:"health"`
|
||||
@@ -184,47 +129,86 @@ type apiRule struct {
|
||||
// MaxUpdates is the max number of recorded ruleStateEntry objects
|
||||
MaxUpdates int `json:"max_updates_entries"`
|
||||
// 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
|
||||
type apiRuleWithUpdates struct {
|
||||
apiRule
|
||||
// Updates contains the ordered list of recorded ruleStateEntry objects
|
||||
StateUpdates []rule.StateEntry `json:"updates,omitempty"`
|
||||
}
|
||||
// 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"`
|
||||
|
||||
// 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)
|
||||
// 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 (ar apiRule) WebLink() string {
|
||||
return fmt.Sprintf("rule?%s=%s&%s=%s",
|
||||
paramGroupID, ar.GroupID, paramRuleID, ar.ID)
|
||||
func (aa *ApiAlert) WebLink() string {
|
||||
return fmt.Sprintf("alert?%s=%s&%s=%s",
|
||||
ParamGroupID, aa.GroupID, ParamAlertID, aa.ID)
|
||||
}
|
||||
|
||||
func ruleToAPI(r any) apiRule {
|
||||
if ar, ok := r.(*rule.AlertingRule); ok {
|
||||
// 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",
|
||||
ParamGroupID, ar.GroupID, ParamRuleID, ar.ID)
|
||||
}
|
||||
|
||||
func RuleToAPI(r any) ApiRule {
|
||||
if ar, ok := r.(*AlertingRule); ok {
|
||||
return alertingToAPI(ar)
|
||||
}
|
||||
if rr, ok := r.(*rule.RecordingRule); ok {
|
||||
if rr, ok := r.(*RecordingRule); ok {
|
||||
return recordingToAPI(rr)
|
||||
}
|
||||
return apiRule{}
|
||||
return ApiRule{}
|
||||
}
|
||||
|
||||
const (
|
||||
ruleTypeRecording = "recording"
|
||||
ruleTypeAlerting = "alerting"
|
||||
)
|
||||
|
||||
func recordingToAPI(rr *rule.RecordingRule) apiRule {
|
||||
lastState := rule.GetLastEntry(rr)
|
||||
r := apiRule{
|
||||
Type: ruleTypeRecording,
|
||||
func recordingToAPI(rr *RecordingRule) ApiRule {
|
||||
lastState := GetLastEntry(rr)
|
||||
r := ApiRule{
|
||||
Type: RuleTypeRecording,
|
||||
DatasourceType: rr.Type.String(),
|
||||
Name: rr.Name,
|
||||
Query: rr.Expr,
|
||||
@@ -234,8 +218,8 @@ func recordingToAPI(rr *rule.RecordingRule) apiRule {
|
||||
Health: "ok",
|
||||
LastSamples: lastState.Samples,
|
||||
LastSeriesFetched: lastState.SeriesFetched,
|
||||
MaxUpdates: rule.GetRuleStateSize(rr),
|
||||
Updates: rule.GetAllRuleState(rr),
|
||||
MaxUpdates: GetRuleStateSize(rr),
|
||||
Updates: GetAllRuleState(rr),
|
||||
|
||||
// encode as strings to avoid rounding
|
||||
ID: fmt.Sprintf("%d", rr.ID()),
|
||||
@@ -250,11 +234,11 @@ func recordingToAPI(rr *rule.RecordingRule) apiRule {
|
||||
return r
|
||||
}
|
||||
|
||||
// alertingToAPI returns Rule representation in form of apiRule
|
||||
func alertingToAPI(ar *rule.AlertingRule) apiRule {
|
||||
lastState := rule.GetLastEntry(ar)
|
||||
r := apiRule{
|
||||
Type: ruleTypeAlerting,
|
||||
// alertingToAPI returns Rule representation in form of ApiRule
|
||||
func alertingToAPI(ar *AlertingRule) ApiRule {
|
||||
lastState := GetLastEntry(ar)
|
||||
r := ApiRule{
|
||||
Type: RuleTypeAlerting,
|
||||
DatasourceType: ar.Type.String(),
|
||||
Name: ar.Name,
|
||||
Query: ar.Expr,
|
||||
@@ -266,11 +250,11 @@ func alertingToAPI(ar *rule.AlertingRule) apiRule {
|
||||
EvaluationTime: lastState.Duration.Seconds(),
|
||||
Health: "ok",
|
||||
State: "inactive",
|
||||
Alerts: ruleToAPIAlert(ar),
|
||||
Alerts: RuleToAPIAlert(ar),
|
||||
LastSamples: lastState.Samples,
|
||||
LastSeriesFetched: lastState.SeriesFetched,
|
||||
MaxUpdates: rule.GetRuleStateSize(ar),
|
||||
Updates: rule.GetAllRuleState(ar),
|
||||
MaxUpdates: GetRuleStateSize(ar),
|
||||
Updates: GetAllRuleState(ar),
|
||||
Debug: ar.Debug,
|
||||
|
||||
// encode as strings to avoid rounding in JSON
|
||||
@@ -297,30 +281,30 @@ func alertingToAPI(ar *rule.AlertingRule) apiRule {
|
||||
return r
|
||||
}
|
||||
|
||||
// ruleToAPIAlert generates list of apiAlert objects from existing alerts
|
||||
func ruleToAPIAlert(ar *rule.AlertingRule) []*apiAlert {
|
||||
var alerts []*apiAlert
|
||||
// RuleToAPIAlert generates list of apiAlert objects from existing alerts
|
||||
func RuleToAPIAlert(ar *AlertingRule) []*ApiAlert {
|
||||
var alerts []*ApiAlert
|
||||
for _, a := range ar.GetAlerts() {
|
||||
if a.State == notifier.StateInactive {
|
||||
continue
|
||||
}
|
||||
alerts = append(alerts, newAlertAPI(ar, a))
|
||||
alerts = append(alerts, NewAlertAPI(ar, a))
|
||||
}
|
||||
return alerts
|
||||
}
|
||||
|
||||
// alertToAPI generates apiAlert object from alert by its id(hash)
|
||||
func alertToAPI(ar *rule.AlertingRule, id uint64) *apiAlert {
|
||||
// AlertToAPI generates apiAlert object from alert by its id(hash)
|
||||
func AlertToAPI(ar *AlertingRule, id uint64) *ApiAlert {
|
||||
a := ar.GetAlert(id)
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
return newAlertAPI(ar, a)
|
||||
return NewAlertAPI(ar, a)
|
||||
}
|
||||
|
||||
// NewAlertAPI creates apiAlert for notifier.Alert
|
||||
func newAlertAPI(ar *rule.AlertingRule, a *notifier.Alert) *apiAlert {
|
||||
aa := &apiAlert{
|
||||
func NewAlertAPI(ar *AlertingRule, a *notifier.Alert) *ApiAlert {
|
||||
aa := &ApiAlert{
|
||||
// encode as strings to avoid rounding
|
||||
ID: fmt.Sprintf("%d", a.ID),
|
||||
GroupID: fmt.Sprintf("%d", a.GroupID),
|
||||
@@ -335,8 +319,8 @@ func newAlertAPI(ar *rule.AlertingRule, a *notifier.Alert) *apiAlert {
|
||||
Restored: a.Restored,
|
||||
Value: strconv.FormatFloat(a.Value, 'f', -1, 32),
|
||||
}
|
||||
if alertURLGeneratorFn != nil {
|
||||
aa.SourceLink = alertURLGeneratorFn(*a)
|
||||
if notifier.AlertURLGeneratorFn != nil {
|
||||
aa.SourceLink = notifier.AlertURLGeneratorFn(*a)
|
||||
}
|
||||
if a.State == notifier.StateFiring && !a.KeepFiringSince.IsZero() {
|
||||
aa.Stabilizing = true
|
||||
@@ -344,9 +328,10 @@ func newAlertAPI(ar *rule.AlertingRule, a *notifier.Alert) *apiAlert {
|
||||
return aa
|
||||
}
|
||||
|
||||
func groupToAPI(g *rule.Group) *apiGroup {
|
||||
g = g.DeepCopy()
|
||||
ag := apiGroup{
|
||||
func GroupToAPI(g *Group) *ApiGroup {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
ag := ApiGroup{
|
||||
// encode as string to avoid rounding
|
||||
ID: strconv.FormatUint(g.GetID(), 10),
|
||||
Name: g.Name,
|
||||
@@ -366,9 +351,9 @@ func groupToAPI(g *rule.Group) *apiGroup {
|
||||
if g.EvalDelay != nil {
|
||||
ag.EvalDelay = g.EvalDelay.Seconds()
|
||||
}
|
||||
ag.Rules = make([]apiRule, 0)
|
||||
ag.Rules = make([]ApiRule, 0)
|
||||
for _, r := range g.Rules {
|
||||
ag.Rules = append(ag.Rules, ruleToAPI(r))
|
||||
ag.Rules = append(ag.Rules, RuleToAPI(r))
|
||||
}
|
||||
return &ag
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package rule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/rule"
|
||||
)
|
||||
|
||||
func TestRecordingToApi(t *testing.T) {
|
||||
@@ -17,7 +16,7 @@ func TestRecordingToApi(t *testing.T) {
|
||||
Values: []float64{1}, Timestamps: []int64{0},
|
||||
})
|
||||
entriesLimit := 44
|
||||
g := rule.NewGroup(config.Group{
|
||||
g := NewGroup(config.Group{
|
||||
Name: "group",
|
||||
File: "rules.yaml",
|
||||
Concurrency: 1,
|
||||
@@ -31,21 +30,21 @@ func TestRecordingToApi(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}, fq, 1*time.Minute, nil)
|
||||
rr := g.Rules[0].(*rule.RecordingRule)
|
||||
rr := g.Rules[0].(*RecordingRule)
|
||||
|
||||
expectedRes := apiRule{
|
||||
expectedRes := ApiRule{
|
||||
Name: "record_name",
|
||||
Query: "up",
|
||||
Labels: map[string]string{"label": "value"},
|
||||
Health: "ok",
|
||||
Type: ruleTypeRecording,
|
||||
Type: RuleTypeRecording,
|
||||
DatasourceType: "prometheus",
|
||||
ID: "1248",
|
||||
GroupID: fmt.Sprintf("%d", g.CreateID()),
|
||||
GroupName: "group",
|
||||
File: "rules.yaml",
|
||||
MaxUpdates: 44,
|
||||
Updates: make([]rule.StateEntry, 0),
|
||||
Updates: make([]StateEntry, 0),
|
||||
}
|
||||
|
||||
res := recordingToAPI(rr)
|
||||
@@ -29,9 +29,9 @@ var (
|
||||
{"api/v1/rules", "list all loaded groups and rules"},
|
||||
{"api/v1/alerts", "list all active alerts"},
|
||||
{"api/v1/notifiers", "list all notifiers"},
|
||||
{fmt.Sprintf("api/v1/alert?%s=<int>&%s=<int>", paramGroupID, paramAlertID), "get alert status by group and alert ID"},
|
||||
{fmt.Sprintf("api/v1/rule?%s=<int>&%s=<int>", paramGroupID, paramRuleID), "get rule status by group and rule ID"},
|
||||
{fmt.Sprintf("api/v1/group?%s=<int>", paramGroupID), "get group status by group ID"},
|
||||
{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{
|
||||
{"vmalert/groups", "UI"},
|
||||
@@ -47,8 +47,8 @@ var (
|
||||
{Name: "Docs", URL: "https://docs.victoriametrics.com/victoriametrics/vmalert/"},
|
||||
}
|
||||
ruleTypeMap = map[string]string{
|
||||
"alert": ruleTypeAlerting,
|
||||
"record": ruleTypeRecording,
|
||||
"alert": rule.RuleTypeAlerting,
|
||||
"record": rule.RuleTypeRecording,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -114,7 +114,7 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
|
||||
case "/rules":
|
||||
// Grafana makes an extra request to `/rules`
|
||||
// handler in addition to `/api/v1/rules` calls in alerts UI
|
||||
var data []*apiGroup
|
||||
var data []*rule.ApiGroup
|
||||
rf, err := newRulesFilter(r)
|
||||
if err != nil {
|
||||
httpserver.Errorf(w, r, "%s", err)
|
||||
@@ -180,14 +180,14 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
|
||||
w.Write(data)
|
||||
return true
|
||||
case "/vmalert/api/v1/rule", "/api/v1/rule":
|
||||
rule, err := rh.getRule(r)
|
||||
apiRule, err := rh.getRule(r)
|
||||
if err != nil {
|
||||
httpserver.Errorf(w, r, "%s", err)
|
||||
return true
|
||||
}
|
||||
rwu := apiRuleWithUpdates{
|
||||
apiRule: rule,
|
||||
StateUpdates: rule.Updates,
|
||||
rwu := rule.ApiRuleWithUpdates{
|
||||
ApiRule: apiRule,
|
||||
StateUpdates: apiRule.Updates,
|
||||
}
|
||||
data, err := json.Marshal(rwu)
|
||||
if err != nil {
|
||||
@@ -225,10 +225,10 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func (rh *requestHandler) getGroup(r *http.Request) (*apiGroup, error) {
|
||||
groupID, err := strconv.ParseUint(r.FormValue(paramGroupID), 10, 64)
|
||||
func (rh *requestHandler) getGroup(r *http.Request) (*rule.ApiGroup, error) {
|
||||
groupID, err := strconv.ParseUint(r.FormValue(rule.ParamGroupID), 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read %q param: %w", paramGroupID, err)
|
||||
return nil, fmt.Errorf("failed to read %q param: %w", rule.ParamGroupID, err)
|
||||
}
|
||||
obj, err := rh.m.groupAPI(groupID)
|
||||
if err != nil {
|
||||
@@ -237,30 +237,30 @@ func (rh *requestHandler) getGroup(r *http.Request) (*apiGroup, error) {
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func (rh *requestHandler) getRule(r *http.Request) (apiRule, error) {
|
||||
groupID, err := strconv.ParseUint(r.FormValue(paramGroupID), 10, 64)
|
||||
func (rh *requestHandler) getRule(r *http.Request) (rule.ApiRule, error) {
|
||||
groupID, err := strconv.ParseUint(r.FormValue(rule.ParamGroupID), 10, 64)
|
||||
if err != nil {
|
||||
return apiRule{}, fmt.Errorf("failed to read %q param: %w", paramGroupID, err)
|
||||
return rule.ApiRule{}, fmt.Errorf("failed to read %q param: %w", rule.ParamGroupID, err)
|
||||
}
|
||||
ruleID, err := strconv.ParseUint(r.FormValue(paramRuleID), 10, 64)
|
||||
ruleID, err := strconv.ParseUint(r.FormValue(rule.ParamRuleID), 10, 64)
|
||||
if err != nil {
|
||||
return apiRule{}, fmt.Errorf("failed to read %q param: %w", paramRuleID, 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 apiRule{}, errResponse(err, http.StatusNotFound)
|
||||
return rule.ApiRule{}, errResponse(err, http.StatusNotFound)
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func (rh *requestHandler) getAlert(r *http.Request) (*apiAlert, error) {
|
||||
groupID, err := strconv.ParseUint(r.FormValue(paramGroupID), 10, 64)
|
||||
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", paramGroupID, err)
|
||||
return nil, fmt.Errorf("failed to read %q param: %w", rule.ParamGroupID, err)
|
||||
}
|
||||
alertID, err := strconv.ParseUint(r.FormValue(paramAlertID), 10, 64)
|
||||
alertID, err := strconv.ParseUint(r.FormValue(rule.ParamAlertID), 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read %q param: %w", paramAlertID, err)
|
||||
return nil, fmt.Errorf("failed to read %q param: %w", rule.ParamAlertID, err)
|
||||
}
|
||||
a, err := rh.m.alertAPI(groupID, alertID)
|
||||
if err != nil {
|
||||
@@ -272,7 +272,7 @@ func (rh *requestHandler) getAlert(r *http.Request) (*apiAlert, error) {
|
||||
type listGroupsResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
Groups []*apiGroup `json:"groups"`
|
||||
Groups []*rule.ApiGroup `json:"groups"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
@@ -338,19 +338,19 @@ func (rf *rulesFilter) matchesGroup(group *rule.Group) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (rh *requestHandler) groups(rf *rulesFilter) []*apiGroup {
|
||||
func (rh *requestHandler) groups(rf *rulesFilter) []*rule.ApiGroup {
|
||||
rh.m.groupsMu.RLock()
|
||||
defer rh.m.groupsMu.RUnlock()
|
||||
|
||||
groups := make([]*apiGroup, 0)
|
||||
groups := make([]*rule.ApiGroup, 0)
|
||||
for _, group := range rh.m.groups {
|
||||
if !rf.matchesGroup(group) {
|
||||
continue
|
||||
}
|
||||
g := groupToAPI(group)
|
||||
g := rule.GroupToAPI(group)
|
||||
// the returned list should always be non-nil
|
||||
// https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4221
|
||||
filteredRules := make([]apiRule, 0)
|
||||
filteredRules := make([]rule.ApiRule, 0)
|
||||
for _, rule := range g.Rules {
|
||||
if rf.ruleType != "" && rf.ruleType != rule.Type {
|
||||
continue
|
||||
@@ -365,12 +365,12 @@ func (rh *requestHandler) groups(rf *rulesFilter) []*apiGroup {
|
||||
rule.Alerts = nil
|
||||
}
|
||||
if rule.LastError != "" {
|
||||
g.unhealthy++
|
||||
g.Unhealthy++
|
||||
} else {
|
||||
g.healthy++
|
||||
g.Healthy++
|
||||
}
|
||||
if isNoMatch(rule) {
|
||||
g.noMatch++
|
||||
g.NoMatch++
|
||||
}
|
||||
filteredRules = append(filteredRules, rule)
|
||||
}
|
||||
@@ -378,7 +378,7 @@ func (rh *requestHandler) groups(rf *rulesFilter) []*apiGroup {
|
||||
groups = append(groups, g)
|
||||
}
|
||||
// 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 {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
}
|
||||
@@ -403,32 +403,32 @@ func (rh *requestHandler) listGroups(rf *rulesFilter) ([]byte, error) {
|
||||
type listAlertsResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
Alerts []*apiAlert `json:"alerts"`
|
||||
Alerts []*rule.ApiAlert `json:"alerts"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
func (rh *requestHandler) groupAlerts() []groupAlerts {
|
||||
func (rh *requestHandler) groupAlerts() []rule.GroupAlerts {
|
||||
rh.m.groupsMu.RLock()
|
||||
defer rh.m.groupsMu.RUnlock()
|
||||
|
||||
var gAlerts []groupAlerts
|
||||
var gAlerts []rule.GroupAlerts
|
||||
for _, g := range rh.m.groups {
|
||||
var alerts []*apiAlert
|
||||
var alerts []*rule.ApiAlert
|
||||
for _, r := range g.Rules {
|
||||
a, ok := r.(*rule.AlertingRule)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
alerts = append(alerts, ruleToAPIAlert(a)...)
|
||||
alerts = append(alerts, rule.RuleToAPIAlert(a)...)
|
||||
}
|
||||
if len(alerts) > 0 {
|
||||
gAlerts = append(gAlerts, groupAlerts{
|
||||
Group: groupToAPI(g),
|
||||
gAlerts = append(gAlerts, rule.GroupAlerts{
|
||||
Group: rule.GroupToAPI(g),
|
||||
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 gAlerts
|
||||
@@ -439,7 +439,7 @@ func (rh *requestHandler) listAlerts(rf *rulesFilter) ([]byte, error) {
|
||||
defer rh.m.groupsMu.RUnlock()
|
||||
|
||||
lr := listAlertsResponse{Status: "success"}
|
||||
lr.Data.Alerts = make([]*apiAlert, 0)
|
||||
lr.Data.Alerts = make([]*rule.ApiAlert, 0)
|
||||
for _, group := range rh.m.groups {
|
||||
if !rf.matchesGroup(group) {
|
||||
continue
|
||||
@@ -449,12 +449,12 @@ func (rh *requestHandler) listAlerts(rf *rulesFilter) ([]byte, error) {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
lr.Data.Alerts = append(lr.Data.Alerts, ruleToAPIAlert(a)...)
|
||||
lr.Data.Alerts = append(lr.Data.Alerts, rule.RuleToAPIAlert(a)...)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
})
|
||||
|
||||
@@ -471,7 +471,7 @@ func (rh *requestHandler) listAlerts(rf *rulesFilter) ([]byte, error) {
|
||||
type listNotifiersResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
Notifiers []*apiNotifier `json:"notifiers"`
|
||||
Notifiers []*notifier.ApiNotifier `json:"notifiers"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
@@ -479,20 +479,20 @@ func (rh *requestHandler) listNotifiers() ([]byte, error) {
|
||||
targets := notifier.GetTargets()
|
||||
|
||||
lr := listNotifiersResponse{Status: "success"}
|
||||
lr.Data.Notifiers = make([]*apiNotifier, 0)
|
||||
lr.Data.Notifiers = make([]*notifier.ApiNotifier, 0)
|
||||
for protoName, protoTargets := range targets {
|
||||
notifier := &apiNotifier{
|
||||
Kind: string(protoName),
|
||||
Targets: make([]*apiTarget, 0, len(protoTargets)),
|
||||
nr := ¬ifier.ApiNotifier{
|
||||
Kind: protoName,
|
||||
Targets: make([]*notifier.ApiTarget, 0, len(protoTargets)),
|
||||
}
|
||||
for _, target := range protoTargets {
|
||||
notifier.Targets = append(notifier.Targets, &apiTarget{
|
||||
nr.Targets = append(nr.Targets, ¬ifier.ApiTarget{
|
||||
Address: target.Addr(),
|
||||
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)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/tpl"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/vmalertutil"
|
||||
"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) %}
|
||||
@@ -93,7 +94,7 @@
|
||||
{%= tpl.Footer(r) %}
|
||||
{% endfunc %}
|
||||
|
||||
{% func ListGroups(r *http.Request, groups []*apiGroup, filter string) %}
|
||||
{% func ListGroups(r *http.Request, groups []*rule.ApiGroup, filter string) %}
|
||||
{%code
|
||||
prefix := vmalertutil.Prefix(r.URL.Path)
|
||||
filters := map[string]string{
|
||||
@@ -113,7 +114,7 @@
|
||||
{%= Controls(prefix, currentIcon, currentText, icons, filters, true) %}
|
||||
{% if len(groups) > 0 %}
|
||||
{% for _, g := range groups %}
|
||||
<div id="group-{%s g.ID %}" class="d-flex w-100 border-0 flex-column group-items{% if g.unhealthy > 0 %} alert-danger{% endif %}">
|
||||
<div id="group-{%s g.ID %}" class="d-flex w-100 border-0 flex-column group-items{% if g.Unhealthy > 0 %} alert-danger{% endif %}">
|
||||
<span class="d-flex justify-content-between">
|
||||
<a href="#group-{%s g.ID %}">{%s g.Name %}{% if g.Type != "prometheus" %} ({%s g.Type %}){% endif %} (every {%f.0 g.Interval %}s) #</a>
|
||||
<span
|
||||
@@ -123,9 +124,9 @@
|
||||
data-bs-target="#sub-{%s g.ID %}"
|
||||
>
|
||||
<span class="d-flex gap-2">
|
||||
{% if g.unhealthy > 0 %}<span class="badge bg-danger" title="Number of rules with status Error">{%d g.unhealthy %}</span> {% endif %}
|
||||
{% if g.noMatch > 0 %}<span class="badge bg-warning" title="Number of rules with status NoMatch">{%d g.noMatch %}</span> {% endif %}
|
||||
<span class="badge bg-success" title="Number of rules with status Ok">{%d g.healthy %}</span>
|
||||
{% if g.Unhealthy > 0 %}<span class="badge bg-danger" title="Number of rules with status Error">{%d g.Unhealthy %}</span> {% endif %}
|
||||
{% if g.NoMatch > 0 %}<span class="badge bg-warning" title="Number of rules with status NoMatch">{%d g.NoMatch %}</span> {% endif %}
|
||||
<span class="badge bg-success" title="Number of rules with status Ok">{%d g.Healthy %}</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
@@ -222,7 +223,7 @@
|
||||
{% endfunc %}
|
||||
|
||||
|
||||
{% func ListAlerts(r *http.Request, groupAlerts []groupAlerts) %}
|
||||
{% func ListAlerts(r *http.Request, groupAlerts []rule.GroupAlerts) %}
|
||||
{%code prefix := vmalertutil.Prefix(r.URL.Path) %}
|
||||
{%= tpl.Header(r, navItems, "Alerts", getLastConfigError()) %}
|
||||
{%= Controls(prefix, "", "", nil, nil, true) %}
|
||||
@@ -231,7 +232,7 @@
|
||||
{%code
|
||||
g := ga.Group
|
||||
var keys []string
|
||||
alertsByRule := make(map[string][]*apiAlert)
|
||||
alertsByRule := make(map[string][]*rule.ApiAlert)
|
||||
for _, alert := range ga.Alerts {
|
||||
if len(alertsByRule[alert.RuleID]) < 1 {
|
||||
keys = append(keys, alert.RuleID)
|
||||
@@ -378,7 +379,7 @@
|
||||
{%= tpl.Footer(r) %}
|
||||
{% endfunc %}
|
||||
|
||||
{% func Alert(r *http.Request, alert *apiAlert) %}
|
||||
{% func Alert(r *http.Request, alert *rule.ApiAlert) %}
|
||||
{%code prefix := vmalertutil.Prefix(r.URL.Path) %}
|
||||
{%= tpl.Header(r, navItems, "", getLastConfigError()) %}
|
||||
{%code
|
||||
@@ -464,7 +465,7 @@
|
||||
{% endfunc %}
|
||||
|
||||
|
||||
{% func RuleDetails(r *http.Request, rule apiRule) %}
|
||||
{% func RuleDetails(r *http.Request, rule rule.ApiRule) %}
|
||||
{%code prefix := vmalertutil.Prefix(r.URL.Path) %}
|
||||
{%= tpl.Header(r, navItems, "", getLastConfigError()) %}
|
||||
{%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>
|
||||
{% endfunc %}
|
||||
|
||||
{% func seriesFetchedWarn(prefix string, r apiRule) %}
|
||||
{% func seriesFetchedWarn(prefix string, r rule.ApiRule) %}
|
||||
{% if isNoMatch(r) %}
|
||||
<svg
|
||||
data-bs-toggle="tooltip"
|
||||
@@ -663,7 +664,7 @@
|
||||
{% endfunc %}
|
||||
|
||||
{%code
|
||||
func isNoMatch (r apiRule) bool {
|
||||
func isNoMatch (r rule.ApiRule) bool {
|
||||
return r.LastSamples == 0 && r.LastSeriesFetched != nil && *r.LastSeriesFetched == 0
|
||||
}
|
||||
%}
|
||||
|
||||
@@ -85,22 +85,22 @@ func TestHandler(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("/vmalert/rule", func(t *testing.T) {
|
||||
a := ruleToAPI(ar)
|
||||
a := rule.RuleToAPI(ar)
|
||||
getResp(t, ts.URL+"/vmalert/"+a.WebLink(), nil, 200)
|
||||
r := ruleToAPI(rr)
|
||||
r := rule.RuleToAPI(rr)
|
||||
getResp(t, ts.URL+"/vmalert/"+r.WebLink(), nil, 200)
|
||||
})
|
||||
t.Run("/vmalert/alert", func(t *testing.T) {
|
||||
alerts := ruleToAPIAlert(ar)
|
||||
alerts := rule.RuleToAPIAlert(ar)
|
||||
for _, a := range alerts {
|
||||
getResp(t, ts.URL+"/vmalert/"+a.WebLink(), nil, 200)
|
||||
}
|
||||
})
|
||||
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)
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
@@ -127,14 +127,14 @@ func TestHandler(t *testing.T) {
|
||||
}
|
||||
})
|
||||
t.Run("/api/v1/alert?alertID&groupID", func(t *testing.T) {
|
||||
expAlert := newAlertAPI(ar, ar.GetAlerts()[0])
|
||||
alert := &apiAlert{}
|
||||
expAlert := rule.NewAlertAPI(ar, ar.GetAlerts()[0])
|
||||
alert := &rule.ApiAlert{}
|
||||
getResp(t, ts.URL+"/"+expAlert.APILink(), alert, 200)
|
||||
if !reflect.DeepEqual(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)
|
||||
if !reflect.DeepEqual(alert, expAlert) {
|
||||
t.Fatalf("expected %v is equal to %v", alert, expAlert)
|
||||
@@ -142,16 +142,16 @@ func TestHandler(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+"/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+"/vmalert/api/v1/alert"+params, nil, 404)
|
||||
|
||||
// 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+"/vmalert/api/v1/alert"+params, nil, 400)
|
||||
})
|
||||
@@ -170,22 +170,22 @@ func TestHandler(t *testing.T) {
|
||||
}
|
||||
})
|
||||
t.Run("/api/v1/rule?ruleID&groupID", func(t *testing.T) {
|
||||
expRule := ruleToAPI(ar)
|
||||
gotRule := apiRule{}
|
||||
expRule := rule.RuleToAPI(ar)
|
||||
gotRule := rule.ApiRule{}
|
||||
getResp(t, ts.URL+"/"+expRule.APILink(), &gotRule, 200)
|
||||
|
||||
if 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)
|
||||
|
||||
if 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)
|
||||
if len(gotRuleWithUpdates.StateUpdates) < 1 {
|
||||
t.Fatalf("expected %+v to have state updates field not empty", gotRuleWithUpdates.StateUpdates)
|
||||
@@ -194,13 +194,13 @@ func TestHandler(t *testing.T) {
|
||||
t.Run("/api/v1/group?groupID", func(t *testing.T) {
|
||||
id := groupIDs[0]
|
||||
g := m.groups[id]
|
||||
expGroup := groupToAPI(g)
|
||||
gotGroup := apiGroup{}
|
||||
expGroup := rule.GroupToAPI(g)
|
||||
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 = apiGroup{}
|
||||
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)
|
||||
|
||||
@@ -41,6 +41,9 @@ var (
|
||||
"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. "+
|
||||
"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. "+
|
||||
"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. "+
|
||||
@@ -75,6 +78,7 @@ type UserInfo struct {
|
||||
DefaultURL *URLPrefix `yaml:"default_url,omitempty"`
|
||||
RetryStatusCodes []int `yaml:"retry_status_codes,omitempty"`
|
||||
LoadBalancingPolicy string `yaml:"load_balancing_policy,omitempty"`
|
||||
MergeQueryArgs []string `yaml:"merge_query_args,omitempty"`
|
||||
DropSrcPathPrefixParts *int `yaml:"drop_src_path_prefix_parts,omitempty"`
|
||||
TLSCAFile string `yaml:"tls_ca_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 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 *int `yaml:"drop_src_path_prefix_parts,omitempty"`
|
||||
}
|
||||
@@ -228,7 +237,7 @@ func (qa *QueryArg) MarshalYAML() (any, error) {
|
||||
return qa.sOriginal, nil
|
||||
}
|
||||
|
||||
// URLPrefix represents passed `url_prefix`
|
||||
// URLPrefix represents the `url_prefix` from auth config.
|
||||
type URLPrefix struct {
|
||||
// requests are re-tried on other backend urls for these http response status codes
|
||||
retryStatusCodes []int
|
||||
@@ -236,6 +245,11 @@ type URLPrefix struct {
|
||||
// load balancing policy used
|
||||
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
|
||||
dropSrcPathPrefixParts int
|
||||
|
||||
@@ -856,6 +870,7 @@ func (ui *UserInfo) getMetricLabels() (string, error) {
|
||||
func (ui *UserInfo) initURLs() error {
|
||||
retryStatusCodes := defaultRetryStatusCodes.Values()
|
||||
loadBalancingPolicy := *defaultLoadBalancingPolicy
|
||||
mergeQueryArgs := *defaultMergeQueryArgs
|
||||
dropSrcPathPrefixParts := 0
|
||||
discoverBackendIPs := *discoverBackendIPsGlobal
|
||||
if ui.RetryStatusCodes != nil {
|
||||
@@ -864,6 +879,9 @@ func (ui *UserInfo) initURLs() error {
|
||||
if ui.LoadBalancingPolicy != "" {
|
||||
loadBalancingPolicy = ui.LoadBalancingPolicy
|
||||
}
|
||||
if len(ui.MergeQueryArgs) != 0 {
|
||||
mergeQueryArgs = ui.MergeQueryArgs
|
||||
}
|
||||
if ui.DropSrcPathPrefixParts != nil {
|
||||
dropSrcPathPrefixParts = *ui.DropSrcPathPrefixParts
|
||||
}
|
||||
@@ -871,16 +889,18 @@ func (ui *UserInfo) initURLs() error {
|
||||
discoverBackendIPs = *ui.DiscoverBackendIPs
|
||||
}
|
||||
|
||||
if ui.URLPrefix != nil {
|
||||
if err := ui.URLPrefix.sanitizeAndInitialize(); err != nil {
|
||||
up := ui.URLPrefix
|
||||
if up != nil {
|
||||
if err := up.sanitizeAndInitialize(); err != nil {
|
||||
return err
|
||||
}
|
||||
ui.URLPrefix.retryStatusCodes = retryStatusCodes
|
||||
ui.URLPrefix.dropSrcPathPrefixParts = dropSrcPathPrefixParts
|
||||
ui.URLPrefix.discoverBackendIPs = discoverBackendIPs
|
||||
if err := ui.URLPrefix.setLoadBalancingPolicy(loadBalancingPolicy); err != nil {
|
||||
up.retryStatusCodes = retryStatusCodes
|
||||
up.dropSrcPathPrefixParts = dropSrcPathPrefixParts
|
||||
up.discoverBackendIPs = discoverBackendIPs
|
||||
if err := up.setLoadBalancingPolicy(loadBalancingPolicy); err != nil {
|
||||
return err
|
||||
}
|
||||
up.mergeQueryArgs = mergeQueryArgs
|
||||
}
|
||||
if ui.DefaultURL != nil {
|
||||
if err := ui.DefaultURL.sanitizeAndInitialize(); err != nil {
|
||||
@@ -899,6 +919,7 @@ func (ui *UserInfo) initURLs() error {
|
||||
}
|
||||
rscs := retryStatusCodes
|
||||
lbp := loadBalancingPolicy
|
||||
mqa := mergeQueryArgs
|
||||
dsp := dropSrcPathPrefixParts
|
||||
dbd := discoverBackendIPs
|
||||
if e.RetryStatusCodes != nil {
|
||||
@@ -907,6 +928,9 @@ func (ui *UserInfo) initURLs() error {
|
||||
if e.LoadBalancingPolicy != "" {
|
||||
lbp = e.LoadBalancingPolicy
|
||||
}
|
||||
if len(e.MergeQueryArgs) != 0 {
|
||||
mqa = e.MergeQueryArgs
|
||||
}
|
||||
if e.DropSrcPathPrefixParts != nil {
|
||||
dsp = *e.DropSrcPathPrefixParts
|
||||
}
|
||||
@@ -917,6 +941,7 @@ func (ui *UserInfo) initURLs() error {
|
||||
if err := e.URLPrefix.setLoadBalancingPolicy(lbp); err != nil {
|
||||
return err
|
||||
}
|
||||
e.URLPrefix.mergeQueryArgs = mqa
|
||||
e.URLPrefix.dropSrcPathPrefixParts = dsp
|
||||
e.URLPrefix.discoverBackendIPs = dbd
|
||||
}
|
||||
|
||||
@@ -280,7 +280,7 @@ users:
|
||||
}
|
||||
|
||||
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()
|
||||
ac, err := parseAuthConfig([]byte(s))
|
||||
if err != nil {
|
||||
@@ -294,15 +294,19 @@ func TestParseAuthConfigSuccess(t *testing.T) {
|
||||
if err := areEqualConfigs(m, expectedAuthConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := areEqualConfigs(ac.UnauthorizedUser, expectedUnauthorizedUserConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
insecureSkipVerifyTrue := true
|
||||
|
||||
// Empty config
|
||||
f(``, map[string]*UserInfo{})
|
||||
f(``, map[string]*UserInfo{}, nil)
|
||||
|
||||
// Empty users
|
||||
f(`users: []`, map[string]*UserInfo{})
|
||||
f(`users: []`, map[string]*UserInfo{}, nil)
|
||||
|
||||
// Single user
|
||||
f(`
|
||||
@@ -320,7 +324,7 @@ users:
|
||||
MaxConcurrentRequests: 5,
|
||||
TLSInsecureSkipVerify: &insecureSkipVerifyTrue,
|
||||
},
|
||||
})
|
||||
}, nil)
|
||||
|
||||
// Single user with auth_token
|
||||
f(`
|
||||
@@ -344,7 +348,7 @@ users:
|
||||
TLSCertFile: "foo/baz",
|
||||
TLSKeyFile: "foo/foo",
|
||||
},
|
||||
})
|
||||
}, nil)
|
||||
|
||||
// Multiple url_prefix entries
|
||||
insecureSkipVerifyFalse := false
|
||||
@@ -359,6 +363,7 @@ users:
|
||||
tls_insecure_skip_verify: false
|
||||
retry_status_codes: [500, 501]
|
||||
load_balancing_policy: first_available
|
||||
merge_query_args: [foo, bar]
|
||||
drop_src_path_prefix_parts: 1
|
||||
discover_backend_ips: true
|
||||
`, map[string]*UserInfo{
|
||||
@@ -372,10 +377,11 @@ users:
|
||||
TLSInsecureSkipVerify: &insecureSkipVerifyFalse,
|
||||
RetryStatusCodes: []int{500, 501},
|
||||
LoadBalancingPolicy: "first_available",
|
||||
MergeQueryArgs: []string{"foo", "bar"},
|
||||
DropSrcPathPrefixParts: intp(1),
|
||||
DiscoverBackendIPs: &discoverBackendIPsTrue,
|
||||
},
|
||||
})
|
||||
}, nil)
|
||||
|
||||
// Multiple users
|
||||
f(`
|
||||
@@ -393,7 +399,7 @@ users:
|
||||
Username: "bar",
|
||||
URLPrefix: mustParseURL("https://bar/x/"),
|
||||
},
|
||||
})
|
||||
}, nil)
|
||||
|
||||
// non-empty URLMap
|
||||
sharedUserInfo := &UserInfo{
|
||||
@@ -443,7 +449,7 @@ users:
|
||||
`, map[string]*UserInfo{
|
||||
getHTTPAuthBearerToken("foo"): sharedUserInfo,
|
||||
getHTTPAuthBasicToken("foo", ""): sharedUserInfo,
|
||||
})
|
||||
}, nil)
|
||||
|
||||
// Multiple users with the same name - this should work, since these users have different passwords
|
||||
f(`
|
||||
@@ -465,7 +471,7 @@ users:
|
||||
Password: "bar",
|
||||
URLPrefix: mustParseURL("https://bar/x"),
|
||||
},
|
||||
})
|
||||
}, nil)
|
||||
|
||||
// with default url
|
||||
keepOriginalHost := true
|
||||
@@ -481,6 +487,8 @@ users:
|
||||
- "foo: bar"
|
||||
- "xxx: y"
|
||||
keep_original_host: true
|
||||
load_balancing_policy: first_available
|
||||
merge_query_args: [foo, bar]
|
||||
default_url:
|
||||
- http://default1/select/0/prometheus
|
||||
- http://default2/select/0/prometheus
|
||||
@@ -505,6 +513,8 @@ users:
|
||||
},
|
||||
KeepOriginalHost: &keepOriginalHost,
|
||||
},
|
||||
LoadBalancingPolicy: "first_available",
|
||||
MergeQueryArgs: []string{"foo", "bar"},
|
||||
},
|
||||
},
|
||||
DefaultURL: mustParseURLs([]string{
|
||||
@@ -532,6 +542,8 @@ users:
|
||||
},
|
||||
KeepOriginalHost: &keepOriginalHost,
|
||||
},
|
||||
LoadBalancingPolicy: "first_available",
|
||||
MergeQueryArgs: []string{"foo", "bar"},
|
||||
},
|
||||
},
|
||||
DefaultURL: mustParseURLs([]string{
|
||||
@@ -539,7 +551,7 @@ users:
|
||||
"http://default2/select/0/prometheus",
|
||||
}),
|
||||
},
|
||||
})
|
||||
}, nil)
|
||||
|
||||
// With metric_labels
|
||||
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)
|
||||
if err != nil {
|
||||
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())
|
||||
targetURL.RawQuery = query.Encode()
|
||||
} else { // Update path for regular routes.
|
||||
targetURL = mergeURLs(targetURL, u, up.dropSrcPathPrefixParts)
|
||||
targetURL = mergeURLs(targetURL, u, up.dropSrcPathPrefixParts, up.mergeQueryArgs)
|
||||
}
|
||||
|
||||
wasLocalRetry := false
|
||||
|
||||
@@ -8,29 +8,42 @@ import (
|
||||
"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
|
||||
|
||||
srcPath := dropPrefixParts(requestURI.Path, dropSrcPathPrefixParts)
|
||||
if strings.HasPrefix(srcPath, "/") {
|
||||
targetURL.Path = strings.TrimSuffix(targetURL.Path, "/")
|
||||
}
|
||||
targetURL.Path += srcPath
|
||||
requestParams := requestURI.Query()
|
||||
// fast path
|
||||
|
||||
if len(requestParams) == 0 {
|
||||
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 {
|
||||
// skip clashed query params from original request
|
||||
if exist := uiParams.Get(k); len(exist) > 0 {
|
||||
if targetParams.Has(k) && !slices.Contains(mergeQueryArgs, k) {
|
||||
// Skip clashed client query params for security reasons
|
||||
continue
|
||||
}
|
||||
for i := range v {
|
||||
uiParams.Add(k, v[i])
|
||||
}
|
||||
}
|
||||
|
||||
targetURL.RawQuery = uiParams.Encode()
|
||||
return &targetURL
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ func TestCreateTargetURLSuccess(t *testing.T) {
|
||||
return
|
||||
}
|
||||
bu := up.getBackendURL()
|
||||
target := mergeURLs(bu.url, u, up.dropSrcPathPrefixParts)
|
||||
target := mergeURLs(bu.url, u, up.dropSrcPathPrefixParts, up.mergeQueryArgs)
|
||||
bu.put()
|
||||
|
||||
gotTarget := target.String()
|
||||
@@ -352,7 +352,7 @@ func TestUserInfoGetBackendURL_SRV(t *testing.T) {
|
||||
return
|
||||
}
|
||||
bu := up.getBackendURL()
|
||||
target := mergeURLs(bu.url, u, up.dropSrcPathPrefixParts)
|
||||
target := mergeURLs(bu.url, u, up.dropSrcPathPrefixParts, up.mergeQueryArgs)
|
||||
bu.put()
|
||||
|
||||
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) {
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
// 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.
|
||||
batchSize := 10000
|
||||
if batchSize > len(timestamps) {
|
||||
batchSize = len(timestamps)
|
||||
}
|
||||
batchSize := min(10000, len(timestamps))
|
||||
timestampsBatch := timestamps[:batchSize]
|
||||
valuesBatch := values[:batchSize]
|
||||
timestamps = timestamps[batchSize:]
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/common"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/relabel"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prommetadata"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus/stream"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
|
||||
@@ -30,7 +30,7 @@ func InsertHandler(req *http.Request) error {
|
||||
return err
|
||||
}
|
||||
encoding := req.Header.Get("Content-Encoding")
|
||||
return stream.Parse(req.Body, defaultTimestamp, encoding, true, 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)
|
||||
}, func(s string) {
|
||||
httpserver.LogError(req, s)
|
||||
|
||||
@@ -142,6 +142,12 @@ func (s *series) summarize(aggrFunc aggrFunc, startTime, endTime, step int64, xF
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse %q: %w", query, err)
|
||||
|
||||
@@ -4070,6 +4070,9 @@ func TestExecExprFailure(t *testing.T) {
|
||||
|
||||
f(`holtWintersConfidenceArea(group(time("foo.baz",15),time("foo.baz",15)))`)
|
||||
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 {
|
||||
|
||||
@@ -1218,10 +1218,7 @@ func transformDelay(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, er
|
||||
values := s.Values
|
||||
stepsLocal := steps
|
||||
if stepsLocal < 0 {
|
||||
stepsLocal = -stepsLocal
|
||||
if stepsLocal > len(values) {
|
||||
stepsLocal = len(values)
|
||||
}
|
||||
stepsLocal = min(-stepsLocal, len(values))
|
||||
copy(values, values[stepsLocal:])
|
||||
for i := len(values) - 1; i >= len(values)-stepsLocal; i-- {
|
||||
values[i] = nan
|
||||
@@ -4663,20 +4660,14 @@ func transformSubstr(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, e
|
||||
if start > len(splitName) {
|
||||
start = len(splitName)
|
||||
} else if start < 0 {
|
||||
start = len(splitName) + start
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
start = max(len(splitName)+start, 0)
|
||||
}
|
||||
if stop == 0 {
|
||||
stop = len(splitName)
|
||||
} else if stop > len(splitName) {
|
||||
stop = len(splitName)
|
||||
} else if stop < 0 {
|
||||
stop = len(splitName) + stop
|
||||
if stop < 0 {
|
||||
stop = 0
|
||||
}
|
||||
stop = max(len(splitName)+stop, 0)
|
||||
}
|
||||
if stop < start {
|
||||
stop = start
|
||||
|
||||
@@ -2,6 +2,7 @@ package vmselect
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -48,13 +49,10 @@ var (
|
||||
var slowQueries = metrics.NewCounter(`vm_slow_queries_total`)
|
||||
|
||||
func getDefaultMaxConcurrentRequests() int {
|
||||
n := cgroup.AvailableCPUs() * 2
|
||||
if n > 16 {
|
||||
// A single request can saturate all the CPU cores, so there is no sense
|
||||
// in allowing higher number of concurrent requests - they will just contend
|
||||
// for unavailable CPU time.
|
||||
n = 16
|
||||
}
|
||||
// A single request can saturate all the CPU cores, so there is no sense
|
||||
// in allowing higher number of concurrent requests - they will just contend
|
||||
// for unavailable CPU time.
|
||||
n := min(cgroup.AvailableCPUs()*2, 16)
|
||||
return n
|
||||
}
|
||||
|
||||
@@ -67,6 +65,7 @@ func Init() {
|
||||
prometheus.InitMaxUniqueTimeseries(*maxConcurrentRequests)
|
||||
|
||||
concurrencyLimitCh = make(chan struct{}, *maxConcurrentRequests)
|
||||
initVMUIConfig()
|
||||
initVMAlertProxy()
|
||||
}
|
||||
|
||||
@@ -128,10 +127,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
default:
|
||||
// Sleep for a while until giving up. This should resolve short bursts in requests.
|
||||
concurrencyLimitReached.Inc()
|
||||
d := searchutil.GetMaxQueryDuration(r)
|
||||
if d > *maxQueueDuration {
|
||||
d = *maxQueueDuration
|
||||
}
|
||||
d := min(searchutil.GetMaxQueryDuration(r), *maxQueueDuration)
|
||||
t := timerpool.Get(d)
|
||||
select {
|
||||
case concurrencyLimitCh <- struct{}{}:
|
||||
@@ -460,6 +456,11 @@ func handleStaticAndSimpleRequests(w http.ResponseWriter, r *http.Request, path
|
||||
return true
|
||||
}
|
||||
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/") {
|
||||
// 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.
|
||||
@@ -734,8 +735,34 @@ func proxyVMAlertRequests(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
vmalertProxyHost string
|
||||
vmalertProxy *nethttputil.ReverseProxy
|
||||
vmuiConfig string
|
||||
)
|
||||
|
||||
func initVMUIConfig() {
|
||||
var cfg struct {
|
||||
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.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.
|
||||
func initVMAlertProxy() {
|
||||
if len(*vmalertProxyURL) == 0 {
|
||||
|
||||
@@ -203,10 +203,7 @@ var defaultMaxWorkersPerQuery = func() int {
|
||||
// for processing an average query, without significant impact on inter-CPU communications.
|
||||
const maxWorkersLimit = 32
|
||||
|
||||
n := gomaxprocs
|
||||
if n > maxWorkersLimit {
|
||||
n = maxWorkersLimit
|
||||
}
|
||||
n := min(gomaxprocs, maxWorkersLimit)
|
||||
return n
|
||||
}()
|
||||
|
||||
@@ -279,10 +276,7 @@ func (rss *Results) runParallel(qt *querytracer.Tracer, f func(rs *Result, worke
|
||||
}
|
||||
|
||||
// Prepare worker channels.
|
||||
workers := len(tsws)
|
||||
if workers > maxWorkers {
|
||||
workers = maxWorkers
|
||||
}
|
||||
workers := min(len(tsws), maxWorkers)
|
||||
itemsPerWorker := (len(tsws) + workers - 1) / workers
|
||||
workChs := make([]chan *timeseriesWork, workers)
|
||||
for i := range workChs {
|
||||
@@ -497,10 +491,7 @@ func (pts *packedTimeseries) unpackTo(dst []*sortBlock, tbf *tmpBlocksFile, tr s
|
||||
}
|
||||
|
||||
// Prepare worker channels.
|
||||
workers := len(upws)
|
||||
if workers > gomaxprocs {
|
||||
workers = gomaxprocs
|
||||
}
|
||||
workers := min(len(upws), gomaxprocs)
|
||||
if 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.
|
||||
// It should reduce pressure on Go GC by reducing the number of string allocations
|
||||
// when constructing metricName string from byte slice.
|
||||
metricNamesBufCap := maxSeriesCount * 100
|
||||
if metricNamesBufCap > maxFastAllocBlockSize {
|
||||
metricNamesBufCap = maxFastAllocBlockSize
|
||||
}
|
||||
metricNamesBufCap := min(maxSeriesCount*100, maxFastAllocBlockSize)
|
||||
metricNamesBuf := make([]byte, 0, metricNamesBufCap)
|
||||
|
||||
// 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.
|
||||
// It should reduce pressure on Go GC by reducing the number of allocations for blockRefs.brs slices.
|
||||
brsPoolCap := uintptr(maxSeriesCount)
|
||||
if brsPoolCap > maxFastAllocBlockSize/unsafe.Sizeof(blockRef{}) {
|
||||
brsPoolCap = maxFastAllocBlockSize / unsafe.Sizeof(blockRef{})
|
||||
}
|
||||
brsPoolCap := min(uintptr(maxSeriesCount), maxFastAllocBlockSize/unsafe.Sizeof(blockRef{}))
|
||||
brsPool := make([]blockRef, 0, brsPoolCap)
|
||||
|
||||
// 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/encoding"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputil"
|
||||
"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. "+
|
||||
"It can be overridden on per-query basis via latency_offset arg. "+
|
||||
"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. "+
|
||||
"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")
|
||||
@@ -733,8 +731,9 @@ func QueryHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWr
|
||||
step = defaultStep
|
||||
}
|
||||
|
||||
if len(query) > maxQueryLen.IntN() {
|
||||
return fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(query), maxQueryLen.N)
|
||||
maxLen := searchutil.GetMaxQueryLen()
|
||||
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)
|
||||
if err != nil {
|
||||
@@ -904,8 +903,9 @@ func queryRangeHandler(qt *querytracer.Tracer, startTime time.Time, w http.Respo
|
||||
}
|
||||
|
||||
// Validate input args.
|
||||
if len(query) > maxQueryLen.IntN() {
|
||||
return fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(query), maxQueryLen.N)
|
||||
maxLen := searchutil.GetMaxQueryLen()
|
||||
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 {
|
||||
end = start + defaultStep
|
||||
@@ -1089,12 +1089,9 @@ func getRoundDigits(r *http.Request) int {
|
||||
}
|
||||
|
||||
func getLatencyOffsetMilliseconds(r *http.Request) (int64, error) {
|
||||
d := latencyOffset.Milliseconds()
|
||||
if d < 0 {
|
||||
// Zero latency offset may be useful for some use cases.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2061#issuecomment-1299109836
|
||||
d = 0
|
||||
}
|
||||
// Zero latency offset may be useful for some use cases.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2061#issuecomment-1299109836
|
||||
d := max(latencyOffset.Milliseconds(), 0)
|
||||
return httputil.GetDuration(r, "latency_offset", d)
|
||||
}
|
||||
|
||||
|
||||
@@ -161,11 +161,8 @@ func aggrFuncAny(afa *aggrFuncArg) ([]*timeseries, error) {
|
||||
afe := func(tss []*timeseries, _ *metricsql.ModifierExpr) []*timeseries {
|
||||
return tss[:1]
|
||||
}
|
||||
limit := afa.ae.Limit
|
||||
if limit > 1 {
|
||||
// Only a single time series per group must be returned
|
||||
limit = 1
|
||||
}
|
||||
// Only a single time series per group must be returned
|
||||
limit := min(afa.ae.Limit, 1)
|
||||
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)) {
|
||||
workers := netstorage.MaxWorkers()
|
||||
if workers > len(tss) {
|
||||
workers = len(tss)
|
||||
}
|
||||
workers := min(netstorage.MaxWorkers(), len(tss))
|
||||
seriesPerWorker := (len(tss) + workers - 1) / workers
|
||||
workChs := make([]chan *timeseries, workers)
|
||||
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)
|
||||
}
|
||||
tooBigOffset := func(offset int64) bool {
|
||||
maxOffset := window / 2
|
||||
if maxOffset > 1800*1000 {
|
||||
maxOffset = 1800 * 1000
|
||||
}
|
||||
maxOffset := min(window/2, 1800*1000)
|
||||
return offset >= maxOffset
|
||||
}
|
||||
deleteCachedSeries := func(qt *querytracer.Tracer) {
|
||||
|
||||
@@ -820,17 +820,11 @@ func seekFirstTimestampIdxAfter(timestamps []int64, seekTimestamp int64, nHint i
|
||||
if len(timestamps) == 0 || timestamps[0] > seekTimestamp {
|
||||
return 0
|
||||
}
|
||||
startIdx := nHint - 2
|
||||
if startIdx < 0 {
|
||||
startIdx = 0
|
||||
}
|
||||
startIdx := max(nHint-2, 0)
|
||||
if startIdx >= len(timestamps) {
|
||||
startIdx = len(timestamps) - 1
|
||||
}
|
||||
endIdx := nHint + 2
|
||||
if endIdx > len(timestamps) {
|
||||
endIdx = len(timestamps)
|
||||
}
|
||||
endIdx := min(nHint+2, len(timestamps))
|
||||
if startIdx > 0 && timestamps[startIdx] <= seekTimestamp {
|
||||
timestamps = timestamps[startIdx:]
|
||||
endIdx -= startIdx
|
||||
|
||||
@@ -7,10 +7,12 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/metricsql"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||
"github.com/VictoriaMetrics/metricsql"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -20,6 +22,7 @@ var (
|
||||
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. "+
|
||||
"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.
|
||||
@@ -227,3 +230,8 @@ func toTagFilter(dst *storage.TagFilter, src *metricsql.LabelFilter) {
|
||||
dst.IsRegexp = src.IsRegexp
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
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).
|
||||
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.
|
||||
* 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)
|
||||
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)`.
|
||||
It is roughly equivalent to `rate(node_network_receive_bytes_total[$__interval])` when used in Grafana.
|
||||
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"}`
|
||||
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.
|
||||
@@ -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.
|
||||
It returns time series with name `foo-bar` containing label `baz=aa` with value `b`.
|
||||
Additionally, the following escape sequences are supported:
|
||||
- `\xXX`, where `XX` is hexadecimal representation of the escaped ascii char.
|
||||
- `\uXXXX`, where `XXXX` is a hexadecimal representation of the escaped unicode char.
|
||||
* `\xXX`, where `XX` is hexadecimal representation of the escaped ascii 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.
|
||||
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.
|
||||
@@ -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.
|
||||
|
||||
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
|
||||
|
||||
@@ -166,10 +167,10 @@ Additional details:
|
||||
* 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.
|
||||
* 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 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`.
|
||||
* 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"}`
|
||||
@@ -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`
|
||||
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.
|
||||
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).
|
||||
|
||||
|
||||
#### 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)
|
||||
@@ -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).
|
||||
|
||||
|
||||
### Transform 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`.
|
||||
For example, `alias(up, "foobar")` would rename `up` series to `foobar` series.
|
||||
|
||||
|
||||
#### drop_common_labels
|
||||
|
||||
`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:
|
||||
|
||||
```
|
||||
```metricsql
|
||||
sum by (__name__) (
|
||||
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).
|
||||
|
||||
|
||||
### Aggregate 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
|
||||
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:
|
||||
- `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
|
||||
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:
|
||||
|
||||
* 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 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`.
|
||||
* 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.
|
||||
1
app/vmselect/vmui/assets/index-Ccv_zSYG.css
Normal file
209
app/vmselect/vmui/assets/index-DK22yiEQ.js
Normal file
@@ -36,10 +36,10 @@
|
||||
<meta property="og:title" content="UI for VictoriaMetrics">
|
||||
<meta property="og:url" content="https://victoriametrics.com/">
|
||||
<meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data">
|
||||
<script type="module" crossorigin src="./assets/index-SqjehVXD.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index-DK22yiEQ.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/vendor-DBOs1yKE.js">
|
||||
<link rel="stylesheet" crossorigin href="./assets/vendor-D1GxaB_c.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-B7vIex3g.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-Ccv_zSYG.css">
|
||||
</head>
|
||||
<body>
|
||||
<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="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/metricName"}`, m.MetricNameCacheCollisions)
|
||||
|
||||
@@ -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.
|
||||
|
||||
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).
|
||||
|
||||
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).
|
||||
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.
|
||||
* 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)
|
||||
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)`.
|
||||
It is roughly equivalent to `rate(node_network_receive_bytes_total[$__interval])` when used in Grafana.
|
||||
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"}`
|
||||
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.
|
||||
@@ -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.
|
||||
It returns time series with name `foo-bar` containing label `baz=aa` with value `b`.
|
||||
Additionally, the following escape sequences are supported:
|
||||
- `\xXX`, where `XX` is hexadecimal representation of the escaped ascii char.
|
||||
- `\uXXXX`, where `XXXX` is a hexadecimal representation of the escaped unicode char.
|
||||
* `\xXX`, where `XX` is hexadecimal representation of the escaped ascii 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.
|
||||
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.
|
||||
@@ -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.
|
||||
|
||||
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
|
||||
|
||||
@@ -166,10 +167,10 @@ Additional details:
|
||||
* 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.
|
||||
* 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 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`.
|
||||
* 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"}`
|
||||
@@ -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`
|
||||
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.
|
||||
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).
|
||||
|
||||
|
||||
#### 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)
|
||||
@@ -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).
|
||||
|
||||
|
||||
### Transform 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`.
|
||||
For example, `alias(up, "foobar")` would rename `up` series to `foobar` series.
|
||||
|
||||
|
||||
#### drop_common_labels
|
||||
|
||||
`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:
|
||||
|
||||
```
|
||||
```metricsql
|
||||
sum by (__name__) (
|
||||
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).
|
||||
|
||||
|
||||
### Aggregate 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
|
||||
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:
|
||||
- `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
|
||||
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:
|
||||
|
||||
* 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 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`.
|
||||
* 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.
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
SearchIcon,
|
||||
} from "../../Main/Icons";
|
||||
import dayjs from "dayjs";
|
||||
import CodeExample from "../../Main/CodeExample/CodeExample";
|
||||
|
||||
interface BaseAlertProps {
|
||||
item: APIAlert;
|
||||
@@ -46,9 +47,9 @@ const BaseAlert = ({ item }: BaseAlertProps) => {
|
||||
<tr>
|
||||
<td className="vm-col-md">Query</td>
|
||||
<td>
|
||||
<pre>
|
||||
<code className="language-promql">{query}</code>
|
||||
</pre>
|
||||
<CodeExample
|
||||
code={query}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -19,30 +19,6 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a:hover > pre {
|
||||
background-color: $color-background-badge;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background-color: $color-background-hover;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: $color-background-badge;
|
||||
padding: 0 $padding-global;
|
||||
border-radius: $border-radius-small;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
.keyword,
|
||||
.function,
|
||||
.attr-name,
|
||||
.range-duration {
|
||||
color: $color-keyword;
|
||||
}
|
||||
}
|
||||
|
||||
.vm-col-sm {
|
||||
width: 10%;
|
||||
white-space: nowrap;
|
||||
@@ -59,6 +35,11 @@
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.vm-button {
|
||||
color: $color-passive;
|
||||
border: 1px solid var(--color-passive);
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
td, th {
|
||||
|
||||
@@ -7,6 +7,7 @@ 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;
|
||||
@@ -56,9 +57,9 @@ const BaseRule = ({ item }: BaseRuleProps) => {
|
||||
<tr>
|
||||
<td className="vm-col-md">Query</td>
|
||||
<td>
|
||||
<pre>
|
||||
<code className="language-promql">{query}</code>
|
||||
</pre>
|
||||
<CodeExample
|
||||
code={query}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{!!item.duration && (
|
||||
|
||||
@@ -19,28 +19,6 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
pre {
|
||||
position: relative;
|
||||
background-color: $color-background-badge;
|
||||
padding: 0 $padding-global;
|
||||
border-radius: $border-radius-small;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
.keyword,
|
||||
.function,
|
||||
.attr-name,
|
||||
.range-duration {
|
||||
color: $color-keyword;
|
||||
}
|
||||
div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
column-gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.vm-col-hidden {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ const ExploreMetricItem: FC<ExploreMetricItemProps> = ({
|
||||
onChangeOrder,
|
||||
}) => {
|
||||
|
||||
const isCounter = useMemo(() => /_sum?|_total?|_count?/.test(name), [name]);
|
||||
const isBucket = useMemo(() => /_bucket?/.test(name), [name]);
|
||||
const isCounter = useMemo(() => /_sum$|_total$|_count$/.test(name), [name]);
|
||||
const isBucket = useMemo(() => /_bucket$/.test(name), [name]);
|
||||
|
||||
const [rateEnabled, setRateEnabled] = useState(isCounter);
|
||||
|
||||
|
||||
@@ -6,12 +6,3 @@ export enum AppType {
|
||||
export const APP_TYPE = import.meta.env.VITE_APP_TYPE;
|
||||
export const APP_TYPE_VM = APP_TYPE === AppType.victoriametrics;
|
||||
export const APP_TYPE_ANOMALY = APP_TYPE === AppType.vmanomaly;
|
||||
|
||||
export const isDefaultDatasourceType = (datasourceType: string): boolean => {
|
||||
switch (APP_TYPE) {
|
||||
case AppType.victoriametrics:
|
||||
return datasourceType == "prometheus" || datasourceType == "";
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useAppDispatch } from "../state/common/StateContext";
|
||||
import { useAppDispatch, useAppState } from "../state/common/StateContext";
|
||||
import { useEffect, useState } from "preact/compat";
|
||||
import { ErrorTypes } from "../types";
|
||||
import { APP_TYPE_VM } from "../constants/appType";
|
||||
|
||||
const useFetchFlags = () => {
|
||||
const useFetchAppConfig = () => {
|
||||
const { serverUrl } = useAppState();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -16,7 +17,7 @@ const useFetchFlags = () => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const data = await fetch("./config.json");
|
||||
const data = await fetch(`${serverUrl}/vmui/config.json`);
|
||||
const config = await data.json();
|
||||
dispatch({ type: "SET_APP_CONFIG", payload: config || {} });
|
||||
} catch (e) {
|
||||
@@ -26,10 +27,10 @@ const useFetchFlags = () => {
|
||||
};
|
||||
|
||||
fetchAppConfig();
|
||||
}, []);
|
||||
}, [serverUrl]);
|
||||
|
||||
return { isLoading, error };
|
||||
};
|
||||
|
||||
export default useFetchFlags;
|
||||
export default useFetchAppConfig;
|
||||
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { useAppDispatch, useAppState } from "../state/common/StateContext";
|
||||
import { useEffect, useState } from "preact/compat";
|
||||
import { ErrorTypes } from "../types";
|
||||
import { APP_TYPE_VM } from "../constants/appType";
|
||||
import { getUrlWithoutTenant } from "../utils/tenants";
|
||||
|
||||
const useFetchFlags = () => {
|
||||
const { serverUrl } = useAppState();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<ErrorTypes | string>("");
|
||||
|
||||
useEffect(() => {
|
||||
const fetchFlags = async () => {
|
||||
if (!serverUrl || !APP_TYPE_VM) return;
|
||||
setError("");
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const url = getUrlWithoutTenant(serverUrl);
|
||||
const response = await fetch(`${url}/flags`);
|
||||
const data = await response.text();
|
||||
const flags = data.split("\n").filter(flag => flag.trim() !== "")
|
||||
.reduce((acc, flag) => {
|
||||
const [keyRaw, valueRaw] = flag.split("=");
|
||||
const key = keyRaw.trim().replace(/^-/, "");
|
||||
acc[key.trim()] = valueRaw ? valueRaw.trim().replace(/^"(.*)"$/, "$1") : null;
|
||||
return acc;
|
||||
}, {} as Record<string, string|null>);
|
||||
dispatch({ type: "SET_FLAGS", payload: flags });
|
||||
} catch (e) {
|
||||
setIsLoading(false);
|
||||
if (e instanceof Error) setError(`${e.name}: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
fetchFlags();
|
||||
}, [serverUrl]);
|
||||
|
||||
return { isLoading, error };
|
||||
};
|
||||
|
||||
export default useFetchFlags;
|
||||
|
||||
@@ -30,7 +30,7 @@ const useSearchParamsFromObject = () => {
|
||||
if (hasSearchParams) {
|
||||
setSearchParams(newSearchParams);
|
||||
} else {
|
||||
navigate(`?${searchParams.toString()}`, { replace: true });
|
||||
navigate(`?${newSearchParams.toString()}`, { replace: true });
|
||||
}
|
||||
}, [searchParams, navigate]);
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import Header from "../Header/Header";
|
||||
import { FC, useEffect } from "preact/compat";
|
||||
import { Outlet, useLocation, useSearchParams } from "react-router-dom";
|
||||
import { Outlet, useSearchParams } from "react-router-dom";
|
||||
import qs from "qs";
|
||||
import "../MainLayout/style.scss";
|
||||
import { getAppModeEnable } from "../../utils/app-mode";
|
||||
import classNames from "classnames";
|
||||
import Footer from "../Footer/Footer";
|
||||
import { routerOptions } from "../../router";
|
||||
import useFetchDefaultTimezone from "../../hooks/useFetchDefaultTimezone";
|
||||
import useDeviceDetect from "../../hooks/useDeviceDetect";
|
||||
import ControlsAnomalyLayout from "./ControlsAnomalyLayout";
|
||||
@@ -14,17 +13,10 @@ import ControlsAnomalyLayout from "./ControlsAnomalyLayout";
|
||||
const AnomalyLayout: FC = () => {
|
||||
const appModeEnable = getAppModeEnable();
|
||||
const { isMobile } = useDeviceDetect();
|
||||
const { pathname } = useLocation();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
useFetchDefaultTimezone();
|
||||
|
||||
const setDocumentTitle = () => {
|
||||
const defaultTitle = "vmui for vmanomaly";
|
||||
const routeTitle = routerOptions[pathname]?.title;
|
||||
document.title = routeTitle ? `${routeTitle} - ${defaultTitle}` : defaultTitle;
|
||||
};
|
||||
|
||||
// for support old links with search params
|
||||
const redirectSearchToHashParams = () => {
|
||||
const { search, href } = window.location;
|
||||
@@ -38,7 +30,6 @@ const AnomalyLayout: FC = () => {
|
||||
if (newHref !== href) window.location.replace(newHref);
|
||||
};
|
||||
|
||||
useEffect(setDocumentTitle, [pathname]);
|
||||
useEffect(redirectSearchToHashParams, []);
|
||||
|
||||
return <section className="vm-container">
|
||||
|
||||
@@ -2,6 +2,7 @@ import { FC, memo } from "preact/compat";
|
||||
import { LogoShortIcon } from "../../components/Main/Icons";
|
||||
import "./style.scss";
|
||||
import { footerLinksByDefault } from "../../constants/footerLinks";
|
||||
import { useAppState } from "../../state/common/StateContext";
|
||||
|
||||
interface Props {
|
||||
links?: {
|
||||
@@ -13,10 +14,12 @@ interface Props {
|
||||
|
||||
const Footer: FC<Props> = memo(({ links = footerLinksByDefault }) => {
|
||||
const copyrightYears = `2019-${new Date().getFullYear()}`;
|
||||
const { appConfig } = useAppState();
|
||||
const version = appConfig?.version;
|
||||
|
||||
return <footer className="vm-footer">
|
||||
<a
|
||||
className="vm-link vm-footer__website"
|
||||
className="vm-link vm-footer__link"
|
||||
target="_blank"
|
||||
href="https://victoriametrics.com/"
|
||||
rel="me noreferrer"
|
||||
@@ -37,7 +40,8 @@ const Footer: FC<Props> = memo(({ links = footerLinksByDefault }) => {
|
||||
</a>
|
||||
))}
|
||||
<div className="vm-footer__copyright">
|
||||
© {copyrightYears} VictoriaMetrics
|
||||
© {copyrightYears} VictoriaMetrics.
|
||||
{version && <span className="vm-footer__version"> {version}</span>}
|
||||
</div>
|
||||
</footer>;
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ const Header: FC<HeaderProps> = ({ controlsComponent }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const windowSize = useWindowSize();
|
||||
const displaySidebar = useMemo(() => window.innerWidth < 1000, [windowSize]);
|
||||
const displaySidebar = useMemo(() => window.innerWidth < 1230, [windowSize]);
|
||||
|
||||
const { isDarkTheme } = useAppState();
|
||||
const appModeEnable = getAppModeEnable();
|
||||
|
||||
@@ -53,7 +53,7 @@ const HeaderControls: FC<ControlsProps & HeaderProps> = ({
|
||||
if (isMobile) {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="vm-header-controls">
|
||||
<Button
|
||||
className={classNames({
|
||||
"vm-header-button": !appModeEnable
|
||||
|
||||
@@ -23,7 +23,7 @@ const NavSubItem: FC<NavItemProps> = ({
|
||||
color,
|
||||
background,
|
||||
submenu,
|
||||
direction
|
||||
direction = "row"
|
||||
}) => {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
@@ -37,8 +37,9 @@ const NavSubItem: FC<NavItemProps> = ({
|
||||
} = useBoolean(false);
|
||||
|
||||
const handleOpenSubmenu = () => {
|
||||
setOpenSubmenu();
|
||||
if (menuTimeout) clearTimeout(menuTimeout);
|
||||
if (direction === "row" || !openSubmenu) setOpenSubmenu();
|
||||
if (direction === "column" && openSubmenu) handleCloseSubmenu();
|
||||
if (direction === "row" && menuTimeout) clearTimeout(menuTimeout);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
@@ -55,50 +56,31 @@ const NavSubItem: FC<NavItemProps> = ({
|
||||
handleCloseSubmenu();
|
||||
}, [pathname]);
|
||||
|
||||
if (direction === "column") {
|
||||
return (
|
||||
<>
|
||||
{submenu.map(sm => (
|
||||
<NavItem
|
||||
key={sm.value}
|
||||
activeMenu={activeMenu}
|
||||
value={sm.value || ""}
|
||||
label={sm.label || ""}
|
||||
type={sm.type || NavigationItemType.internalLink}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-header-nav-item": true,
|
||||
"vm-header-nav-item_sub": true,
|
||||
"vm-header-nav-item_open": openSubmenu,
|
||||
"vm-header-nav-item_active": submenu.find(m => m.value === activeMenu)
|
||||
})}
|
||||
style={{ color }}
|
||||
onMouseEnter={handleOpenSubmenu}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onMouseEnter={direction === "column" ? undefined : handleOpenSubmenu}
|
||||
onMouseLeave={direction === "column" ? undefined : handleMouseLeave}
|
||||
onClick={direction === "column" ? handleOpenSubmenu : undefined}
|
||||
ref={buttonRef}
|
||||
>
|
||||
{label}
|
||||
<ArrowDropDownIcon/>
|
||||
|
||||
<Popper
|
||||
open={openSubmenu}
|
||||
placement="bottom-left"
|
||||
offset={{ top: 12, left: 0 }}
|
||||
onClose={handleCloseSubmenu}
|
||||
buttonRef={buttonRef}
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-header-nav-item": true,
|
||||
"vm-header-nav-item_sub": true,
|
||||
"vm-header-nav-item_active": submenu.find(m => m.value === activeMenu),
|
||||
})}
|
||||
>
|
||||
{label}
|
||||
<ArrowDropDownIcon/>
|
||||
</div>
|
||||
{direction === "column" ? (
|
||||
<div
|
||||
className="vm-header-nav-item-submenu"
|
||||
style={{ background }}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onMouseEnter={handleMouseEnterPopup}
|
||||
>
|
||||
{submenu.map(sm => (
|
||||
<NavItem
|
||||
@@ -111,7 +93,33 @@ const NavSubItem: FC<NavItemProps> = ({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Popper>
|
||||
) : (
|
||||
<Popper
|
||||
open={openSubmenu}
|
||||
placement="bottom-left"
|
||||
offset={{ top: 12, left: 0 }}
|
||||
onClose={handleCloseSubmenu}
|
||||
buttonRef={buttonRef}
|
||||
>
|
||||
<div
|
||||
className="vm-header-nav-item-submenu"
|
||||
style={{ background }}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onMouseEnter={handleMouseEnterPopup}
|
||||
>
|
||||
{submenu.map(sm => (
|
||||
<NavItem
|
||||
key={sm.value}
|
||||
activeMenu={activeMenu}
|
||||
value={sm.value || ""}
|
||||
label={sm.label || ""}
|
||||
color={color}
|
||||
type={sm.type || NavigationItemType.internalLink}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Popper>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,6 +20,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
&_column &-item-submenu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&_column &-item_open &-item-submenu {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
&-item {
|
||||
position: relative;
|
||||
padding: $padding-global $padding-small;
|
||||
@@ -44,6 +52,7 @@
|
||||
}
|
||||
|
||||
&_active {
|
||||
padding: $padding-global $padding-small 10px $padding-small;
|
||||
border-bottom: 2px solid rgba($color-black, 0.2);
|
||||
}
|
||||
|
||||
@@ -60,7 +69,6 @@
|
||||
|
||||
&-submenu {
|
||||
display: grid;
|
||||
white-space: nowrap;
|
||||
padding: $padding-small;
|
||||
color: $color-white;
|
||||
border-radius: $border-radius-small;
|
||||
|
||||
@@ -52,7 +52,7 @@ const SidebarHeader: FC<SidebarHeaderProps> = ({
|
||||
"vm-header-sidebar-menu_open": openMenu
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
<div className="vm-header-sidebar-scrollable">
|
||||
<HeaderNav
|
||||
color={color}
|
||||
background={background}
|
||||
|
||||
@@ -25,7 +25,13 @@ $sidebar-transition: cubic-bezier(0.280, 0.840, 0.420, 1);
|
||||
z-index: 102;
|
||||
}
|
||||
}
|
||||
|
||||
&-scrollable {
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
&-scrollable::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
&-menu {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
||||
@@ -22,15 +22,9 @@
|
||||
}
|
||||
|
||||
&_sidebar {
|
||||
display: grid;
|
||||
grid-template-columns: 40px auto 1fr;
|
||||
box-shadow: $color-background-body 0 1px 1px 0px;
|
||||
}
|
||||
|
||||
&_mobile {
|
||||
display: grid;
|
||||
grid-template-columns: 33px 1fr 33px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
box-shadow: $color-background-body 0 1px 1px 0px;
|
||||
}
|
||||
|
||||
&_dark {
|
||||
|
||||
@@ -11,7 +11,6 @@ import { useFetchDashboards } from "../../pages/PredefinedPanels/hooks/useFetchD
|
||||
import useDeviceDetect from "../../hooks/useDeviceDetect";
|
||||
import ControlsMainLayout from "./ControlsMainLayout";
|
||||
import useFetchDefaultTimezone from "../../hooks/useFetchDefaultTimezone";
|
||||
import useFetchFlags from "../../hooks/useFetchFlags";
|
||||
import useFetchAppConfig from "../../hooks/useFetchAppConfig";
|
||||
|
||||
const MainLayout: FC = () => {
|
||||
@@ -23,7 +22,6 @@ const MainLayout: FC = () => {
|
||||
useFetchDashboards();
|
||||
useFetchDefaultTimezone();
|
||||
useFetchAppConfig();
|
||||
useFetchFlags();
|
||||
|
||||
const setDocumentTitle = () => {
|
||||
const defaultTitle = "vmui";
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
align-items: stretch;
|
||||
gap: $padding-medium;
|
||||
max-width: calc(100vw - var(--scrollbar-width));
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
align-items: stretch;
|
||||
gap: $padding-medium;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { APP_TYPE, AppType } from "../constants/appType";
|
||||
|
||||
const router = {
|
||||
home: "/",
|
||||
metrics: "/metrics",
|
||||
@@ -15,7 +17,6 @@ const router = {
|
||||
rawQuery: "/raw-query",
|
||||
downsamplingDebug: "/downsampling-filters-debug",
|
||||
retentionDebug: "/retention-filters-debug",
|
||||
alerts: "/alerts",
|
||||
rules: "/rules",
|
||||
notifiers: "/notifiers",
|
||||
};
|
||||
@@ -51,11 +52,23 @@ const routerOptionsDefault = {
|
||||
},
|
||||
};
|
||||
|
||||
const getDefaultOptions = (appType: AppType) => {
|
||||
switch (appType) {
|
||||
case AppType.vmanomaly:
|
||||
return {
|
||||
title: "Anomaly exploration",
|
||||
...routerOptionsDefault,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
title: "Query",
|
||||
...routerOptionsDefault,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const routerOptions: { [key: string]: RouterOptions } = {
|
||||
[router.home]: {
|
||||
title: "Query",
|
||||
...routerOptionsDefault,
|
||||
},
|
||||
[router.home]: getDefaultOptions(APP_TYPE),
|
||||
[router.rawQuery]: {
|
||||
title: "Raw query",
|
||||
...routerOptionsDefault,
|
||||
@@ -127,10 +140,7 @@ export const routerOptions: { [key: string]: RouterOptions } = {
|
||||
title: "Icons",
|
||||
header: {},
|
||||
},
|
||||
[router.anomaly]: {
|
||||
title: "Anomaly exploration",
|
||||
...routerOptionsDefault,
|
||||
},
|
||||
[router.anomaly]: getDefaultOptions(AppType.vmanomaly),
|
||||
[router.query]: {
|
||||
title: "Query",
|
||||
...routerOptionsDefault,
|
||||
|
||||
@@ -17,7 +17,7 @@ interface NavigationConfig {
|
||||
serverUrl: string,
|
||||
isEnterpriseLicense: boolean,
|
||||
showPredefinedDashboards: boolean,
|
||||
showAlertLink: boolean,
|
||||
showAlerting: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,7 +43,7 @@ const getExploreNav = () => [
|
||||
];
|
||||
|
||||
/**
|
||||
* Submenu for Alerts tab
|
||||
* Submenu for Alerting tab
|
||||
*/
|
||||
|
||||
const getAlertingNav = () => [
|
||||
@@ -57,14 +57,14 @@ const getAlertingNav = () => [
|
||||
export const getDefaultNavigation = ({
|
||||
isEnterpriseLicense,
|
||||
showPredefinedDashboards,
|
||||
showAlertLink,
|
||||
showAlerting,
|
||||
}: NavigationConfig): NavigationItem[] => [
|
||||
{ value: router.home },
|
||||
{ value: router.rawQuery },
|
||||
{ label: "Explore", submenu: getExploreNav() },
|
||||
{ label: "Tools", submenu: getToolsNav(isEnterpriseLicense) },
|
||||
{ value: router.dashboards, hide: !showPredefinedDashboards },
|
||||
{ value: "Alerting", submenu: getAlertingNav(), hide: !showAlertLink },
|
||||
{ value: "Alerting", submenu: getAlertingNav(), hide: !showAlerting },
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,17 +9,17 @@ import { APP_TYPE, AppType } from "../constants/appType";
|
||||
const useNavigationMenu = () => {
|
||||
const appModeEnable = getAppModeEnable();
|
||||
const { dashboardsSettings } = useDashboardsState();
|
||||
const { serverUrl, flags, appConfig } = useAppState();
|
||||
const { serverUrl, appConfig } = useAppState();
|
||||
const isEnterpriseLicense = appConfig.license?.type === "enterprise";
|
||||
const showAlertLink = Boolean(flags["vmalert.proxyURL"]);
|
||||
const showAlerting = appConfig?.vmalert?.enabled || false;
|
||||
const showPredefinedDashboards = Boolean(!appModeEnable && dashboardsSettings.length);
|
||||
|
||||
const navigationConfig = useMemo(() => ({
|
||||
serverUrl,
|
||||
isEnterpriseLicense,
|
||||
showAlertLink,
|
||||
showAlerting,
|
||||
showPredefinedDashboards
|
||||
}), [serverUrl, isEnterpriseLicense, showAlertLink, showPredefinedDashboards]);
|
||||
}), [serverUrl, isEnterpriseLicense, showAlerting, showPredefinedDashboards]);
|
||||
|
||||
|
||||
const menu = useMemo(() => {
|
||||
|
||||
@@ -10,7 +10,6 @@ export interface AppState {
|
||||
tenantId: string;
|
||||
theme: Theme;
|
||||
isDarkTheme: boolean | null;
|
||||
flags: Record<string, string | null>;
|
||||
appConfig: AppConfig
|
||||
}
|
||||
|
||||
@@ -18,7 +17,6 @@ export type Action =
|
||||
| { type: "SET_SERVER", payload: string }
|
||||
| { type: "SET_THEME", payload: Theme }
|
||||
| { type: "SET_TENANT_ID", payload: string }
|
||||
| { type: "SET_FLAGS", payload: Record<string, string | null> }
|
||||
| { type: "SET_APP_CONFIG", payload: AppConfig }
|
||||
| { type: "SET_DARK_THEME" }
|
||||
|
||||
@@ -29,7 +27,6 @@ export const initialState: AppState = {
|
||||
tenantId,
|
||||
theme: (getFromStorage("THEME") || Theme.system) as Theme,
|
||||
isDarkTheme: null,
|
||||
flags: {},
|
||||
appConfig: {}
|
||||
};
|
||||
|
||||
@@ -56,11 +53,6 @@ export function reducer(state: AppState, action: Action): AppState {
|
||||
...state,
|
||||
isDarkTheme: isDarkTheme(state.theme)
|
||||
};
|
||||
case "SET_FLAGS":
|
||||
return {
|
||||
...state,
|
||||
flags: action.payload
|
||||
};
|
||||
case "SET_APP_CONFIG":
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -180,6 +180,10 @@ export interface AppConfig {
|
||||
license?: {
|
||||
type?: "enterprise" | "opensource";
|
||||
};
|
||||
vmalert?: {
|
||||
enabled: boolean;
|
||||
};
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
|
||||
@@ -48,6 +48,9 @@ TSBS_WORKERS ?= 4
|
||||
TSBS_QUERY_TYPE := cpu-max-all-8
|
||||
TSBS_DATA_FILE := /tmp/tsbs-data-$(TSBS_SCALE)-$(TSBS_START)-$(TSBS_END)-$(TSBS_STEP).gz
|
||||
TSBS_QUERY_FILE := /tmp/tsbs-queries-$(TSBS_QUERY_TYPE)-$(TSBS_SCALE)-$(TSBS_START)-$(TSBS_END)-$(TSBS_QUERIES).gz
|
||||
TSBS_LOAD_RESULT_CSV_FILE := /tmp/tsbs-load-$(TSBS_SCALE)-$(TSBS_START)-$(TSBS_END)-$(TSBS_STEP).csv
|
||||
TSBS_LOAD_RESULT_CSV_FILE_COMPARE ?=
|
||||
TSBS_PLOT_SCRIPT := $(shell pwd)/benchmarks/plot-load.sh
|
||||
# For cluster setup use http://vminsert:8480/insert/0/influx/write
|
||||
TSBS_WRITE_URLS ?= http://localhost:8428/write
|
||||
# For cluster setup use http://vmselect:8481/select/0/prometheus
|
||||
@@ -76,7 +79,10 @@ tsbs-generate-data:
|
||||
|
||||
# Load data into VictoriaMetrics
|
||||
tsbs-load-data:
|
||||
cat $(TSBS_DATA_FILE) | gunzip | /tmp/tsbs/bin/tsbs_load_victoriametrics --workers=$(TSBS_WORKERS) --urls=$(TSBS_WRITE_URLS)
|
||||
cat $(TSBS_DATA_FILE) | gunzip | /tmp/tsbs/bin/tsbs_load_victoriametrics \
|
||||
--workers=$(TSBS_WORKERS) \
|
||||
--urls=$(TSBS_WRITE_URLS) \
|
||||
2>/dev/tty | tee $(TSBS_LOAD_RESULT_CSV_FILE)
|
||||
curl -s $(TSBS_METRICS_URL) | grep \
|
||||
-e process_cpu_seconds_user_total \
|
||||
-e process_cpu_seconds_system_total \
|
||||
@@ -133,3 +139,16 @@ tsbs-run-queries-all:
|
||||
|
||||
# Too heavy: retrieves 1B samples
|
||||
# $(MAKE) tsbs-run-queries TSBS_QUERY_TYPE=double-groupby-all
|
||||
|
||||
# Plot the data load `per interval metrics/s` measurements
|
||||
#
|
||||
# To plot measurements collected during the recent benchmark, run
|
||||
# make tsbs-plot-load
|
||||
#
|
||||
# To plot the measurements from some other benchmark, run
|
||||
# make tsbs-plot-load TSBS_LOAD_RESULT_CSV_FILE=/path/to/file.csv
|
||||
#
|
||||
# To plot the measurements from two benchmarks, run
|
||||
# make tsbs-plot-load TSBS_LOAD_RESULT_CSV_FILE=/path/to/file1.csv TSBS_LOAD_RESULT_CSV_FILE_COMPARE=/path/to/file2.csv
|
||||
tsbs-plot-load:
|
||||
$(TSBS_PLOT_SCRIPT) $(TSBS_LOAD_RESULT_CSV_FILE) $(TSBS_LOAD_RESULT_CSV_FILE_COMPARE)
|
||||
|
||||
55
benchmarks/plot-load.sh
Executable file
@@ -0,0 +1,55 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Check the number of arguments
|
||||
if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then
|
||||
echo "Usage: $(basename $0) file1.csv [file2.csv]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
file1="$1"
|
||||
file2="$2"
|
||||
|
||||
# Check if the files exist
|
||||
if [ ! -f "$file1" ]; then
|
||||
echo "File not found: $file1"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "$file2" ] && [ ! -f "$file2" ]; then
|
||||
echo "File not found: $file2"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Temporary file for plot data
|
||||
plotdata="/tmp/tsbs_plot.dat"
|
||||
|
||||
if [ -z "$file2" ]; then
|
||||
# === One file ===
|
||||
awk -F, '/^Summary:/ {exit} NR > 1 {print $1, $2}' "$file1" > "$plotdata"
|
||||
else
|
||||
# === Two files ===
|
||||
|
||||
# Get time and per.metric/s from file1
|
||||
awk -F, '/^Summary:/ {exit} NR > 1 {print $1, $2}' "$file1" > /tmp/file1.dat
|
||||
# Get per.metric/s from file2
|
||||
awk -F, '/^Summary:/ {exit} NR > 1 {print $2}' "$file2" > /tmp/file2.dat
|
||||
# Merge by rows: time, val1, val2
|
||||
paste /tmp/file1.dat /tmp/file2.dat | awk '{print $1, $2, $3}' > "$plotdata"
|
||||
fi
|
||||
|
||||
# === Build plot dynamically ===
|
||||
gnuplot -persist <<-EOF
|
||||
set datafile separator " "
|
||||
set title "per.metric/s"
|
||||
set xlabel "Timestamp"
|
||||
set xdata time
|
||||
set timefmt "%s"
|
||||
set format x "%H:%M:%S"
|
||||
set ylabel "per. metric/s"
|
||||
set format y "%'.0f"
|
||||
set grid
|
||||
|
||||
plot "$plotdata" using 1:2 with lines title "$(basename "$file1")" \
|
||||
$( [[ -n "$file2" ]] && echo ", \\
|
||||
\"$plotdata\" using 1:3 with lines title \"$(basename "$file2")\"" )
|
||||
EOF
|
||||
@@ -120,7 +120,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -154,7 +154,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -191,7 +191,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -225,7 +225,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -264,7 +264,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -298,7 +298,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -336,7 +336,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -370,7 +370,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -408,7 +408,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -442,7 +442,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -480,7 +480,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -514,7 +514,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -552,7 +552,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -586,7 +586,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -623,7 +623,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -657,7 +657,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -702,7 +702,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -763,7 +763,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -834,7 +834,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -870,7 +870,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -956,7 +956,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -994,7 +994,7 @@
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -1065,7 +1065,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -1103,7 +1103,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -1174,7 +1174,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -1212,7 +1212,7 @@
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -1281,7 +1281,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -1319,7 +1319,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -1390,7 +1390,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -1428,7 +1428,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -1505,7 +1505,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -1543,7 +1543,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -1580,7 +1580,7 @@
|
||||
"type": "prometheus",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Percentage of used RSS memory (resident).\nThe RSS memory shows the amount of memory recently accessed by the application. It includes anonymous memory and data from recently accessed files (aka page cache).\nThe application's performance will significantly degrade when memory usage is close to 100%.\n\nClick on the line and choose Drilldown to show memory usage per instance",
|
||||
"description": "How much resident memory (RAM) the process is actually using, compared to its allowed container or system limit.\n\n- Good: Below 70% most of the time, maybe spiking a bit under load.\n- Bad: Above 90% for more than 5 minutes = risk of out-of-memory (OOM) kill.\n\nClick on the line and choose Drilldown to show memory usage per instance",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -1670,7 +1670,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -1783,7 +1783,7 @@
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -1899,7 +1899,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -1923,7 +1923,7 @@
|
||||
"type": "prometheus",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Shows CPU pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html).\n\nThe lower the better.",
|
||||
"description": "CPU pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html). Helps troubleshoot high CPU usage or throttling.\nLower is better.\n\n- waiting: The percentage of time at least one task in the process was ready to run (runnable) but couldn't get scheduled on the CPU.\n- stalled: The percentage of time all tasks in the process (except idle ones) were unable to get CPU time — a full CPU stall.\n\nIf there's a CPU burst, it's normal to see waiting or stalled > 1%. It only becomes a concern if it consistently climbs above 5–10% and aligns with latency spikes or GC slowdowns.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -2004,7 +2004,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -2044,7 +2044,7 @@
|
||||
"type": "prometheus",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Shows memory pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html).\n\nThe lower the better.",
|
||||
"description": "Shows memory pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html).\nLower is better.\n\n- waiting: Time fraction where at least one thread was blocked on memory.\n- stalled: Time fraction where every thread was blocked on memory (severe pressure).\n\nIf queries slow down and both series spike, the host is likely limited by RAM or I/O throughput.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -2125,7 +2125,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -2261,7 +2261,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -2404,7 +2404,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -2526,7 +2526,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -2652,7 +2652,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -2678,7 +2678,7 @@
|
||||
"type": "prometheus",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Shows IO pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html).\n\nThe lower the better.",
|
||||
"description": "Shows IO pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html).\nThe lower the better.\n\n- waiting: at least one runnable thread blocked on block-`I/O` (disk, NVMe, network-storage) while others could still make progress.\n- stalled: all non-idle threads simultaneously waiting on `I/O`; no useful user code ran during these periods → true `I/O` thrashing.\n\nIf stalled > 0 while querying, it's recommended to increase queue depth on NVMe, raise blk-mq budgets, or relax cgroup I/O limits.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -2759,7 +2759,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -2883,7 +2883,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -2990,7 +2990,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -3097,7 +3097,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -3203,7 +3203,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -3306,7 +3306,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -3414,7 +3414,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -3470,7 +3470,7 @@
|
||||
"content": "See [Troubleshooting](https://docs.victoriametrics.com/victoriametrics/troubleshooting/) docs.",
|
||||
"mode": "markdown"
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"title": "",
|
||||
"transparent": true,
|
||||
"type": "text"
|
||||
@@ -3526,8 +3526,7 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -3582,7 +3581,7 @@
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -3662,8 +3661,7 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "transparent",
|
||||
"value": null
|
||||
"color": "transparent"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -3699,7 +3697,7 @@
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -3768,8 +3766,7 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -3805,7 +3802,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -3878,8 +3875,7 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -3915,7 +3911,7 @@
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -3982,8 +3978,7 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -4019,7 +4014,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -4090,8 +4085,7 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -4129,7 +4123,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -4202,8 +4196,7 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -4246,7 +4239,7 @@
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -4318,8 +4311,7 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -4355,7 +4347,7 @@
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -4425,8 +4417,7 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -4464,7 +4455,7 @@
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -4549,8 +4540,7 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -4586,7 +4576,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -4626,8 +4616,7 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -4688,7 +4677,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -4787,8 +4776,7 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -4824,7 +4812,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -4892,8 +4880,7 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -4925,7 +4912,7 @@
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -5648,6 +5635,122 @@
|
||||
},
|
||||
"id": 24,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Shows the approx time needed to reach 100% of allowed disk capacity for at least one vmstorage node based on the following params:\n* free disk space (after -storage.minFreeDiskSpaceBytes);\n* row ingestion rate;\n* compression.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"links": [
|
||||
{
|
||||
"targetBlank": true,
|
||||
"title": "Drilldown",
|
||||
"url": "/d/oS7Bi_0Wz?viewPanel=196&${__url_time_range}&${__all_variables}"
|
||||
}
|
||||
],
|
||||
"mappings": [],
|
||||
"min": 0,
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "s"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 41
|
||||
},
|
||||
"id": 113,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"mean",
|
||||
"lastNotNull",
|
||||
"min"
|
||||
],
|
||||
"displayMode": "table",
|
||||
"placement": "bottom",
|
||||
"showLegend": true,
|
||||
"sortBy": "Last *",
|
||||
"sortDesc": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "multi",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "min((vm_free_disk_space_bytes{job=~\"$job_storage\", instance=~\"$instance\"}-vm_free_disk_space_limit_bytes{job=~\"$job_storage\", instance=~\"$instance\"}) \n/ \nignoring(path) (\n (rate(vm_rows_added_to_storage_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d]) - \n sum(rate(vm_deduplicated_samples_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d])) without (type)) * \n (\n sum(vm_data_size_bytes{job=~\"$job_storage\", instance=~\"$instance\", type!~\"indexdb.*\"}) without(type) /\n sum(vm_rows{job=~\"$job_storage\", instance=~\"$instance\", type!~\"indexdb.*\"}) without(type)\n )\n +\n rate(vm_new_timeseries_created_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d]) * \n (\n sum(vm_data_size_bytes{job=~\"$job_storage\", instance=~\"$instance\", type=\"indexdb/file\"}) /\n sum(vm_rows{job=~\"$job_storage\", instance=~\"$instance\", type=\"indexdb/file\"})\n )\n) > 0)",
|
||||
"format": "time_series",
|
||||
"interval": "",
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "min ETA",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Storage full ETA ($instance)",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
@@ -5737,7 +5840,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -5851,7 +5954,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -6007,7 +6110,7 @@
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -6135,7 +6238,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -6298,7 +6401,7 @@
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -6421,7 +6524,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -6528,7 +6631,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -6634,7 +6737,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -6764,7 +6867,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -6917,7 +7020,7 @@
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -7042,7 +7145,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -7163,7 +7266,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -7282,7 +7385,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -7396,7 +7499,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -10103,7 +10206,7 @@
|
||||
"type": "prometheus",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Shows the approx time needed to reach 100% of disk capacity based on the following params:\n* free disk space (after -storage.minFreeDiskSpaceBytes);\n* row ingestion rate;\n* compression.",
|
||||
"description": "Shows the approx time needed to reach 100% of allowed disk capacity based on the following params:\n* free disk space (after -storage.minFreeDiskSpaceBytes);\n* row ingestion rate;\n* compression.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -10288,7 +10391,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -10535,7 +10638,7 @@
|
||||
],
|
||||
"preload": false,
|
||||
"refresh": "",
|
||||
"schemaVersion": 40,
|
||||
"schemaVersion": 41,
|
||||
"tags": [
|
||||
"victoriametrics"
|
||||
],
|
||||
@@ -10555,7 +10658,14 @@
|
||||
"type": "datasource"
|
||||
},
|
||||
{
|
||||
"current": {},
|
||||
"current": {
|
||||
"text": [
|
||||
"All"
|
||||
],
|
||||
"value": [
|
||||
"$__all"
|
||||
]
|
||||
},
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "$ds"
|
||||
@@ -10574,7 +10684,14 @@
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"current": {},
|
||||
"current": {
|
||||
"text": [
|
||||
"All"
|
||||
],
|
||||
"value": [
|
||||
"$__all"
|
||||
]
|
||||
},
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "$ds"
|
||||
@@ -10594,7 +10711,14 @@
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"current": {},
|
||||
"current": {
|
||||
"text": [
|
||||
"All"
|
||||
],
|
||||
"value": [
|
||||
"$__all"
|
||||
]
|
||||
},
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "$ds"
|
||||
@@ -10614,7 +10738,14 @@
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"current": {},
|
||||
"current": {
|
||||
"text": [
|
||||
"All"
|
||||
],
|
||||
"value": [
|
||||
"$__all"
|
||||
]
|
||||
},
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "$ds"
|
||||
@@ -10635,7 +10766,14 @@
|
||||
},
|
||||
{
|
||||
"allValue": ".*",
|
||||
"current": {},
|
||||
"current": {
|
||||
"text": [
|
||||
"All"
|
||||
],
|
||||
"value": [
|
||||
"$__all"
|
||||
]
|
||||
},
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "$ds"
|
||||
@@ -10685,6 +10823,5 @@
|
||||
"timezone": "",
|
||||
"title": "VictoriaMetrics - cluster",
|
||||
"uid": "oS7Bi_0Wz",
|
||||
"version": 1,
|
||||
"weekStart": ""
|
||||
"version": 1
|
||||
}
|
||||
|
||||
@@ -1449,7 +1449,7 @@
|
||||
"type": "prometheus",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Percentage of used RSS memory (resident).\nThe RSS memory shows the amount of memory recently accessed by the application. It includes anonymous memory and data from recently accessed files (aka page cache).\nThe application's performance will significantly degrade when memory usage is close to 100%.",
|
||||
"description": "How much resident memory (RAM) the process is actually using, compared to its allowed container or system limit.\n\n- Good: Below 70% most of the time, maybe spiking a bit under load.\n- Bad: Above 90% for more than 5 minutes = risk of out-of-memory (OOM) kill.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -2088,7 +2088,7 @@
|
||||
"type": "prometheus",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Shows CPU pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html).\n\nThe lower the better.",
|
||||
"description": "CPU pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html). Helps troubleshoot high CPU usage or throttling.\nLower is better.\n\n- waiting: The percentage of time at least one task in the process was ready to run (runnable) but couldn't get scheduled on the CPU.\n- stalled: The percentage of time all tasks in the process (except idle ones) were unable to get CPU time — a full CPU stall.\n\nIf there's a CPU burst, it's normal to see waiting or stalled > 1%. It only becomes a concern if it consistently climbs above 5–10% and aligns with latency spikes or GC slowdowns.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -2210,7 +2210,7 @@
|
||||
"type": "prometheus",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Shows memory pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html).\n\nThe lower the better.",
|
||||
"description": "Shows memory pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html).\nLower is better.\n\n- waiting: Time fraction where at least one thread was blocked on memory.\n- stalled: Time fraction where every thread was blocked on memory (severe pressure).\n\nIf queries slow down and both series spike, the host is likely limited by RAM or I/O throughput.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -4899,7 +4899,7 @@
|
||||
"type": "prometheus",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Shows the approx time needed to reach 100% of disk capacity based on the following params:\n* free disk space (after -storage.minFreeDiskSpaceBytes);\n* row ingestion rate;\n* compression.",
|
||||
"description": "Shows the approx time needed to reach 100% of allowed disk capacity based on the following params:\n* free disk space (after -storage.minFreeDiskSpaceBytes);\n* row ingestion rate;\n* compression.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
@@ -121,7 +120,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -155,7 +154,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -192,7 +191,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -226,7 +225,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -265,7 +264,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -299,7 +298,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -337,7 +336,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -371,7 +370,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -409,7 +408,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -443,7 +442,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -481,7 +480,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -515,7 +514,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -553,7 +552,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -587,7 +586,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -624,7 +623,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -658,7 +657,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -703,7 +702,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -764,7 +763,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -835,7 +834,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -871,7 +870,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -957,7 +956,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -995,7 +994,7 @@
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -1066,7 +1065,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -1104,7 +1103,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -1175,7 +1174,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -1213,7 +1212,7 @@
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -1282,7 +1281,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -1320,7 +1319,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -1391,7 +1390,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -1429,7 +1428,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -1506,7 +1505,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -1544,7 +1543,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -1581,7 +1580,7 @@
|
||||
"type": "victoriametrics-metrics-datasource",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Percentage of used RSS memory (resident).\nThe RSS memory shows the amount of memory recently accessed by the application. It includes anonymous memory and data from recently accessed files (aka page cache).\nThe application's performance will significantly degrade when memory usage is close to 100%.\n\nClick on the line and choose Drilldown to show memory usage per instance",
|
||||
"description": "How much resident memory (RAM) the process is actually using, compared to its allowed container or system limit.\n\n- Good: Below 70% most of the time, maybe spiking a bit under load.\n- Bad: Above 90% for more than 5 minutes = risk of out-of-memory (OOM) kill.\n\nClick on the line and choose Drilldown to show memory usage per instance",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -1671,7 +1670,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -1784,7 +1783,7 @@
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -1900,7 +1899,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -1924,7 +1923,7 @@
|
||||
"type": "victoriametrics-metrics-datasource",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Shows CPU pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html).\n\nThe lower the better.",
|
||||
"description": "CPU pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html). Helps troubleshoot high CPU usage or throttling.\nLower is better.\n\n- waiting: The percentage of time at least one task in the process was ready to run (runnable) but couldn't get scheduled on the CPU.\n- stalled: The percentage of time all tasks in the process (except idle ones) were unable to get CPU time — a full CPU stall.\n\nIf there's a CPU burst, it's normal to see waiting or stalled > 1%. It only becomes a concern if it consistently climbs above 5–10% and aligns with latency spikes or GC slowdowns.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -2005,7 +2004,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -2045,7 +2044,7 @@
|
||||
"type": "victoriametrics-metrics-datasource",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Shows memory pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html).\n\nThe lower the better.",
|
||||
"description": "Shows memory pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html).\nLower is better.\n\n- waiting: Time fraction where at least one thread was blocked on memory.\n- stalled: Time fraction where every thread was blocked on memory (severe pressure).\n\nIf queries slow down and both series spike, the host is likely limited by RAM or I/O throughput.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -2126,7 +2125,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -2262,7 +2261,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -2405,7 +2404,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -2527,7 +2526,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -2653,7 +2652,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -2679,7 +2678,7 @@
|
||||
"type": "victoriametrics-metrics-datasource",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Shows IO pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html).\n\nThe lower the better.",
|
||||
"description": "Shows IO pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html).\nThe lower the better.\n\n- waiting: at least one runnable thread blocked on block-`I/O` (disk, NVMe, network-storage) while others could still make progress.\n- stalled: all non-idle threads simultaneously waiting on `I/O`; no useful user code ran during these periods → true `I/O` thrashing.\n\nIf stalled > 0 while querying, it's recommended to increase queue depth on NVMe, raise blk-mq budgets, or relax cgroup I/O limits.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -2760,7 +2759,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -2884,7 +2883,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -2991,7 +2990,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -3098,7 +3097,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -3204,7 +3203,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -3307,7 +3306,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -3415,7 +3414,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -3471,7 +3470,7 @@
|
||||
"content": "See [Troubleshooting](https://docs.victoriametrics.com/victoriametrics/troubleshooting/) docs.",
|
||||
"mode": "markdown"
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"title": "",
|
||||
"transparent": true,
|
||||
"type": "text"
|
||||
@@ -3527,8 +3526,7 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -3583,7 +3581,7 @@
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -3663,8 +3661,7 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "transparent",
|
||||
"value": null
|
||||
"color": "transparent"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -3700,7 +3697,7 @@
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -3769,8 +3766,7 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -3806,7 +3802,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -3879,8 +3875,7 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -3916,7 +3911,7 @@
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -3983,8 +3978,7 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -4020,7 +4014,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -4091,8 +4085,7 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -4130,7 +4123,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -4203,8 +4196,7 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -4247,7 +4239,7 @@
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -4319,8 +4311,7 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -4356,7 +4347,7 @@
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -4426,8 +4417,7 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -4465,7 +4455,7 @@
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -4550,8 +4540,7 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -4587,7 +4576,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -4627,8 +4616,7 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -4689,7 +4677,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -4788,8 +4776,7 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -4825,7 +4812,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -4893,8 +4880,7 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -4926,7 +4912,7 @@
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -5649,6 +5635,122 @@
|
||||
},
|
||||
"id": 24,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "victoriametrics-datasource",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Shows the approx time needed to reach 100% of allowed disk capacity for at least one vmstorage node based on the following params:\n* free disk space (after -storage.minFreeDiskSpaceBytes);\n* row ingestion rate;\n* compression.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"links": [
|
||||
{
|
||||
"targetBlank": true,
|
||||
"title": "Drilldown",
|
||||
"url": "/d/oS7Bi_0Wz_vm?viewPanel=196&${__url_time_range}&${__all_variables}"
|
||||
}
|
||||
],
|
||||
"mappings": [],
|
||||
"min": 0,
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "s"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 41
|
||||
},
|
||||
"id": 113,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"mean",
|
||||
"lastNotNull",
|
||||
"min"
|
||||
],
|
||||
"displayMode": "table",
|
||||
"placement": "bottom",
|
||||
"showLegend": true,
|
||||
"sortBy": "Last *",
|
||||
"sortDesc": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "multi",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "victoriametrics-datasource",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "min((vm_free_disk_space_bytes{job=~\"$job_storage\", instance=~\"$instance\"}-vm_free_disk_space_limit_bytes{job=~\"$job_storage\", instance=~\"$instance\"}) \n/ \nignoring(path) (\n (rate(vm_rows_added_to_storage_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d]) - \n sum(rate(vm_deduplicated_samples_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d])) without (type)) * \n (\n sum(vm_data_size_bytes{job=~\"$job_storage\", instance=~\"$instance\", type!~\"indexdb.*\"}) without(type) /\n sum(vm_rows{job=~\"$job_storage\", instance=~\"$instance\", type!~\"indexdb.*\"}) without(type)\n )\n +\n rate(vm_new_timeseries_created_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d]) * \n (\n sum(vm_data_size_bytes{job=~\"$job_storage\", instance=~\"$instance\", type=\"indexdb/file\"}) /\n sum(vm_rows{job=~\"$job_storage\", instance=~\"$instance\", type=\"indexdb/file\"})\n )\n) > 0)",
|
||||
"format": "time_series",
|
||||
"interval": "",
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "min ETA",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Storage full ETA ($instance)",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "victoriametrics-metrics-datasource",
|
||||
@@ -5738,7 +5840,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -5852,7 +5954,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -6008,7 +6110,7 @@
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -6136,7 +6238,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -6299,7 +6401,7 @@
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -6422,7 +6524,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -6529,7 +6631,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -6635,7 +6737,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -6765,7 +6867,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -6918,7 +7020,7 @@
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -7043,7 +7145,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -7164,7 +7266,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -7283,7 +7385,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -7397,7 +7499,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -10104,7 +10206,7 @@
|
||||
"type": "victoriametrics-metrics-datasource",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Shows the approx time needed to reach 100% of disk capacity based on the following params:\n* free disk space (after -storage.minFreeDiskSpaceBytes);\n* row ingestion rate;\n* compression.",
|
||||
"description": "Shows the approx time needed to reach 100% of allowed disk capacity based on the following params:\n* free disk space (after -storage.minFreeDiskSpaceBytes);\n* row ingestion rate;\n* compression.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -10289,7 +10391,7 @@
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.5.0",
|
||||
"pluginVersion": "12.1.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -10536,7 +10638,7 @@
|
||||
],
|
||||
"preload": false,
|
||||
"refresh": "",
|
||||
"schemaVersion": 40,
|
||||
"schemaVersion": 41,
|
||||
"tags": [
|
||||
"victoriametrics"
|
||||
],
|
||||
@@ -10544,8 +10646,8 @@
|
||||
"list": [
|
||||
{
|
||||
"current": {
|
||||
"text": "VictoriaMetrics - cluster",
|
||||
"value": "PAF93674D0B4E9963"
|
||||
"text": "victoriametrics-metrics-datasource",
|
||||
"value": "ceuqoq3dxttkwb"
|
||||
},
|
||||
"includeAll": false,
|
||||
"name": "ds",
|
||||
@@ -10556,7 +10658,14 @@
|
||||
"type": "datasource"
|
||||
},
|
||||
{
|
||||
"current": {},
|
||||
"current": {
|
||||
"text": [
|
||||
"All"
|
||||
],
|
||||
"value": [
|
||||
"$__all"
|
||||
]
|
||||
},
|
||||
"datasource": {
|
||||
"type": "victoriametrics-metrics-datasource",
|
||||
"uid": "$ds"
|
||||
@@ -10575,7 +10684,14 @@
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"current": {},
|
||||
"current": {
|
||||
"text": [
|
||||
"All"
|
||||
],
|
||||
"value": [
|
||||
"$__all"
|
||||
]
|
||||
},
|
||||
"datasource": {
|
||||
"type": "victoriametrics-metrics-datasource",
|
||||
"uid": "$ds"
|
||||
@@ -10595,7 +10711,14 @@
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"current": {},
|
||||
"current": {
|
||||
"text": [
|
||||
"All"
|
||||
],
|
||||
"value": [
|
||||
"$__all"
|
||||
]
|
||||
},
|
||||
"datasource": {
|
||||
"type": "victoriametrics-metrics-datasource",
|
||||
"uid": "$ds"
|
||||
@@ -10615,7 +10738,14 @@
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"current": {},
|
||||
"current": {
|
||||
"text": [
|
||||
"All"
|
||||
],
|
||||
"value": [
|
||||
"$__all"
|
||||
]
|
||||
},
|
||||
"datasource": {
|
||||
"type": "victoriametrics-metrics-datasource",
|
||||
"uid": "$ds"
|
||||
@@ -10636,7 +10766,14 @@
|
||||
},
|
||||
{
|
||||
"allValue": ".*",
|
||||
"current": {},
|
||||
"current": {
|
||||
"text": [
|
||||
"All"
|
||||
],
|
||||
"value": [
|
||||
"$__all"
|
||||
]
|
||||
},
|
||||
"datasource": {
|
||||
"type": "victoriametrics-metrics-datasource",
|
||||
"uid": "$ds"
|
||||
@@ -10686,6 +10823,5 @@
|
||||
"timezone": "",
|
||||
"title": "VictoriaMetrics - cluster (VM)",
|
||||
"uid": "oS7Bi_0Wz_vm",
|
||||
"version": 1,
|
||||
"weekStart": ""
|
||||
"version": 1
|
||||
}
|
||||
|
||||
@@ -1450,7 +1450,7 @@
|
||||
"type": "victoriametrics-metrics-datasource",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Percentage of used RSS memory (resident).\nThe RSS memory shows the amount of memory recently accessed by the application. It includes anonymous memory and data from recently accessed files (aka page cache).\nThe application's performance will significantly degrade when memory usage is close to 100%.",
|
||||
"description": "How much resident memory (RAM) the process is actually using, compared to its allowed container or system limit.\n\n- Good: Below 70% most of the time, maybe spiking a bit under load.\n- Bad: Above 90% for more than 5 minutes = risk of out-of-memory (OOM) kill.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -2089,7 +2089,7 @@
|
||||
"type": "victoriametrics-metrics-datasource",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Shows CPU pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html).\n\nThe lower the better.",
|
||||
"description": "CPU pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html). Helps troubleshoot high CPU usage or throttling.\nLower is better.\n\n- waiting: The percentage of time at least one task in the process was ready to run (runnable) but couldn't get scheduled on the CPU.\n- stalled: The percentage of time all tasks in the process (except idle ones) were unable to get CPU time — a full CPU stall.\n\nIf there's a CPU burst, it's normal to see waiting or stalled > 1%. It only becomes a concern if it consistently climbs above 5–10% and aligns with latency spikes or GC slowdowns.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -2211,7 +2211,7 @@
|
||||
"type": "victoriametrics-metrics-datasource",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Shows memory pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html).\n\nThe lower the better.",
|
||||
"description": "Shows memory pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html).\nLower is better.\n\n- waiting: Time fraction where at least one thread was blocked on memory.\n- stalled: Time fraction where every thread was blocked on memory (severe pressure).\n\nIf queries slow down and both series spike, the host is likely limited by RAM or I/O throughput.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -4900,7 +4900,7 @@
|
||||
"type": "victoriametrics-metrics-datasource",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Shows the approx time needed to reach 100% of disk capacity based on the following params:\n* free disk space (after -storage.minFreeDiskSpaceBytes);\n* row ingestion rate;\n* compression.",
|
||||
"description": "Shows the approx time needed to reach 100% of allowed disk capacity based on the following params:\n* free disk space (after -storage.minFreeDiskSpaceBytes);\n* row ingestion rate;\n* compression.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
|
||||
@@ -1675,7 +1675,7 @@
|
||||
"type": "victoriametrics-metrics-datasource",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Percentage of used RSS memory (resident).\nThe RSS memory shows the amount of memory recently accessed by the application. It includes anonymous memory and data from recently accessed files (aka page cache).\nThe application's performance will significantly degrade when memory usage is close to 100%.\n\nClick on the line and choose Drilldown to show memory usage per instance",
|
||||
"description": "How much resident memory (RAM) the process is actually using, compared to its allowed container or system limit.\n\n- Good: Below 70% most of the time, maybe spiking a bit under load.\n- Bad: Above 90% for more than 5 minutes = risk of out-of-memory (OOM) kill.\n\nClick on the line and choose Drilldown to show memory usage per instance",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -2015,7 +2015,7 @@
|
||||
"type": "victoriametrics-metrics-datasource",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Shows CPU pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html).\n\nThe lower the better.",
|
||||
"description": "CPU pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html). Helps troubleshoot high CPU usage or throttling.\nLower is better.\n\n- waiting: The percentage of time at least one task in the process was ready to run (runnable) but couldn't get scheduled on the CPU.\n- stalled: The percentage of time all tasks in the process (except idle ones) were unable to get CPU time — a full CPU stall.\n\nIf there's a CPU burst, it's normal to see waiting or stalled > 1%. It only becomes a concern if it consistently climbs above 5–10% and aligns with latency spikes or GC slowdowns.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -2137,7 +2137,7 @@
|
||||
"type": "victoriametrics-metrics-datasource",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Shows memory pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html).\n\nThe lower the better.",
|
||||
"description": "Shows memory pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html).\nLower is better.\n\n- waiting: Time fraction where at least one thread was blocked on memory.\n- stalled: Time fraction where every thread was blocked on memory (severe pressure).\n\nIf queries slow down and both series spike, the host is likely limited by RAM or I/O throughput.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
|
||||
@@ -1119,7 +1119,7 @@
|
||||
"type": "victoriametrics-metrics-datasource",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "The percentage of used RSS memory\n\nIf you think that usage is abnormal or unexpected, please file an issue and attach memory profile if possible.",
|
||||
"description": "How much resident memory (RAM) the process is actually using, compared to its allowed container or system limit.\n\n- Good: Below 70% most of the time, maybe spiking a bit under load.\n- Bad: Above 90% for more than 5 minutes = risk of out-of-memory (OOM) kill.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -1707,7 +1707,7 @@
|
||||
"type": "victoriametrics-metrics-datasource",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Shows CPU pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html).\n\nThe lower the better.",
|
||||
"description": "CPU pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html). Helps troubleshoot high CPU usage or throttling.\nLower is better.\n\n- waiting: The percentage of time at least one task in the process was ready to run (runnable) but couldn't get scheduled on the CPU.\n- stalled: The percentage of time all tasks in the process (except idle ones) were unable to get CPU time — a full CPU stall.\n\nIf there's a CPU burst, it's normal to see waiting or stalled > 1%. It only becomes a concern if it consistently climbs above 5–10% and aligns with latency spikes or GC slowdowns.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -1828,7 +1828,7 @@
|
||||
"type": "victoriametrics-metrics-datasource",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Shows memory pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html).\n\nThe lower the better.",
|
||||
"description": "Shows memory pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html).\nLower is better.\n\n- waiting: Time fraction where at least one thread was blocked on memory.\n- stalled: Time fraction where every thread was blocked on memory (severe pressure).\n\nIf queries slow down and both series spike, the host is likely limited by RAM or I/O throughput.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
|
||||
@@ -1674,7 +1674,7 @@
|
||||
"type": "prometheus",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Percentage of used RSS memory (resident).\nThe RSS memory shows the amount of memory recently accessed by the application. It includes anonymous memory and data from recently accessed files (aka page cache).\nThe application's performance will significantly degrade when memory usage is close to 100%.\n\nClick on the line and choose Drilldown to show memory usage per instance",
|
||||
"description": "How much resident memory (RAM) the process is actually using, compared to its allowed container or system limit.\n\n- Good: Below 70% most of the time, maybe spiking a bit under load.\n- Bad: Above 90% for more than 5 minutes = risk of out-of-memory (OOM) kill.\n\nClick on the line and choose Drilldown to show memory usage per instance",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -2014,7 +2014,7 @@
|
||||
"type": "prometheus",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Shows CPU pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html).\n\nThe lower the better.",
|
||||
"description": "CPU pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html). Helps troubleshoot high CPU usage or throttling.\nLower is better.\n\n- waiting: The percentage of time at least one task in the process was ready to run (runnable) but couldn't get scheduled on the CPU.\n- stalled: The percentage of time all tasks in the process (except idle ones) were unable to get CPU time — a full CPU stall.\n\nIf there's a CPU burst, it's normal to see waiting or stalled > 1%. It only becomes a concern if it consistently climbs above 5–10% and aligns with latency spikes or GC slowdowns.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -2136,7 +2136,7 @@
|
||||
"type": "prometheus",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Shows memory pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html).\n\nThe lower the better.",
|
||||
"description": "Shows memory pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html).\nLower is better.\n\n- waiting: Time fraction where at least one thread was blocked on memory.\n- stalled: Time fraction where every thread was blocked on memory (severe pressure).\n\nIf queries slow down and both series spike, the host is likely limited by RAM or I/O throughput.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
|
||||
@@ -1118,7 +1118,7 @@
|
||||
"type": "prometheus",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "The percentage of used RSS memory\n\nIf you think that usage is abnormal or unexpected, please file an issue and attach memory profile if possible.",
|
||||
"description": "How much resident memory (RAM) the process is actually using, compared to its allowed container or system limit.\n\n- Good: Below 70% most of the time, maybe spiking a bit under load.\n- Bad: Above 90% for more than 5 minutes = risk of out-of-memory (OOM) kill.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -1706,7 +1706,7 @@
|
||||
"type": "prometheus",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Shows CPU pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html).\n\nThe lower the better.",
|
||||
"description": "CPU pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html). Helps troubleshoot high CPU usage or throttling.\nLower is better.\n\n- waiting: The percentage of time at least one task in the process was ready to run (runnable) but couldn't get scheduled on the CPU.\n- stalled: The percentage of time all tasks in the process (except idle ones) were unable to get CPU time — a full CPU stall.\n\nIf there's a CPU burst, it's normal to see waiting or stalled > 1%. It only becomes a concern if it consistently climbs above 5–10% and aligns with latency spikes or GC slowdowns.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -1827,7 +1827,7 @@
|
||||
"type": "prometheus",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"description": "Shows memory pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html).\n\nThe lower the better.",
|
||||
"description": "Shows memory pressure based on [Pressure Stall Information](https://docs.kernel.org/accounting/psi.html).\nLower is better.\n\n- waiting: Time fraction where at least one thread was blocked on memory.\n- stalled: Time fraction where every thread was blocked on memory (severe pressure).\n\nIf queries slow down and both series spike, the host is likely limited by RAM or I/O throughput.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
|
||||
@@ -7,7 +7,8 @@ ROOT_IMAGE ?= alpine:3.22.1
|
||||
ROOT_IMAGE_SCRATCH ?= scratch
|
||||
CERTS_IMAGE := alpine:3.22.1
|
||||
|
||||
GO_BUILDER_IMAGE := golang:1.25.0-alpine
|
||||
GO_BUILDER_IMAGE := golang:1.25.0
|
||||
|
||||
BUILDER_IMAGE := local/builder:2.0.0-$(shell echo $(GO_BUILDER_IMAGE) | tr :/ __)-1
|
||||
BASE_IMAGE := local/base:1.1.4-$(shell echo $(ROOT_IMAGE) | tr :/ __)-$(shell echo $(CERTS_IMAGE) | tr :/ __)
|
||||
DOCKER ?= docker
|
||||
@@ -43,7 +44,7 @@ app-via-docker: package-builder
|
||||
$(BUILDER_IMAGE) \
|
||||
go build $(RACE) -trimpath -buildvcs=false \
|
||||
-ldflags "-extldflags '-static' $(GO_BUILDINFO)" \
|
||||
-tags 'netgo osusergo musl $(EXTRA_GO_BUILD_TAGS)' \
|
||||
-tags 'netgo osusergo $(EXTRA_GO_BUILD_TAGS)' \
|
||||
-o bin/$(APP_NAME)$(APP_SUFFIX)-prod $(PKG_PREFIX)/app/$(APP_NAME)
|
||||
|
||||
app-via-docker-windows: package-builder
|
||||
@@ -158,11 +159,11 @@ app-via-docker-pure:
|
||||
APP_SUFFIX='-pure' DOCKER_OPTS='--env CGO_ENABLED=0' $(MAKE) app-via-docker
|
||||
|
||||
app-via-docker-linux-amd64:
|
||||
EXTRA_DOCKER_ENVS='CC=/opt/cross-builder/x86_64-linux-musl-cross/bin/x86_64-linux-musl-gcc' \
|
||||
EXTRA_DOCKER_ENVS='CC=x86_64-linux-gnu-gcc' \
|
||||
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 $(MAKE) app-via-docker-goos-goarch
|
||||
|
||||
app-via-docker-linux-arm64:
|
||||
EXTRA_DOCKER_ENVS='CC=/opt/cross-builder/aarch64-linux-musl-cross/bin/aarch64-linux-musl-gcc' \
|
||||
EXTRA_DOCKER_ENVS='CC=aarch64-linux-gnu-gcc' \
|
||||
CGO_ENABLED=1 GOOS=linux GOARCH=arm64 $(MAKE) app-via-docker-goos-goarch
|
||||
|
||||
app-via-docker-linux-arm:
|
||||
@@ -201,11 +202,11 @@ package-via-docker-pure:
|
||||
APP_SUFFIX='-pure' DOCKER_OPTS='--env CGO_ENABLED=0' $(MAKE) package-via-docker
|
||||
|
||||
package-via-docker-amd64:
|
||||
EXTRA_DOCKER_ENVS='CC=/opt/cross-builder/x86_64-linux-musl-cross/bin/x86_64-linux-musl-gcc' \
|
||||
EXTRA_DOCKER_ENVS='CC=x86_64-linux-gnu-gcc' \
|
||||
CGO_ENABLED=1 GOARCH=amd64 $(MAKE) package-via-docker-goarch
|
||||
|
||||
package-via-docker-arm64:
|
||||
EXTRA_DOCKER_ENVS='CC=/opt/cross-builder/aarch64-linux-musl-cross/bin/aarch64-linux-musl-gcc' \
|
||||
EXTRA_DOCKER_ENVS='CC=aarch64-linux-gnu-gcc' \
|
||||
CGO_ENABLED=1 GOARCH=arm64 $(MAKE) package-via-docker-goarch
|
||||
|
||||
package-via-docker-arm:
|
||||
|
||||
@@ -40,7 +40,11 @@ The communication scheme between components is the following:
|
||||
and recording rules results back to `vmagent`;
|
||||
* [alertmanager](#alertmanager) is configured to receive notifications from `vmalert`.
|
||||
|
||||
<img alt="VictoriaMetrics single-server deployment" width="500" src="assets/vm-single-server.png">
|
||||
<picture>
|
||||
<source srcset="assets/vm-single-server-dark.png" media="(prefers-color-scheme: dark)">
|
||||
<source srcset="assets/vm-single-server-light.png" media="(prefers-color-scheme: light)">
|
||||
<img src="assets/vm-single-server-light.png" alt="VictoriaMetrics single-server deployment" width="500" >
|
||||
</picture>
|
||||
|
||||
To access Grafana use link [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
@@ -78,7 +82,11 @@ The communication scheme between components is the following:
|
||||
and recording rules to `vmagent`;
|
||||
* [alertmanager](#alertmanager) is configured to receive notifications from `vmalert`.
|
||||
|
||||
<img alt="VictoriaMetrics cluster deployment" width="500" src="assets/vm-cluster.png">
|
||||
<picture>
|
||||
<source srcset="assets/vm-cluster-dark.png" media="(prefers-color-scheme: dark)">
|
||||
<source srcset="assets/vm-cluster-light.png" media="(prefers-color-scheme: light)">
|
||||
<img src="assets/vm-cluster-light.png" alt="VictoriaMetrics cluster deployment" width="500" src="assets/vm-cluster-light.png" >
|
||||
</picture>
|
||||
|
||||
To access Grafana use link [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 99 KiB |
BIN
deployment/docker/assets/vm-cluster-dark.png
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
deployment/docker/assets/vm-cluster-light.png
Normal file
|
After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 152 KiB |
BIN
deployment/docker/assets/vm-single-server-dark.png
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
deployment/docker/assets/vm-single-server-light.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 93 KiB |
@@ -1,5 +1,5 @@
|
||||
# balance load among vmselects
|
||||
# see https://docs.victoriametrics.com/victoriametrics/vmauth/#load-balancing
|
||||
# balance load among vminsert and vmselect instances,
|
||||
# see https://docs.victoriametrics.com/victoriametrics/vmauth/#load-balancing.
|
||||
# Note: if username and password are changes, please update the Grafana datasource configuration
|
||||
# and check other places where these credentials are used.
|
||||
users:
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
ARG go_builder_image=non-existing
|
||||
FROM $go_builder_image
|
||||
STOPSIGNAL SIGINT
|
||||
RUN apk add git gcc musl-dev make wget --no-cache && \
|
||||
mkdir /opt/cross-builder && \
|
||||
cd /opt/cross-builder && \
|
||||
for arch in aarch64 x86_64; do \
|
||||
wget \
|
||||
https://github.com/VictoriaMetrics/muslcc-mirror/releases/download/v1.0.0/${arch}-linux-musl-cross.tgz \
|
||||
-O /opt/cross-builder/${arch}-musl.tgz \
|
||||
--no-verbose && \
|
||||
tar zxf ${arch}-musl.tgz -C ./ && \
|
||||
rm /opt/cross-builder/${arch}-musl.tgz; \
|
||||
done
|
||||
RUN apt update && apt install -y \
|
||||
gcc-x86-64-linux-gnu \
|
||||
gcc-aarch64-linux-gnu
|
||||
|
||||
@@ -3,7 +3,7 @@ services:
|
||||
# It scrapes targets defined in --promscrape.config
|
||||
# And forward them to --remoteWrite.url
|
||||
vmagent:
|
||||
image: victoriametrics/vmagent:v1.124.0
|
||||
image: victoriametrics/vmagent:v1.125.1
|
||||
depends_on:
|
||||
- "vmauth"
|
||||
ports:
|
||||
@@ -14,10 +14,12 @@ services:
|
||||
command:
|
||||
- "--promscrape.config=/etc/prometheus/prometheus.yml"
|
||||
- "--remoteWrite.url=http://vmauth:8427/insert/0/prometheus/api/v1/write"
|
||||
- "--remoteWrite.basicAuth.username=foo"
|
||||
- "--remoteWrite.basicAuth.password=bar"
|
||||
restart: always
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:12.0.2
|
||||
image: grafana/grafana:12.1.1
|
||||
depends_on:
|
||||
- "vmauth"
|
||||
ports:
|
||||
@@ -35,14 +37,14 @@ services:
|
||||
# vmstorage shards. Each shard receives 1/N of all metrics sent to vminserts,
|
||||
# where N is number of vmstorages (2 in this case).
|
||||
vmstorage-1:
|
||||
image: victoriametrics/vmstorage:v1.124.0-cluster
|
||||
image: victoriametrics/vmstorage:v1.125.1-cluster
|
||||
volumes:
|
||||
- strgdata-1:/storage
|
||||
command:
|
||||
- "--storageDataPath=/storage"
|
||||
restart: always
|
||||
vmstorage-2:
|
||||
image: victoriametrics/vmstorage:v1.124.0-cluster
|
||||
image: victoriametrics/vmstorage:v1.125.1-cluster
|
||||
volumes:
|
||||
- strgdata-2:/storage
|
||||
command:
|
||||
@@ -52,7 +54,7 @@ services:
|
||||
# vminsert is ingestion frontend. It receives metrics pushed by vmagent,
|
||||
# pre-process them and distributes across configured vmstorage shards.
|
||||
vminsert-1:
|
||||
image: victoriametrics/vminsert:v1.124.0-cluster
|
||||
image: victoriametrics/vminsert:v1.125.1-cluster
|
||||
depends_on:
|
||||
- "vmstorage-1"
|
||||
- "vmstorage-2"
|
||||
@@ -61,7 +63,7 @@ services:
|
||||
- "--storageNode=vmstorage-2:8400"
|
||||
restart: always
|
||||
vminsert-2:
|
||||
image: victoriametrics/vminsert:v1.124.0-cluster
|
||||
image: victoriametrics/vminsert:v1.125.1-cluster
|
||||
depends_on:
|
||||
- "vmstorage-1"
|
||||
- "vmstorage-2"
|
||||
@@ -73,7 +75,7 @@ services:
|
||||
# vmselect is a query fronted. It serves read queries in MetricsQL or PromQL.
|
||||
# vmselect collects results from configured `--storageNode` shards.
|
||||
vmselect-1:
|
||||
image: victoriametrics/vmselect:v1.124.0-cluster
|
||||
image: victoriametrics/vmselect:v1.125.1-cluster
|
||||
depends_on:
|
||||
- "vmstorage-1"
|
||||
- "vmstorage-2"
|
||||
@@ -83,7 +85,7 @@ services:
|
||||
- "--vmalert.proxyURL=http://vmalert:8880"
|
||||
restart: always
|
||||
vmselect-2:
|
||||
image: victoriametrics/vmselect:v1.124.0-cluster
|
||||
image: victoriametrics/vmselect:v1.125.1-cluster
|
||||
depends_on:
|
||||
- "vmstorage-1"
|
||||
- "vmstorage-2"
|
||||
@@ -98,7 +100,7 @@ services:
|
||||
# read requests from Grafana, vmui, vmalert among vmselects.
|
||||
# It can be used as an authentication proxy.
|
||||
vmauth:
|
||||
image: victoriametrics/vmauth:v1.124.0
|
||||
image: victoriametrics/vmauth:v1.125.1
|
||||
depends_on:
|
||||
- "vmselect-1"
|
||||
- "vmselect-2"
|
||||
@@ -112,7 +114,7 @@ services:
|
||||
|
||||
# vmalert executes alerting and recording rules
|
||||
vmalert:
|
||||
image: victoriametrics/vmalert:v1.124.0
|
||||
image: victoriametrics/vmalert:v1.125.1
|
||||
depends_on:
|
||||
- "vmauth"
|
||||
ports:
|
||||
|
||||
@@ -3,7 +3,7 @@ services:
|
||||
# It scrapes targets defined in --promscrape.config
|
||||
# And forward them to --remoteWrite.url
|
||||
vmagent:
|
||||
image: victoriametrics/vmagent:v1.124.0
|
||||
image: victoriametrics/vmagent:v1.125.1
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
@@ -18,7 +18,7 @@ services:
|
||||
# VictoriaMetrics instance, a single process responsible for
|
||||
# storing metrics and serve read requests.
|
||||
victoriametrics:
|
||||
image: victoriametrics/victoria-metrics:v1.124.0
|
||||
image: victoriametrics/victoria-metrics:v1.125.1
|
||||
ports:
|
||||
- 8428:8428
|
||||
- 8089:8089
|
||||
@@ -38,7 +38,7 @@ services:
|
||||
restart: always
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:12.0.2
|
||||
image: grafana/grafana:12.1.1
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
@@ -54,7 +54,7 @@ services:
|
||||
|
||||
# vmalert executes alerting and recording rules
|
||||
vmalert:
|
||||
image: victoriametrics/vmalert:v1.124.0
|
||||
image: victoriametrics/vmalert:v1.125.1
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
- "alertmanager"
|
||||
|
||||
@@ -17,7 +17,8 @@ scrape_configs:
|
||||
- job_name: vminsert
|
||||
static_configs:
|
||||
- targets:
|
||||
- vminsert:8480
|
||||
- vminsert-1:8480
|
||||
- vminsert-2:8480
|
||||
- job_name: vmselect
|
||||
static_configs:
|
||||
- targets:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
vmagent:
|
||||
image: victoriametrics/vmagent:v1.124.0
|
||||
image: victoriametrics/vmagent:v1.125.1
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
@@ -14,7 +14,7 @@ services:
|
||||
restart: always
|
||||
|
||||
victoriametrics:
|
||||
image: victoriametrics/victoria-metrics:v1.124.0
|
||||
image: victoriametrics/victoria-metrics:v1.125.1
|
||||
ports:
|
||||
- 8428:8428
|
||||
volumes:
|
||||
@@ -27,7 +27,7 @@ services:
|
||||
restart: always
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:12.0.2
|
||||
image: grafana/grafana:12.1.1
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
@@ -40,7 +40,7 @@ services:
|
||||
restart: always
|
||||
|
||||
vmalert:
|
||||
image: victoriametrics/vmalert:v1.124.0
|
||||
image: victoriametrics/vmalert:v1.125.1
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
|
||||
@@ -58,7 +58,7 @@ services:
|
||||
- ./vmsingle/promscrape.yml:/promscrape.yml
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:12.0.2
|
||||
image: grafana/grafana:12.1.1
|
||||
depends_on: [vmsingle]
|
||||
ports:
|
||||
- 3000:3000
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
build:
|
||||
list: never
|
||||
publishResources: false
|
||||
render: never
|
||||
sitemap:
|
||||
disable: true
|
||||
---
|
||||
In today's fast-paced and complex landscape of system monitoring, [VictoriaMetrics Anomaly Detection](https://victoriametrics.com/products/enterprise/anomaly-detection/) (`vmanomaly`), part of our [Enterprise offering](https://victoriametrics.com/products/enterprise/), serves as a **powerful observability tool** for SREs and DevOps teams. It **automates the detection of anomalies in time-series data**, reducing manual efforts required to identify abnormal system behavior.
|
||||
|
||||
Unlike traditional threshold-based alerting, which relies on **raw metric values** and requires constant tuning and maintenance of thresholds and alerting rules, `vmanomaly` introduces a **unified, interpretable [anomaly score](https://docs.victoriametrics.com/anomaly-detection/faq/#what-is-anomaly-score)** - a **de-trended, de-seasonalized metric** generated through machine learning. This approach eliminates the need for frequent manual adjustments by enabling **stable, long-term static thresholds (as simple as `anomaly_score > 1`)** that remain effective over time through continuous model retraining.
|
||||
|
||||